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";