From 1eba85bc1cd030e9db23739be626cbad4f7e8de4 Mon Sep 17 00:00:00 2001 From: spuhaha18 Date: Fri, 7 Aug 2026 20:06:59 +0900 Subject: [PATCH 1/2] fix(secrets): store openclaw_gateway credentials as secret refs The openclaw_gateway adapter ships no getConfigSchema(), so listAdapterSchemaSecretFieldKeys() falls back to FALLBACK_ADAPTER_SCHEMA_SECRET_FIELDS, which only had an entry for hermes_gateway. As a result no adapterConfig field of an openclaw_gateway agent was ever routed through normalizeSchemaSecretFieldForPersistence(), and authToken / token / password / devicePrivateKeyPem were persisted verbatim in agents.adapter_config. Declare those four fields for openclaw_gateway so they are converted to secret refs on write, exactly like hermes_gateway apiKey. Runtime resolution is unchanged: resolveAdapterConfigForRuntime() rehydrates the plaintext before execute(), so the adapter needs no change. Co-Authored-By: Claude Opus 5 --- .../agents-service-secret-bindings.test.ts | 71 +++++++++++++++++++ server/src/services/secrets.ts | 1 + 2 files changed, 72 insertions(+) diff --git a/server/src/__tests__/agents-service-secret-bindings.test.ts b/server/src/__tests__/agents-service-secret-bindings.test.ts index 2c12fa4312..e85d3688f1 100644 --- a/server/src/__tests__/agents-service-secret-bindings.test.ts +++ b/server/src/__tests__/agents-service-secret-bindings.test.ts @@ -262,6 +262,77 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { expect(JSON.stringify(persistedConfig)).not.toContain(literalApiKey); }); + it("converts OpenClaw gateway credential strings into persisted secret refs", async () => { + const companyId = await seedCompany(); + const literalAuthToken = `openclaw-token-${randomUUID()}`; + const literalPassword = `openclaw-password-${randomUUID()}`; + const literalPrivateKeyPem = [ + "-----BEGIN PRIVATE KEY-----", + `openclaw-device-key-${randomUUID()}`, + "-----END PRIVATE KEY-----", + ].join("\n"); + + const created = await agentService(db).create(companyId, { + name: "OpenClaw Gateway", + role: "engineer", + status: "idle", + adapterType: "openclaw_gateway", + adapterConfig: { + url: "ws://127.0.0.1:18789", + authToken: literalAuthToken, + password: literalPassword, + devicePrivateKeyPem: literalPrivateKeyPem, + }, + runtimeConfig: {}, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }); + + const persistedRows = await db + .select() + .from(agents) + .where(eq(agents.id, created.id)); + const persistedConfig = persistedRows[0]?.adapterConfig as Record; + const serializedConfig = JSON.stringify(persistedConfig); + expect(serializedConfig).not.toContain(literalAuthToken); + expect(serializedConfig).not.toContain(literalPassword); + expect(serializedConfig).not.toContain(literalPrivateKeyPem); + for (const key of ["authToken", "password", "devicePrivateKeyPem"]) { + expect(persistedConfig[key]).toMatchObject({ + type: "secret_ref", + version: "latest", + }); + } + expect(persistedConfig.url).toBe("ws://127.0.0.1:18789"); + + const bindings = await db + .select() + .from(companySecretBindings) + .where(and( + eq(companySecretBindings.companyId, companyId), + eq(companySecretBindings.targetType, "agent"), + eq(companySecretBindings.targetId, created.id), + )); + expect(bindings.map((binding) => binding.configPath).sort()).toEqual([ + "authToken", + "devicePrivateKeyPem", + "password", + ]); + + const resolved = await secretService(db).resolveAdapterConfigForRuntime( + companyId, + persistedConfig, + { + consumerType: "agent", + consumerId: created.id, + }, + { adapterType: "openclaw_gateway" }, + ); + expect(resolved.config.authToken).toBe(literalAuthToken); + expect(resolved.config.password).toBe(literalPassword); + expect(resolved.config.devicePrivateKeyPem).toBe(literalPrivateKeyPem); + }); + it("replaces agent secret bindings when adapterConfig env changes", async () => { const companyId = await seedCompany(); const secrets = secretService(db); diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 20bec111a7..94bc3341a6 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -77,6 +77,7 @@ const COMING_SOON_SECRET_PROVIDERS: ReadonlySet = new Set([ ]); const FALLBACK_ADAPTER_SCHEMA_SECRET_FIELDS: Readonly> = { hermes_gateway: ["apiKey"], + openclaw_gateway: ["authToken", "token", "password", "devicePrivateKeyPem"], }; const USER_SECRET_DEFINITION_KEY_UNIQUE_CONSTRAINT = "user_secret_definitions_company_key_uq"; const USER_SECRET_VALUE_UNIQUE_CONSTRAINT = "company_secrets_user_definition_owner_uq"; From 8dbec8ba8eed7f34b2992e1951b67bb179004204 Mon Sep 17 00:00:00 2001 From: spuhaha18 Date: Fri, 7 Aug 2026 20:14:03 +0900 Subject: [PATCH 2/2] fix(secrets): also normalize openclaw_gateway deviceToken Greptile P1: execute.ts reads a top-level `deviceToken` from adapterConfig and sends it as gateway authentication material alongside authToken and password, so it must be normalized too. Confirmed at packages/adapters/openclaw-gateway/src/server/execute.ts:1071,1252,1255. Extends the regression test to cover it. Co-Authored-By: Claude Opus 5 --- .../src/__tests__/agents-service-secret-bindings.test.ts | 7 ++++++- server/src/services/secrets.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/agents-service-secret-bindings.test.ts b/server/src/__tests__/agents-service-secret-bindings.test.ts index e85d3688f1..82603cd89f 100644 --- a/server/src/__tests__/agents-service-secret-bindings.test.ts +++ b/server/src/__tests__/agents-service-secret-bindings.test.ts @@ -266,6 +266,7 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { const companyId = await seedCompany(); const literalAuthToken = `openclaw-token-${randomUUID()}`; const literalPassword = `openclaw-password-${randomUUID()}`; + const literalDeviceToken = `openclaw-device-token-${randomUUID()}`; const literalPrivateKeyPem = [ "-----BEGIN PRIVATE KEY-----", `openclaw-device-key-${randomUUID()}`, @@ -281,6 +282,7 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { url: "ws://127.0.0.1:18789", authToken: literalAuthToken, password: literalPassword, + deviceToken: literalDeviceToken, devicePrivateKeyPem: literalPrivateKeyPem, }, runtimeConfig: {}, @@ -296,8 +298,9 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { const serializedConfig = JSON.stringify(persistedConfig); expect(serializedConfig).not.toContain(literalAuthToken); expect(serializedConfig).not.toContain(literalPassword); + expect(serializedConfig).not.toContain(literalDeviceToken); expect(serializedConfig).not.toContain(literalPrivateKeyPem); - for (const key of ["authToken", "password", "devicePrivateKeyPem"]) { + for (const key of ["authToken", "password", "deviceToken", "devicePrivateKeyPem"]) { expect(persistedConfig[key]).toMatchObject({ type: "secret_ref", version: "latest", @@ -316,6 +319,7 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { expect(bindings.map((binding) => binding.configPath).sort()).toEqual([ "authToken", "devicePrivateKeyPem", + "deviceToken", "password", ]); @@ -330,6 +334,7 @@ describeEmbeddedPostgres("agent service secret binding sync", () => { ); expect(resolved.config.authToken).toBe(literalAuthToken); expect(resolved.config.password).toBe(literalPassword); + expect(resolved.config.deviceToken).toBe(literalDeviceToken); expect(resolved.config.devicePrivateKeyPem).toBe(literalPrivateKeyPem); }); diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 94bc3341a6..6e06ba4e3b 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -77,7 +77,7 @@ const COMING_SOON_SECRET_PROVIDERS: ReadonlySet = new Set([ ]); const FALLBACK_ADAPTER_SCHEMA_SECRET_FIELDS: Readonly> = { hermes_gateway: ["apiKey"], - openclaw_gateway: ["authToken", "token", "password", "devicePrivateKeyPem"], + openclaw_gateway: ["authToken", "token", "deviceToken", "password", "devicePrivateKeyPem"], }; const USER_SECRET_DEFINITION_KEY_UNIQUE_CONSTRAINT = "user_secret_definitions_company_key_uq"; const USER_SECRET_VALUE_UNIQUE_CONSTRAINT = "company_secrets_user_definition_owner_uq";