diff --git a/cli/src/adapters/registry.ts b/cli/src/adapters/registry.ts index 7dfa12db95..65ac44210c 100644 --- a/cli/src/adapters/registry.ts +++ b/cli/src/adapters/registry.ts @@ -69,6 +69,11 @@ const hermesLocalCLIAdapter: CLIAdapterModule = { formatStdoutEvent: printHermesStreamEvent, }; +const googleVertexCLIAdapter: CLIAdapterModule = { + type: "google_vertex", + formatStdoutEvent: printHermesStreamEvent, +}; + const openclawGatewayCLIAdapter: CLIAdapterModule = { type: "openclaw_gateway", formatStdoutEvent: printOpenClawGatewayStreamEvent, @@ -87,6 +92,7 @@ const adaptersByType = new Map( kimiLocalCLIAdapter, hermesGatewayCLIAdapter, hermesLocalCLIAdapter, + googleVertexCLIAdapter, openclawGatewayCLIAdapter, processCLIAdapter, httpCLIAdapter, diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index 3042456a95..014be960ed 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -158,7 +158,7 @@ Invariant: every business record belongs to exactly one company. - `status` enum: `active | paused | idle | running | error | pending_approval | terminated` - `reports_to` uuid fk `agents.id` null - `capabilities` text null -- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `cursor`, `hermes_local`, `hermes_gateway`, and `openclaw_gateway` +- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `google_vertex`, `opencode_local`, `pi_local`, `cursor`, `hermes_local`, `hermes_gateway`, and `openclaw_gateway` - `adapter_config` jsonb not null - `runtime_config` jsonb not null default `{}`; contains Paperclip runtime policy such as heartbeat scheduling and debug settings - `default_environment_id` uuid fk `environments.id` null diff --git a/docs/adapters/google-vertex.md b/docs/adapters/google-vertex.md new file mode 100644 index 0000000000..1671332d7b --- /dev/null +++ b/docs/adapters/google-vertex.md @@ -0,0 +1,32 @@ +--- +title: "Google Vertex AI" +description: "Run Paperclip agents on Gemini models through Google Cloud Vertex AI" +--- + +The `google_vertex` adapter runs the local Hermes agent harness with its provider fixed to Google Vertex AI. It uses Gemini models through Vertex's OpenAI-compatible endpoint while retaining Hermes tools, skills, and session continuity. + +## Prerequisites + +- Hermes Agent 0.21.2 or newer on the execution host +- A Google Cloud project with the Vertex AI API enabled and billing active +- An identity with the `roles/aiplatform.user` role +- Either a service-account JSON file or Google Application Default Credentials (ADC) + +## Configure an agent + +Choose **Google Vertex AI** when creating an agent, then set: + +- **Model**: a Vertex model ID such as `google/gemini-3.8-flash` +- **Google Cloud project ID**: optional when embedded in the credential +- **Vertex region**: defaults to `global`; Gemini 3 preview models require it +- **Service-account JSON path**: optional absolute path on the execution host; leave blank to use ADC + +Vertex uses OAuth2 rather than a static API key. Paperclip stores only the credential-file path and non-secret routing configuration. Hermes mints and refreshes short-lived access tokens at runtime. + +For local development with ADC, configure Google Cloud on the same host that executes the agent: + +```bash +gcloud auth application-default login +``` + +For servers, place the service-account JSON outside the repository, restrict its filesystem permissions, and configure its absolute path in the adapter. Do not paste service-account JSON or access tokens into agent configuration fields. diff --git a/docs/adapters/overview.md b/docs/adapters/overview.md index 1f5717cffb..dcb661527b 100644 --- a/docs/adapters/overview.md +++ b/docs/adapters/overview.md @@ -21,6 +21,7 @@ When a heartbeat fires, Paperclip: | [Claude Code](/adapters/claude-local) | `claude_local` | Runs Claude Code CLI locally, with a native ACP engine when available | | [Codex](/adapters/codex-local) | `codex_local` | Runs OpenAI Codex CLI locally, with a native ACP engine when available | | [Gemini CLI](/adapters/gemini-local) | `gemini_local` | Runs Gemini CLI locally (experimental — adapter package exists, not yet in stable type enum) | +| [Google Vertex AI](/adapters/google-vertex) | `google_vertex` | Runs Hermes with Gemini on Vertex AI using OAuth2 service-account credentials or ADC | | [Kimi Code CLI](/adapters/kimi-local) | `kimi_local` | Runs Kimi Code CLI locally through ACP, with explicitly selectable headless `-p` mode | | OpenCode | `opencode_local` | Runs OpenCode CLI locally (multi-provider `provider/model`) | | Cursor | `cursor` | Runs Cursor in background mode | diff --git a/docs/agents-runtime.md b/docs/agents-runtime.md index 0da495b7f1..f80b47d425 100644 --- a/docs/agents-runtime.md +++ b/docs/agents-runtime.md @@ -41,6 +41,7 @@ Built-in adapters: - `pi_local`: runs an embedded Pi agent locally - `hermes_local`: starts your local `hermes` CLI through `@paperclipai/hermes-paperclip-adapter` - `hermes_gateway`: calls an already-running Hermes API server through `@paperclipai/hermes-paperclip-adapter/gateway` +- `google_vertex`: runs Hermes locally with its provider fixed to Google Vertex AI, using OAuth2 service-account credentials or ADC - `openclaw_gateway`: connects to an OpenClaw gateway endpoint - `process`: generic shell command adapter - `http`: calls an external HTTP endpoint @@ -49,7 +50,7 @@ External plugin adapters (install via the adapter manager or API): - `droid_local`: runs your local Factory Droid CLI (`@henkey/droid-paperclip-adapter`) -For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `droid_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine. For `hermes_gateway`, Paperclip assumes the Hermes API server is already running, reachable from the Paperclip server, and configured with an API key. The older `@paperclipai/adapter-hermes-gateway` npm package is only a deprecated compatibility shim; the adapter type remains `hermes_gateway`. +For local CLI adapters (`claude_local`, `codex_local`, `opencode_local`, `hermes_local`, `google_vertex`, `droid_local`), Paperclip assumes the CLI is already installed and authenticated on the host machine. `google_vertex` uses Hermes with Vertex OAuth2 credentials rather than a static model-provider key. For `hermes_gateway`, Paperclip assumes the Hermes API server is already running, reachable from the Paperclip server, and configured with an API key. The older `@paperclipai/adapter-hermes-gateway` npm package is only a deprecated compatibility shim; the adapter type remains `hermes_gateway`. ## 3.2 Runtime behavior diff --git a/docs/docs.json b/docs/docs.json index 8538840176..2d020fbd85 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -102,6 +102,7 @@ "adapters/overview", "adapters/claude-local", "adapters/codex-local", + "adapters/google-vertex", "adapters/process", "adapters/http", "adapters/external-adapters", diff --git a/packages/adapter-utils/src/session-compaction.ts b/packages/adapter-utils/src/session-compaction.ts index 3764155c40..968373c632 100644 --- a/packages/adapter-utils/src/session-compaction.ts +++ b/packages/adapter-utils/src/session-compaction.ts @@ -43,6 +43,7 @@ export const LEGACY_SESSIONED_ADAPTER_TYPES = new Set([ "cursor", "gemini_local", "hermes_local", + "google_vertex", "kimi_local", "opencode_local", "pi_local", @@ -94,6 +95,11 @@ export const ADAPTER_SESSION_MANAGEMENT: Record { diff --git a/packages/adapters/hermes/README.md b/packages/adapters/hermes/README.md index 64f6ae39dd..e67527ec76 100644 --- a/packages/adapters/hermes/README.md +++ b/packages/adapters/hermes/README.md @@ -4,10 +4,11 @@ A [Paperclip](https://paperclip.ing) adapter package that lets you run [Hermes A Hermes Agent is a full-featured AI agent by [Nous Research](https://nousresearch.com) with 30+ native tools, persistent memory, session persistence, 80+ skills, MCP support, and multi-provider model access. -This package owns both built-in Hermes adapter types: +This package owns three built-in Hermes adapter types: - `hermes_local` runs the local Hermes CLI as a child process. The package root exports remain compatible with the original local adapter. - `hermes_gateway` calls an already-running Hermes API server over HTTP/SSE. Gateway entrypoints live under the `./gateway` export namespace. +- `google_vertex` runs the local Hermes CLI with the provider fixed to Google Vertex AI. Vertex entrypoints live under the `./vertex` export namespace. Choose `hermes_local` when Paperclip and Hermes run on the same trusted host and Paperclip should start `hermes chat` for each heartbeat. Choose diff --git a/packages/adapters/hermes/package.json b/packages/adapters/hermes/package.json index b097155e62..e9c039eabd 100644 --- a/packages/adapters/hermes/package.json +++ b/packages/adapters/hermes/package.json @@ -32,7 +32,10 @@ "./gateway/server": "./src/gateway/server/index.ts", "./gateway/ui": "./src/gateway/ui/index.ts", "./gateway/cli": "./src/gateway/cli/index.ts", - "./gateway/ui-parser": "./gateway-ui-parser.cjs" + "./gateway/ui-parser": "./gateway-ui-parser.cjs", + "./vertex": "./src/vertex/index.ts", + "./vertex/server": "./src/vertex/server/index.ts", + "./vertex/ui": "./src/vertex/ui/index.ts" }, "paperclip": { "adapterUiParser": "1.0.0" @@ -73,7 +76,19 @@ "types": "./dist/gateway/cli/index.d.ts", "import": "./dist/gateway/cli/index.js" }, - "./gateway/ui-parser": "./gateway-ui-parser.cjs" + "./gateway/ui-parser": "./gateway-ui-parser.cjs", + "./vertex": { + "types": "./dist/vertex/index.d.ts", + "import": "./dist/vertex/index.js" + }, + "./vertex/server": { + "types": "./dist/vertex/server/index.d.ts", + "import": "./dist/vertex/server/index.js" + }, + "./vertex/ui": { + "types": "./dist/vertex/ui/index.d.ts", + "import": "./dist/vertex/ui/index.js" + } }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/adapters/hermes/src/index.test.ts b/packages/adapters/hermes/src/index.test.ts index fec9fdf724..716b0559c3 100644 --- a/packages/adapters/hermes/src/index.test.ts +++ b/packages/adapters/hermes/src/index.test.ts @@ -6,6 +6,7 @@ import { expect, test } from "vitest"; import { createHermesGatewayServerAdapter, createHermesLocalServerAdapter, + createGoogleVertexServerAdapter, createServerAdapter, hermesGatewayType, } from "./index.js"; @@ -42,6 +43,21 @@ test("root package export keeps explicit local and gateway adapter factories", ( expect(gatewayAdapter.supportsInstructionsBundle).toBe(false); }); +test("root package export exposes the Google Vertex AI adapter factory", () => { + const adapter = createGoogleVertexServerAdapter(); + + expect(adapter.type).toBe("google_vertex"); + expect(adapter.models?.[0]).toEqual({ + id: "google/gemini-3.8-flash", + label: "Gemini 3.8 Flash", + }); + expect(adapter.supportsLocalAgentJwt).toBe(true); + expect(adapter.supportsInstructionsBundle).toBe(true); + expect(typeof adapter.execute).toBe("function"); + expect(typeof adapter.testEnvironment).toBe("function"); + expect(typeof adapter.getConfigSchema).toBe("function"); +}); + test("gateway subpath export exposes the Hermes Gateway adapter entrypoint", () => { const adapter = createGatewayServerAdapterFromSubpath(); diff --git a/packages/adapters/hermes/src/index.ts b/packages/adapters/hermes/src/index.ts index 3494bf5970..ed50e6ee82 100644 --- a/packages/adapters/hermes/src/index.ts +++ b/packages/adapters/hermes/src/index.ts @@ -36,6 +36,13 @@ export { models as hermesGatewayModels, type as hermesGatewayType, } from "./gateway/index.js"; +export { + createServerAdapter as createGoogleVertexServerAdapter, + agentConfigurationDoc as googleVertexAgentConfigurationDoc, + label as googleVertexLabel, + models as googleVertexModels, + type as googleVertexType, +} from "./vertex/index.js"; /** * Models available through Hermes Agent. diff --git a/packages/adapters/hermes/src/shared/constants.ts b/packages/adapters/hermes/src/shared/constants.ts index 25f57cd86e..b90661d4db 100644 --- a/packages/adapters/hermes/src/shared/constants.ts +++ b/packages/adapters/hermes/src/shared/constants.ts @@ -45,6 +45,7 @@ export const VALID_PROVIDERS = [ "minimax", "minimax-cn", "kilocode", + "vertex", ] as const; /** diff --git a/packages/adapters/hermes/src/vertex/index.ts b/packages/adapters/hermes/src/vertex/index.ts new file mode 100644 index 0000000000..7513c52e3b --- /dev/null +++ b/packages/adapters/hermes/src/vertex/index.ts @@ -0,0 +1,66 @@ +import type { ServerAdapterModule } from "@paperclipai/adapter-utils"; + +import { resolveHermesCommand } from "../server/execute.js"; +import { listHermesSkills, syncHermesSkills } from "../server/skills.js"; +import { sessionCodec } from "../server/index.js"; +import { + executeGoogleVertex, + getGoogleVertexConfigSchema, + testGoogleVertexEnvironment, +} from "./server/index.js"; +import { + GOOGLE_VERTEX_ADAPTER_LABEL, + GOOGLE_VERTEX_ADAPTER_TYPE, + GOOGLE_VERTEX_MODELS, +} from "./shared/constants.js"; + +export const type = GOOGLE_VERTEX_ADAPTER_TYPE; +export const label = GOOGLE_VERTEX_ADAPTER_LABEL; +export const models = [...GOOGLE_VERTEX_MODELS]; + +export const agentConfigurationDoc = `# Google Vertex AI Configuration + +This adapter runs Hermes Agent with the Google Vertex AI provider fixed for every heartbeat. +It uses Gemini models through Vertex's OpenAI-compatible endpoint. + +## Authentication + +Vertex uses OAuth2, not a static API key. Configure one of: + +- \`credentialsPath\`: absolute path to a service-account JSON file on the execution host. +- Application Default Credentials (ADC): leave \`credentialsPath\` blank and configure ADC on the host. + +Set \`projectId\` when it cannot be inferred from the credential. The default region is \`global\`, which is required by Gemini 3 preview models. Hermes mints and refreshes short-lived OAuth2 tokens at runtime. +`; + +export function createServerAdapter(): ServerAdapterModule { + return { + type, + execute: executeGoogleVertex, + testEnvironment: testGoogleVertexEnvironment, + sessionCodec, + sessionManagement: { + supportsSessionResume: true, + nativeContextManagement: "confirmed", + defaultSessionCompaction: { + enabled: true, + maxSessionRuns: 0, + maxRawInputTokens: 0, + maxSessionAgeHours: 0, + }, + }, + listSkills: listHermesSkills, + syncSkills: syncHermesSkills, + models, + supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: false, + getRuntimeCommandSpec: (config) => { + const command = resolveHermesCommand(config); + return { command, detectCommand: command, installCommand: null }; + }, + agentConfigurationDoc, + getConfigSchema: getGoogleVertexConfigSchema, + }; +} diff --git a/packages/adapters/hermes/src/vertex/server/config-schema.ts b/packages/adapters/hermes/src/vertex/server/config-schema.ts new file mode 100644 index 0000000000..c6ef07088d --- /dev/null +++ b/packages/adapters/hermes/src/vertex/server/config-schema.ts @@ -0,0 +1,61 @@ +import type { AdapterConfigSchema } from "@paperclipai/adapter-utils"; + +import { DEFAULT_GRACE_SEC, DEFAULT_TIMEOUT_SEC } from "../../shared/constants.js"; +import { DEFAULT_GOOGLE_VERTEX_REGION } from "../shared/constants.js"; + +export function getGoogleVertexConfigSchema(): AdapterConfigSchema { + return { + fields: [ + { + key: "projectId", + label: "Google Cloud project ID", + type: "text", + hint: "Optional when the credential contains a project ID; otherwise required.", + }, + { + key: "region", + label: "Vertex region", + type: "text", + default: DEFAULT_GOOGLE_VERTEX_REGION, + hint: "Use global for Gemini 3 preview models; pin a region only when your deployment requires it.", + }, + { + key: "credentialsPath", + label: "Service-account JSON path", + type: "text", + hint: "Optional absolute path on the execution host. Leave blank to use Application Default Credentials.", + meta: { path: true }, + }, + { + key: "timeoutSec", + label: "Timeout seconds", + type: "number", + default: DEFAULT_TIMEOUT_SEC, + }, + { + key: "graceSec", + label: "Grace seconds", + type: "number", + default: DEFAULT_GRACE_SEC, + }, + { + key: "maxTurnsPerRun", + label: "Max turns per run", + type: "number", + hint: "Optional Hermes tool-calling iteration limit.", + }, + { + key: "persistSession", + label: "Persist session", + type: "toggle", + default: true, + }, + { + key: "toolsets", + label: "Toolsets", + type: "text", + hint: "Optional comma-separated Hermes toolsets, such as terminal,file,web.", + }, + ], + }; +} diff --git a/packages/adapters/hermes/src/vertex/server/config.test.ts b/packages/adapters/hermes/src/vertex/server/config.test.ts new file mode 100644 index 0000000000..6af6469d51 --- /dev/null +++ b/packages/adapters/hermes/src/vertex/server/config.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; + +import { + DEFAULT_GOOGLE_VERTEX_MODEL, + DEFAULT_GOOGLE_VERTEX_REGION, + GOOGLE_VERTEX_PROVIDER, +} from "../shared/constants.js"; +import { buildGoogleVertexRuntimeConfig } from "./config.js"; + +describe("buildGoogleVertexRuntimeConfig", () => { + it("fixes the provider and supplies safe Vertex defaults", () => { + expect(buildGoogleVertexRuntimeConfig({ provider: "openrouter" })).toMatchObject({ + provider: GOOGLE_VERTEX_PROVIDER, + model: DEFAULT_GOOGLE_VERTEX_MODEL, + env: { VERTEX_REGION: DEFAULT_GOOGLE_VERTEX_REGION }, + }); + }); + + it("maps routing fields and the service-account pointer into Hermes env", () => { + const config = buildGoogleVertexRuntimeConfig({ + model: " google/gemini-3.1-pro-preview ", + projectId: " project-123 ", + region: " us-central1 ", + credentialsPath: " /secure/vertex.json ", + env: { EXISTING: "value" }, + }); + + expect(config).toMatchObject({ + provider: "vertex", + model: "google/gemini-3.1-pro-preview", + env: { + EXISTING: "value", + VERTEX_PROJECT_ID: "project-123", + VERTEX_REGION: "us-central1", + VERTEX_CREDENTIALS_PATH: "/secure/vertex.json", + }, + }); + }); +}); diff --git a/packages/adapters/hermes/src/vertex/server/config.ts b/packages/adapters/hermes/src/vertex/server/config.ts new file mode 100644 index 0000000000..f8d014ab8e --- /dev/null +++ b/packages/adapters/hermes/src/vertex/server/config.ts @@ -0,0 +1,51 @@ +import type { AdapterExecutionContext, AdapterEnvironmentTestContext } from "@paperclipai/adapter-utils"; + +import { + DEFAULT_GOOGLE_VERTEX_MODEL, + DEFAULT_GOOGLE_VERTEX_REGION, + GOOGLE_VERTEX_PROVIDER, +} from "../shared/constants.js"; + +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; +} + +export function buildGoogleVertexRuntimeConfig( + config: Record, +): Record { + const env = + config.env && typeof config.env === "object" && !Array.isArray(config.env) + ? { ...(config.env as Record) } + : {}; + const projectId = nonEmptyString(config.projectId); + const region = nonEmptyString(config.region) ?? DEFAULT_GOOGLE_VERTEX_REGION; + const credentialsPath = nonEmptyString(config.credentialsPath); + + if (projectId) env.VERTEX_PROJECT_ID = projectId; + if (region) env.VERTEX_REGION = region; + if (credentialsPath) env.VERTEX_CREDENTIALS_PATH = credentialsPath; + + return { + ...config, + env, + provider: GOOGLE_VERTEX_PROVIDER, + model: nonEmptyString(config.model) ?? DEFAULT_GOOGLE_VERTEX_MODEL, + }; +} + +export function withGoogleVertexExecutionConfig( + ctx: AdapterExecutionContext, +): AdapterExecutionContext { + const config = buildGoogleVertexRuntimeConfig(ctx.config ?? {}); + return { + ...ctx, + config, + agent: { ...ctx.agent, adapterConfig: config }, + }; +} + +export function withGoogleVertexTestConfig( + ctx: AdapterEnvironmentTestContext, +): AdapterEnvironmentTestContext { + return { ...ctx, config: buildGoogleVertexRuntimeConfig(ctx.config ?? {}) }; +} diff --git a/packages/adapters/hermes/src/vertex/server/index.ts b/packages/adapters/hermes/src/vertex/server/index.ts new file mode 100644 index 0000000000..561a244735 --- /dev/null +++ b/packages/adapters/hermes/src/vertex/server/index.ts @@ -0,0 +1,75 @@ +import type { + AdapterEnvironmentCheck, + AdapterEnvironmentTestContext, + AdapterEnvironmentTestResult, + AdapterExecutionContext, +} from "@paperclipai/adapter-utils"; +import { access } from "node:fs/promises"; + +import { execute as executeHermes } from "../../server/execute.js"; +import { testEnvironment as testHermesEnvironment } from "../../server/test.js"; +import { GOOGLE_VERTEX_ADAPTER_TYPE } from "../shared/constants.js"; +import { + buildGoogleVertexRuntimeConfig, + withGoogleVertexExecutionConfig, + withGoogleVertexTestConfig, +} from "./config.js"; +export { getGoogleVertexConfigSchema } from "./config-schema.js"; +export { buildGoogleVertexRuntimeConfig } from "./config.js"; + +export async function executeGoogleVertex(ctx: AdapterExecutionContext) { + return executeHermes(withGoogleVertexExecutionConfig(ctx)); +} + +async function vertexCredentialCheck( + config: Record, +): Promise { + const env = (config.env ?? {}) as Record; + const credentialPath = + typeof env.VERTEX_CREDENTIALS_PATH === "string" && env.VERTEX_CREDENTIALS_PATH.length > 0 + ? env.VERTEX_CREDENTIALS_PATH + : typeof env.GOOGLE_APPLICATION_CREDENTIALS === "string" && env.GOOGLE_APPLICATION_CREDENTIALS.length > 0 + ? env.GOOGLE_APPLICATION_CREDENTIALS + : null; + if (credentialPath) { + try { + await access(credentialPath); + } catch { + return { + level: "error", + message: "Vertex service-account credential file is not readable", + hint: "Check credentialsPath on the selected execution host, or leave it blank to use Application Default Credentials.", + code: "google_vertex_service_account_unreadable", + }; + } + return { + level: "info", + message: "Vertex service-account credential path is configured", + hint: "Hermes will mint and refresh OAuth2 access tokens at runtime; tokens are never stored in Paperclip config.", + code: "google_vertex_service_account_configured", + }; + } + return { + level: "info", + message: "Vertex will use Google Application Default Credentials", + hint: "Ensure ADC is available on the selected execution host and can access the configured Google Cloud project.", + code: "google_vertex_adc", + }; +} + +export async function testGoogleVertexEnvironment( + ctx: AdapterEnvironmentTestContext, +): Promise { + const vertexCtx = withGoogleVertexTestConfig(ctx); + const result = await testHermesEnvironment(vertexCtx); + const checks = result.checks.filter((check) => check.code !== "hermes_no_api_keys"); + checks.push(await vertexCredentialCheck(buildGoogleVertexRuntimeConfig(ctx.config ?? {}))); + const hasErrors = checks.some((check) => check.level === "error"); + const hasWarnings = checks.some((check) => check.level === "warn"); + return { + ...result, + adapterType: GOOGLE_VERTEX_ADAPTER_TYPE, + status: hasErrors ? "fail" : hasWarnings ? "warn" : "pass", + checks, + }; +} diff --git a/packages/adapters/hermes/src/vertex/shared/constants.ts b/packages/adapters/hermes/src/vertex/shared/constants.ts new file mode 100644 index 0000000000..71146028ec --- /dev/null +++ b/packages/adapters/hermes/src/vertex/shared/constants.ts @@ -0,0 +1,19 @@ +export const GOOGLE_VERTEX_ADAPTER_TYPE = "google_vertex"; +export const GOOGLE_VERTEX_ADAPTER_LABEL = "Google Vertex AI"; +export const GOOGLE_VERTEX_PROVIDER = "vertex"; +export const DEFAULT_GOOGLE_VERTEX_MODEL = "google/gemini-3.8-flash"; +export const DEFAULT_GOOGLE_VERTEX_REGION = "global"; + +/** Vertex's OpenAI-compatible endpoint has no model-list route. */ +export const GOOGLE_VERTEX_MODELS = [ + { id: "google/gemini-3.8-flash", label: "Gemini 3.8 Flash" }, + { id: "google/gemini-3.7-flash", label: "Gemini 3.7 Flash" }, + { id: "google/gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview" }, + { id: "google/gemini-3-pro-preview", label: "Gemini 3 Pro Preview" }, + { id: "google/gemini-3.6-flash", label: "Gemini 3.6 Flash" }, + { id: "google/gemini-3.5-flash", label: "Gemini 3.5 Flash" }, + { id: "google/gemini-3.5-flash-lite", label: "Gemini 3.5 Flash Lite" }, + { id: "google/gemini-3-flash-preview", label: "Gemini 3 Flash Preview" }, + { id: "google/gemini-3.1-flash-lite-preview", label: "Gemini 3.1 Flash Lite Preview" }, + { id: "google/gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite" }, +] as const; diff --git a/packages/adapters/hermes/src/vertex/ui/build-config.test.ts b/packages/adapters/hermes/src/vertex/ui/build-config.test.ts new file mode 100644 index 0000000000..5e3a82a11a --- /dev/null +++ b/packages/adapters/hermes/src/vertex/ui/build-config.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import type { CreateConfigValues } from "@paperclipai/adapter-utils"; + +import { DEFAULT_GOOGLE_VERTEX_MODEL } from "../shared/constants.js"; +import { buildGoogleVertexConfig } from "./build-config.js"; + +function values(overrides: Partial = {}): CreateConfigValues { + return { + adapterType: "google_vertex", + cwd: "", + promptTemplate: "", + model: "", + thinkingEffort: "", + chrome: false, + dangerouslySkipPermissions: false, + search: false, + fastMode: false, + dangerouslyBypassSandbox: false, + command: "", + args: "", + extraArgs: "", + envVars: "", + envBindings: {}, + url: "", + bootstrapPrompt: "", + payloadTemplateJson: "", + maxTurnsPerRun: 0, + heartbeatEnabled: true, + intervalSec: 3600, + ...overrides, + }; +} + +describe("buildGoogleVertexConfig", () => { + it("creates a Vertex-only config with the default model and region", () => { + expect(buildGoogleVertexConfig(values())).toMatchObject({ + provider: "vertex", + model: DEFAULT_GOOGLE_VERTEX_MODEL, + region: "global", + persistSession: true, + }); + }); + + it("keeps schema routing fields while preventing a provider override", () => { + expect( + buildGoogleVertexConfig( + values({ + model: "google/gemini-3.1-pro-preview", + adapterSchemaValues: { + provider: "openrouter", + projectId: "project-123", + region: "us-central1", + credentialsPath: "/secure/vertex.json", + }, + }), + ), + ).toMatchObject({ + provider: "vertex", + model: "google/gemini-3.1-pro-preview", + projectId: "project-123", + region: "us-central1", + credentialsPath: "/secure/vertex.json", + }); + }); +}); diff --git a/packages/adapters/hermes/src/vertex/ui/build-config.ts b/packages/adapters/hermes/src/vertex/ui/build-config.ts new file mode 100644 index 0000000000..1075b9f3d0 --- /dev/null +++ b/packages/adapters/hermes/src/vertex/ui/build-config.ts @@ -0,0 +1,23 @@ +import type { CreateConfigValues } from "@paperclipai/adapter-utils"; + +import { buildHermesConfig } from "../../ui/build-config.js"; +import { + DEFAULT_GOOGLE_VERTEX_MODEL, + DEFAULT_GOOGLE_VERTEX_REGION, + GOOGLE_VERTEX_PROVIDER, +} from "../shared/constants.js"; + +export function buildGoogleVertexConfig(values: CreateConfigValues): Record { + return { + ...buildHermesConfig({ + ...values, + model: values.model.trim() || DEFAULT_GOOGLE_VERTEX_MODEL, + }), + ...(values.adapterSchemaValues ?? {}), + provider: GOOGLE_VERTEX_PROVIDER, + region: + typeof values.adapterSchemaValues?.region === "string" && values.adapterSchemaValues.region.trim() + ? values.adapterSchemaValues.region.trim() + : DEFAULT_GOOGLE_VERTEX_REGION, + }; +} diff --git a/packages/adapters/hermes/src/vertex/ui/index.ts b/packages/adapters/hermes/src/vertex/ui/index.ts new file mode 100644 index 0000000000..851340cf5a --- /dev/null +++ b/packages/adapters/hermes/src/vertex/ui/index.ts @@ -0,0 +1,2 @@ +export { buildGoogleVertexConfig } from "./build-config.js"; +export { parseHermesStdoutLine as parseGoogleVertexStdoutLine } from "../../ui/parse-stdout.js"; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 28e18d3dfd..ccfec2fe48 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -32,6 +32,7 @@ export const AGENT_ADAPTER_TYPES = [ "paperclip_runner", "cursor_cloud", "gemini_local", + "google_vertex", "grok_local", "hermes_gateway", "hermes_local", diff --git a/server/src/__tests__/adapter-registry.test.ts b/server/src/__tests__/adapter-registry.test.ts index 1da4fcdc4d..d0241b2b85 100644 --- a/server/src/__tests__/adapter-registry.test.ts +++ b/server/src/__tests__/adapter-registry.test.ts @@ -189,6 +189,20 @@ describe("server adapter registry", () => { expect(requireServerAdapter("hermes_gateway")).toBe(builtInGateway); }); + it("ships Google Vertex AI as a Hermes-backed built-in adapter", () => { + const adapter = findServerAdapter("google_vertex"); + + expect(adapter).not.toBeNull(); + expect(adapter?.supportsLocalAgentJwt).toBe(true); + expect(adapter?.supportsInstructionsBundle).toBe(true); + expect(adapter?.requiresMaterializedRuntimeSkills).toBe(false); + expect(adapter?.getConfigSchema).toBeTypeOf("function"); + expect(adapter?.models?.[0]).toEqual({ + id: "google/gemini-3.8-flash", + label: "Gemini 3.8 Flash", + }); + }); + it("exposes capability flags from registered adapters", () => { const adapterWithCaps: ServerAdapterModule = { type: "external_test", diff --git a/server/src/adapters/builtin-adapter-types.ts b/server/src/adapters/builtin-adapter-types.ts index 58c2e95b6d..79bc2683e5 100644 --- a/server/src/adapters/builtin-adapter-types.ts +++ b/server/src/adapters/builtin-adapter-types.ts @@ -9,6 +9,7 @@ export const BUILTIN_ADAPTER_TYPES = new Set([ "cursor_cloud", "cursor", "gemini_local", + "google_vertex", "grok_local", "hermes_gateway", "hermes_local", diff --git a/server/src/adapters/registry.test.ts b/server/src/adapters/registry.test.ts index 4f078588fc..ae948b7e66 100644 --- a/server/src/adapters/registry.test.ts +++ b/server/src/adapters/registry.test.ts @@ -62,6 +62,7 @@ describe("built-in runtime connection tool delivery", () => { ["grok_local", "environment"], ["hermes_gateway", "invocation_context"], ["hermes_local", "environment"], + ["google_vertex", "environment"], ["kimi_local", "environment"], ["openclaw_gateway", "invocation_context"], ["opencode_local", "environment"], diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 96f97b5885..bdc6980c26 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -98,6 +98,7 @@ import { import { createHermesGatewayServerAdapter, createHermesLocalServerAdapter, + createGoogleVertexServerAdapter, } from "@paperclipai/hermes-paperclip-adapter"; import { execute as openCodeExecute, @@ -788,6 +789,11 @@ const hermesLocalAdapter: ServerAdapterModule = { runtimeToolDelivery: "environment", }; +const googleVertexAdapter: ServerAdapterModule = { + ...createGoogleVertexServerAdapter(), + runtimeToolDelivery: "environment", +}; + const openclawGatewayAdapter: ServerAdapterModule = { type: "openclaw_gateway", runtimeToolDelivery: "invocation_context", @@ -865,6 +871,7 @@ function registerBuiltInAdapters() { kimiLocalAdapter, hermesGatewayAdapter, hermesLocalAdapter, + googleVertexAdapter, openclawGatewayAdapter, processAdapter, httpAdapter, diff --git a/server/src/modules/active-run-watchdog/adapters/process.ts b/server/src/modules/active-run-watchdog/adapters/process.ts index 17fcbb95c4..db93ca0b77 100644 --- a/server/src/modules/active-run-watchdog/adapters/process.ts +++ b/server/src/modules/active-run-watchdog/adapters/process.ts @@ -9,6 +9,7 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([ "cursor", "gemini_local", "hermes_local", + "google_vertex", "kimi_local", "opencode_local", "pi_local", diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 1428fae7b8..4f70af1888 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -491,6 +491,7 @@ export function agentRoutes( codex_local: "instructionsFilePath", droid_local: "instructionsFilePath", gemini_local: "instructionsFilePath", + google_vertex: "instructionsFilePath", kimi_local: "instructionsFilePath", opencode_local: "instructionsFilePath", cursor: "instructionsFilePath", diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 3de2209b3c..5544d27b59 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -824,6 +824,7 @@ const GIT_SENSITIVE_LOCAL_ADAPTER_TYPES = new Set([ "gemini_local", "grok_local", "hermes_local", + "google_vertex", "kimi_local", "opencode_local", "pi_local", @@ -1213,6 +1214,7 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([ "cursor", "gemini_local", "hermes_local", + "google_vertex", "kimi_local", "opencode_local", "pi_local", @@ -8871,12 +8873,12 @@ export function resolveSkillTestRunCompletionForHeartbeatOutcome( return null; } -const HERMES_ADAPTER_TYPE = "hermes_local"; +const HERMES_ADAPTER_TYPES = new Set(["hermes_local", "google_vertex"]); const HERMES_SESSION_ID_REGEX = /^(?:\d{8}_\d{6}_[A-Za-z0-9_-]{4,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/; function requiresCanonicalSessionIds(adapterType: string | null | undefined) { - return adapterType === HERMES_ADAPTER_TYPE; + return typeof adapterType === "string" && HERMES_ADAPTER_TYPES.has(adapterType); } function isCanonicalSessionIdForAdapter( diff --git a/ui/src/adapters/adapter-display-registry.test.ts b/ui/src/adapters/adapter-display-registry.test.ts index 0b34460347..4ef0775a2d 100644 --- a/ui/src/adapters/adapter-display-registry.test.ts +++ b/ui/src/adapters/adapter-display-registry.test.ts @@ -9,6 +9,7 @@ describe("adapter display registry", () => { expect(getAdapterLabel("acpx_local")).toBe("ACPX (retired)"); expect(getAdapterLabel("cursor")).toBe("Cursor"); expect(getAdapterLabel("gemini_local")).toBe("Gemini CLI"); + expect(getAdapterLabel("google_vertex")).toBe("Google Vertex AI"); expect(getAdapterLabel("grok_local")).toBe("Grok Build"); expect(getAdapterLabel("kimi_local")).toBe("Kimi Code"); expect(getAdapterLabel("hermes_local")).toBe("Hermes"); @@ -22,6 +23,7 @@ describe("adapter display registry", () => { acpx_local: "ACPX (retired)", cursor: "Cursor", gemini_local: "Gemini CLI", + google_vertex: "Google Vertex AI", grok_local: "Grok Build", kimi_local: "Kimi Code", hermes_local: "Hermes", diff --git a/ui/src/adapters/adapter-display-registry.ts b/ui/src/adapters/adapter-display-registry.ts index b4f500eed6..854cf7b14f 100644 --- a/ui/src/adapters/adapter-display-registry.ts +++ b/ui/src/adapters/adapter-display-registry.ts @@ -91,6 +91,11 @@ const adapterDisplayMap: Record = { description: "Gemini CLI harness", icon: Gem, }, + google_vertex: { + label: "Google Vertex AI", + description: "Gemini on Google Cloud via Hermes", + icon: Gem, + }, grok_local: { label: "Grok Build", description: "Grok Build harness", diff --git a/ui/src/adapters/google-vertex/index.ts b/ui/src/adapters/google-vertex/index.ts new file mode 100644 index 0000000000..36816b63e4 --- /dev/null +++ b/ui/src/adapters/google-vertex/index.ts @@ -0,0 +1,14 @@ +import type { UIAdapterModule } from "../types"; +import { + buildGoogleVertexConfig, + parseGoogleVertexStdoutLine, +} from "@paperclipai/hermes-paperclip-adapter/vertex/ui"; +import { SchemaConfigFields } from "../schema-config-fields"; + +export const googleVertexUIAdapter: UIAdapterModule = { + type: "google_vertex", + label: "Google Vertex AI", + parseStdoutLine: parseGoogleVertexStdoutLine, + ConfigFields: SchemaConfigFields, + buildAdapterConfig: buildGoogleVertexConfig, +}; diff --git a/ui/src/adapters/registry.test.ts b/ui/src/adapters/registry.test.ts index 2d36af655e..3e0aea4e30 100644 --- a/ui/src/adapters/registry.test.ts +++ b/ui/src/adapters/registry.test.ts @@ -77,4 +77,11 @@ describe("ui adapter registry", () => { expect(getUIAdapter(type)).toBe(builtin); } }); + + it("registers the Google Vertex AI UI adapter", () => { + const adapter = getUIAdapter("google_vertex"); + + expect(adapter.label).toBe("Google Vertex AI"); + expect(adapter.ConfigFields).toBe(SchemaConfigFields); + }); }); diff --git a/ui/src/adapters/registry.ts b/ui/src/adapters/registry.ts index 85ee9e0240..dd2680d6fc 100644 --- a/ui/src/adapters/registry.ts +++ b/ui/src/adapters/registry.ts @@ -5,6 +5,7 @@ import { paperclipRunnerUIAdapter } from "./paperclip-runner"; import { cursorCloudUIAdapter } from "./cursor-cloud"; import { cursorLocalUIAdapter } from "./cursor"; import { geminiLocalUIAdapter } from "./gemini-local"; +import { googleVertexUIAdapter } from "./google-vertex"; import { grokLocalUIAdapter } from "./grok-local"; import { kimiLocalUIAdapter } from "./kimi-local"; import { hermesGatewayUIAdapter } from "./hermes-gateway"; @@ -59,6 +60,7 @@ function registerBuiltInUIAdapters() { paperclipRunnerUIAdapter, cursorCloudUIAdapter, geminiLocalUIAdapter, + googleVertexUIAdapter, grokLocalUIAdapter, kimiLocalUIAdapter, hermesGatewayUIAdapter, diff --git a/ui/src/adapters/use-adapter-capabilities.ts b/ui/src/adapters/use-adapter-capabilities.ts index a3da3d9e83..0966c9a613 100644 --- a/ui/src/adapters/use-adapter-capabilities.ts +++ b/ui/src/adapters/use-adapter-capabilities.ts @@ -25,6 +25,7 @@ const KNOWN_DEFAULTS: Record = { paperclip_runner: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: false, requiresMaterializedRuntimeSkills: false, supportsAcp: false }, cursor: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true, supportsAcp: false }, gemini_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true, supportsAcp: true }, + google_vertex: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false, supportsAcp: false }, grok_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true, supportsAcp: false, login: { panelMode: "displayed_code", timeoutPolicy: "caller_bounded" } }, kimi_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true, supportsAcp: true }, opencode_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true, supportsAcp: false }, diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 5daf58cd44..2d1ed73e83 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -1091,6 +1091,7 @@ function OnboardingWizardInner({ adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "gemini_local" || + adapterType === "google_vertex" || adapterType === "kimi_local" || adapterType === "opencode_local" || adapterType === "pi_local" || diff --git a/ui/src/components/new-agent/AgentBasicsDialog.tsx b/ui/src/components/new-agent/AgentBasicsDialog.tsx index 1b7ee5b27e..d78b961e1a 100644 --- a/ui/src/components/new-agent/AgentBasicsDialog.tsx +++ b/ui/src/components/new-agent/AgentBasicsDialog.tsx @@ -27,6 +27,7 @@ const brandMarks: Record = { claude_local: { src: "/brands/claude-color.svg" }, codex_local: { src: "/brands/codex-color.svg" }, gemini_local: { src: "/brands/adapters/gemini-color.svg" }, + google_vertex: { src: "/brands/adapters/gemini-color.svg" }, kimi_local: { src: "/brands/adapters/kimi-color-light.svg", dark: "/brands/adapters/kimi-color.svg", diff --git a/ui/src/pages/InviteLanding.tsx b/ui/src/pages/InviteLanding.tsx index 4835a00417..1f7e308034 100644 --- a/ui/src/pages/InviteLanding.tsx +++ b/ui/src/pages/InviteLanding.tsx @@ -28,6 +28,7 @@ const ENABLED_INVITE_ADAPTERS = new Set([ "claude_local", "codex_local", "gemini_local", + "google_vertex", "kimi_local", "opencode_local", "pi_local",