From 6fc69f5d3307c87e555eb6e49586a84f4b9842a2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Apr 2026 15:22:41 +0900 Subject: [PATCH] fix(secrets): drop legacy talk apiKey target surface (#60717) --- docs/reference/api-usage-costs.md | 4 +- .../reference/secretref-credential-surface.md | 1 - ...tref-user-supplied-credentials-matrix.json | 7 - src/cli/command-secret-gateway.test.ts | 175 ++++++++++-------- src/gateway/server-methods/secrets.test.ts | 30 ++- src/gateway/server.reload.test.ts | 20 +- src/secrets/command-config.test.ts | 36 +++- src/secrets/configure-plan.test.ts | 37 ++-- src/secrets/exec-secret-ref-id-parity.test.ts | 7 +- src/secrets/plan.test.ts | 21 ++- src/secrets/target-registry-data.ts | 11 -- src/secrets/target-registry-test-helpers.ts | 3 - src/secrets/target-registry.test.ts | 12 +- 13 files changed, 211 insertions(+), 153 deletions(-) diff --git a/docs/reference/api-usage-costs.md b/docs/reference/api-usage-costs.md index a85eb25bf98..7b7e4a34586 100644 --- a/docs/reference/api-usage-costs.md +++ b/docs/reference/api-usage-costs.md @@ -44,7 +44,7 @@ OpenClaw can pick up credentials from: - **Auth profiles** (per-agent, stored in `auth-profiles.json`). - **Environment variables** (e.g. `OPENAI_API_KEY`, `BRAVE_API_KEY`, `FIRECRAWL_API_KEY`). - **Config** (`models.providers.*.apiKey`, `tools.web.search.*`, `tools.web.fetch.firecrawl.*`, - `memorySearch.*`, `talk.apiKey`). + `memorySearch.*`, `talk.providers.*.apiKey`). - **Skills** (`skills.entries..apiKey`) which may export keys to the skill process env. ## Features that can spend keys @@ -148,7 +148,7 @@ See [Models CLI](/cli/models). Talk mode can invoke **ElevenLabs** when configured: -- `ELEVENLABS_API_KEY` or `talk.apiKey` +- `ELEVENLABS_API_KEY` or `talk.providers.elevenlabs.apiKey` See [Talk mode](/nodes/talk). diff --git a/docs/reference/secretref-credential-surface.md b/docs/reference/secretref-credential-surface.md index 2ab3abf3203..108e52402a9 100644 --- a/docs/reference/secretref-credential-surface.md +++ b/docs/reference/secretref-credential-surface.md @@ -38,7 +38,6 @@ Scope intent: - `skills.entries.*.apiKey` - `agents.defaults.memorySearch.remote.apiKey` - `agents.list[].memorySearch.remote.apiKey` -- `talk.apiKey` - `talk.providers.*.apiKey` - `messages.tts.providers.*.apiKey` - `tools.web.fetch.firecrawl.apiKey` diff --git a/docs/reference/secretref-user-supplied-credentials-matrix.json b/docs/reference/secretref-user-supplied-credentials-matrix.json index 22749b3e5f4..d8549d644ca 100644 --- a/docs/reference/secretref-user-supplied-credentials-matrix.json +++ b/docs/reference/secretref-user-supplied-credentials-matrix.json @@ -575,13 +575,6 @@ "secretShape": "secret_input", "optIn": true }, - { - "id": "talk.apiKey", - "configFile": "openclaw.json", - "path": "talk.apiKey", - "secretShape": "secret_input", - "optIn": true - }, { "id": "talk.providers.*.apiKey", "configFile": "openclaw.json", diff --git a/src/cli/command-secret-gateway.test.ts b/src/cli/command-secret-gateway.test.ts index 03dd8d796b5..cee29ef4fa1 100644 --- a/src/cli/command-secret-gateway.test.ts +++ b/src/cli/command-secret-gateway.test.ts @@ -26,16 +26,20 @@ beforeEach(() => { }); describe("resolveCommandSecretRefsViaGateway", () => { - function makeTalkApiKeySecretRefConfig(envKey: string): OpenClawConfig { + function makeTalkProviderApiKeySecretRefConfig(envKey: string): OpenClawConfig { return { talk: { - apiKey: { source: "env", provider: "default", id: envKey }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: envKey }, + }, + }, }, } as unknown as OpenClawConfig; } - function readLegacyTalkApiKey(config: OpenClawConfig): unknown { - return (config.talk as Record | undefined)?.apiKey; + function readTalkProviderApiKey(config: OpenClawConfig): unknown { + return config.talk?.providers?.elevenlabs?.apiKey; } async function withEnvValue( @@ -60,24 +64,24 @@ describe("resolveCommandSecretRefsViaGateway", () => { } } - async function resolveTalkApiKey(params: { + async function resolveTalkProviderApiKey(params: { envKey: string; commandName?: string; mode?: "enforce_resolved" | "read_only_status"; }) { return resolveCommandSecretRefsViaGateway({ - config: makeTalkApiKeySecretRefConfig(params.envKey), + config: makeTalkProviderApiKeySecretRefConfig(params.envKey), commandName: params.commandName ?? "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), mode: params.mode, }); } - function expectTalkApiKeySecretRef( - result: Awaited>, + function expectTalkProviderApiKeySecretRef( + result: Awaited>, envKey: string, ) { - expect(readLegacyTalkApiKey(result.resolvedConfig)).toEqual({ + expect(readTalkProviderApiKey(result.resolvedConfig)).toEqual({ source: "env", provider: "default", id: envKey, @@ -98,13 +102,17 @@ describe("resolveCommandSecretRefsViaGateway", () => { it("returns config unchanged when no target SecretRefs are configured", async () => { const config = { talk: { - apiKey: "plain", // pragma: allowlist secret + providers: { + elevenlabs: { + apiKey: "plain", // pragma: allowlist secret + }, + }, }, } as unknown as OpenClawConfig; const result = await resolveCommandSecretRefsViaGateway({ config, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }); expect(result.resolvedConfig).toEqual(config); expect(callGateway).not.toHaveBeenCalled(); @@ -144,8 +152,8 @@ describe("resolveCommandSecretRefsViaGateway", () => { callGateway.mockResolvedValueOnce({ assignments: [ { - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], value: "sk-live", }, ], @@ -153,13 +161,17 @@ describe("resolveCommandSecretRefsViaGateway", () => { }); const config = { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, } as OpenClawConfig; const result = await resolveCommandSecretRefsViaGateway({ config, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }); expect(callGateway).toHaveBeenCalledWith( expect.objectContaining({ @@ -168,11 +180,11 @@ describe("resolveCommandSecretRefsViaGateway", () => { requiredMethods: ["secrets.resolve"], params: { commandName: "memory status", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }, }), ); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBe("sk-live"); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("sk-live"); }); it("enforces unresolved checks only for allowed paths when provided", async () => { @@ -224,11 +236,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: envKey }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: envKey }, + }, + }, }, } as unknown as OpenClawConfig, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }), ).rejects.toThrow(/failed to resolve secrets from the active gateway snapshot/i); } finally { @@ -248,7 +264,11 @@ describe("resolveCommandSecretRefsViaGateway", () => { const result = await resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, secrets: { providers: { @@ -257,10 +277,10 @@ describe("resolveCommandSecretRefsViaGateway", () => { }, } as unknown as OpenClawConfig, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBe("local-fallback-key"); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("local-fallback-key"); expect( result.diagnostics.some((entry) => entry.includes("gateway secrets.resolve unavailable")), ).toBe(true); @@ -405,7 +425,7 @@ describe("resolveCommandSecretRefsViaGateway", () => { const envKey = "TALK_API_KEY_UNSUPPORTED"; callGateway.mockRejectedValueOnce(new Error("unknown method: secrets.resolve")); await withEnvValue(envKey, undefined, async () => { - await expect(resolveTalkApiKey({ envKey })).rejects.toThrow( + await expect(resolveTalkProviderApiKey({ envKey })).rejects.toThrow( /does not support secrets\.resolve/i, ); }); @@ -419,7 +439,7 @@ describe("resolveCommandSecretRefsViaGateway", () => { ), ); await withEnvValue(envKey, undefined, async () => { - await expect(resolveTalkApiKey({ envKey })).rejects.toThrow( + await expect(resolveTalkProviderApiKey({ envKey })).rejects.toThrow( /does not support secrets\.resolve/i, ); }); @@ -434,11 +454,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, } as unknown as OpenClawConfig, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }), ).rejects.toThrow(/invalid secrets\.resolve payload/i); }); @@ -458,11 +482,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, } as OpenClawConfig, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }), ).rejects.toThrow(/Path segment does not exist/i); }); @@ -475,8 +503,8 @@ describe("resolveCommandSecretRefsViaGateway", () => { }); await withEnvValue(envKey, undefined, async () => { - await expect(resolveTalkApiKey({ envKey })).rejects.toThrow( - /talk\.apiKey is unresolved in the active runtime snapshot/i, + await expect(resolveTalkProviderApiKey({ envKey })).rejects.toThrow( + /talk\.providers\.elevenlabs\.apiKey is unresolved in the active runtime snapshot/i, ); }); }); @@ -485,15 +513,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { callGateway.mockResolvedValueOnce({ assignments: [], diagnostics: [ - "talk.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.", + "talk.providers.elevenlabs.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.", ], }); - const result = await resolveTalkApiKey({ envKey: "TALK_API_KEY" }); + const result = await resolveTalkProviderApiKey({ envKey: "TALK_API_KEY" }); - expectTalkApiKeySecretRef(result, "TALK_API_KEY"); + expectTalkProviderApiKeySecretRef(result, "TALK_API_KEY"); expect(result.diagnostics).toEqual([ - "talk.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.", + "talk.providers.elevenlabs.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.", ]); }); @@ -501,12 +529,12 @@ describe("resolveCommandSecretRefsViaGateway", () => { callGateway.mockResolvedValueOnce({ assignments: [], diagnostics: ["talk api key inactive"], - inactiveRefPaths: ["talk.apiKey"], + inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"], }); - const result = await resolveTalkApiKey({ envKey: "TALK_API_KEY" }); + const result = await resolveTalkProviderApiKey({ envKey: "TALK_API_KEY" }); - expectTalkApiKeySecretRef(result, "TALK_API_KEY"); + expectTalkProviderApiKeySecretRef(result, "TALK_API_KEY"); expect(result.diagnostics).toEqual(["talk api key inactive"]); }); @@ -553,17 +581,17 @@ describe("resolveCommandSecretRefsViaGateway", () => { diagnostics: [], }); await withEnvValue(envKey, undefined, async () => { - const result = await resolveTalkApiKey({ + const result = await resolveTalkProviderApiKey({ envKey, commandName: "status", mode: "read_only_status", }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBeUndefined(); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined(); expect(result.hadUnresolvedTargets).toBe(true); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("unresolved"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved"); expect( result.diagnostics.some((entry) => - entry.includes("talk.apiKey is unavailable in this command path"), + entry.includes("talk.providers.elevenlabs.apiKey is unavailable in this command path"), ), ).toBe(true); }); @@ -577,14 +605,14 @@ describe("resolveCommandSecretRefsViaGateway", () => { }); await withEnvValue(envKey, undefined, async () => { const result = await resolveCommandSecretRefsViaGateway({ - config: makeTalkApiKeySecretRefConfig(envKey), + config: makeTalkProviderApiKeySecretRefConfig(envKey), commandName: "status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), mode: "summary", }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBeUndefined(); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined(); expect(result.hadUnresolvedTargets).toBe(true); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("unresolved"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved"); }); }); @@ -595,14 +623,14 @@ describe("resolveCommandSecretRefsViaGateway", () => { diagnostics: [], }); await withEnvValue(envKey, "recovered-locally", async () => { - const result = await resolveTalkApiKey({ + const result = await resolveTalkProviderApiKey({ envKey, commandName: "status", mode: "read_only_status", }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBe("recovered-locally"); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("recovered-locally"); expect(result.hadUnresolvedTargets).toBe(false); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("resolved_local"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("resolved_local"); expect( result.diagnostics.some((entry) => entry.includes( @@ -614,17 +642,14 @@ describe("resolveCommandSecretRefsViaGateway", () => { }); it("limits strict local fallback analysis to unresolved gateway paths", async () => { - const gatewayResolvedKey = "TALK_API_KEY_PARTIAL_GATEWAY_RESOLVED"; const locallyRecoveredKey = "TALK_API_KEY_PARTIAL_GATEWAY_LOCAL"; - const priorGatewayResolvedValue = process.env[gatewayResolvedKey]; const priorLocallyRecoveredValue = process.env[locallyRecoveredKey]; - delete process.env[gatewayResolvedKey]; process.env[locallyRecoveredKey] = "recovered-locally"; callGateway.mockResolvedValueOnce({ assignments: [ { - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], value: "resolved-by-gateway", }, ], @@ -635,7 +660,6 @@ describe("resolveCommandSecretRefsViaGateway", () => { const result = await resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: gatewayResolvedKey }, providers: { elevenlabs: { apiKey: { source: "env", provider: "default", id: locallyRecoveredKey }, @@ -644,20 +668,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { }, } as unknown as OpenClawConfig, commandName: "message send", - targetIds: new Set(["talk.apiKey", "talk.providers.*.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBe("resolved-by-gateway"); - expect(result.resolvedConfig.talk?.providers?.elevenlabs?.apiKey).toBe("recovered-locally"); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("resolved-by-gateway"); expect(result.hadUnresolvedTargets).toBe(false); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("resolved_gateway"); - expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("resolved_local"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe( + "resolved_gateway", + ); } finally { - if (priorGatewayResolvedValue === undefined) { - delete process.env[gatewayResolvedKey]; - } else { - process.env[gatewayResolvedKey] = priorGatewayResolvedValue; - } if (priorLocallyRecoveredValue === undefined) { delete process.env[locallyRecoveredKey]; } else { @@ -679,7 +698,11 @@ describe("resolveCommandSecretRefsViaGateway", () => { const result = await resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: talkEnvKey }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: talkEnvKey }, + }, + }, }, gateway: { auth: { @@ -688,13 +711,15 @@ describe("resolveCommandSecretRefsViaGateway", () => { }, } as unknown as OpenClawConfig, commandName: "status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), mode: "read_only_status", }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBe("target-only"); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("target-only"); expect(result.hadUnresolvedTargets).toBe(false); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("resolved_local"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe( + "resolved_local", + ); } finally { if (priorTalkValue === undefined) { delete process.env[talkEnvKey]; @@ -719,17 +744,21 @@ describe("resolveCommandSecretRefsViaGateway", () => { const result = await resolveCommandSecretRefsViaGateway({ config: { talk: { - apiKey: { source: "env", provider: "default", id: envKey }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: envKey }, + }, + }, }, } as unknown as OpenClawConfig, commandName: "channels resolve", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), mode: "read_only_operational", }); - expect(readLegacyTalkApiKey(result.resolvedConfig)).toBeUndefined(); + expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined(); expect(result.hadUnresolvedTargets).toBe(true); - expect(result.targetStatesByPath["talk.apiKey"]).toBe("unresolved"); + expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved"); expect( result.diagnostics.some((entry) => entry.includes("attempted local command-secret resolution"), diff --git a/src/gateway/server-methods/secrets.test.ts b/src/gateway/server-methods/secrets.test.ts index c0afd2520dc..4b39b9bc131 100644 --- a/src/gateway/server-methods/secrets.test.ts +++ b/src/gateway/server-methods/secrets.test.ts @@ -88,9 +88,15 @@ describe("secrets handlers", () => { it("resolves requested command secret assignments from the active snapshot", async () => { const resolveSecrets = vi.fn().mockResolvedValue({ - assignments: [{ path: "talk.apiKey", pathSegments: ["talk", "apiKey"], value: "sk" }], + assignments: [ + { + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + value: "sk", + }, + ], diagnostics: ["note"], - inactiveRefPaths: ["talk.apiKey"], + inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"], }); const handlers = createHandlers({ resolveSecrets }); const respond = vi.fn(); @@ -98,17 +104,23 @@ describe("secrets handlers", () => { handlers, respond, commandName: "memory status", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }); expect(resolveSecrets).toHaveBeenCalledWith({ commandName: "memory status", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }); expect(respond).toHaveBeenCalledWith(true, { ok: true, - assignments: [{ path: "talk.apiKey", pathSegments: ["talk", "apiKey"], value: "sk" }], + assignments: [ + { + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + value: "sk", + }, + ], diagnostics: ["note"], - inactiveRefPaths: ["talk.apiKey"], + inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"], }); }); @@ -138,7 +150,7 @@ describe("secrets handlers", () => { handlers, respond, commandName: "memory status", - targetIds: ["talk.apiKey", 12], + targetIds: ["talk.providers.*.apiKey", 12], }); expect(resolveSecrets).not.toHaveBeenCalled(); expect(respond).toHaveBeenCalledWith( @@ -174,7 +186,7 @@ describe("secrets handlers", () => { it("returns unavailable when secrets.resolve handler returns an invalid payload shape", async () => { const resolveSecrets = vi.fn().mockResolvedValue({ - assignments: [{ path: "talk.apiKey", pathSegments: [""], value: "sk" }], + assignments: [{ path: "talk.providers.elevenlabs.apiKey", pathSegments: [""], value: "sk" }], diagnostics: [], inactiveRefPaths: [], }); @@ -184,7 +196,7 @@ describe("secrets handlers", () => { handlers, respond, commandName: "memory status", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }); expect(respond).toHaveBeenCalledWith( false, diff --git a/src/gateway/server.reload.test.ts b/src/gateway/server.reload.test.ts index 366d31ed20c..e9c9c07b479 100644 --- a/src/gateway/server.reload.test.ts +++ b/src/gateway/server.reload.test.ts @@ -233,10 +233,14 @@ describe("gateway hot reload", () => { await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); } - async function writeTalkApiKeyEnvRefConfig(refId = "TALK_API_KEY_REF") { + async function writeTalkProviderApiKeyEnvRefConfig(refId = "TALK_API_KEY_REF") { await writeConfigFile({ talk: { - apiKey: { source: "env", provider: "default", id: refId }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: refId }, + }, + }, }, }); } @@ -753,7 +757,7 @@ describe("gateway hot reload", () => { const refId = "RUNTIME_LKG_TALK_API_KEY"; const previousRefValue = process.env[refId]; process.env[refId] = "talk-key-before-reload-failure"; // pragma: allowlist secret - await writeTalkApiKeyEnvRefConfig(refId); + await writeTalkProviderApiKeyEnvRefConfig(refId); const { server, ws } = await startServerWithClient(); try { @@ -762,10 +766,10 @@ describe("gateway hot reload", () => { assignments?: Array<{ path: string; pathSegments: string[]; value: unknown }>; }>(ws, "secrets.resolve", { commandName: "runtime-lkg-test", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }); expect(preResolve.ok).toBe(true); - expect(preResolve.payload?.assignments?.[0]?.path).toBe("talk.apiKey"); + expect(preResolve.payload?.assignments?.[0]?.path).toBe("talk.providers.elevenlabs.apiKey"); expect(preResolve.payload?.assignments?.[0]?.value).toBe("talk-key-before-reload-failure"); delete process.env[refId]; @@ -778,10 +782,12 @@ describe("gateway hot reload", () => { assignments?: Array<{ path: string; pathSegments: string[]; value: unknown }>; }>(ws, "secrets.resolve", { commandName: "runtime-lkg-test", - targetIds: ["talk.apiKey"], + targetIds: ["talk.providers.*.apiKey"], }); expect(postResolve.ok).toBe(true); - expect(postResolve.payload?.assignments?.[0]?.path).toBe("talk.apiKey"); + expect(postResolve.payload?.assignments?.[0]?.path).toBe( + "talk.providers.elevenlabs.apiKey", + ); expect(postResolve.payload?.assignments?.[0]?.value).toBe("talk-key-before-reload-failure"); } finally { if (previousRefValue === undefined) { diff --git a/src/secrets/command-config.test.ts b/src/secrets/command-config.test.ts index 259916efcb7..ed99d0c5fb0 100644 --- a/src/secrets/command-config.test.ts +++ b/src/secrets/command-config.test.ts @@ -6,12 +6,20 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => { it("returns assignments from the active runtime snapshot for configured refs", () => { const sourceConfig = { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, } as unknown as OpenClawConfig; const resolvedConfig = { talk: { - apiKey: "talk-key", // pragma: allowlist secret + providers: { + elevenlabs: { + apiKey: "talk-key", // pragma: allowlist secret + }, + }, }, } as unknown as OpenClawConfig; @@ -19,13 +27,13 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => { sourceConfig, resolvedConfig, commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }); expect(result.assignments).toEqual([ { - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], value: "talk-key", }, ]); @@ -34,11 +42,19 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => { it("throws when configured refs are unresolved in the snapshot", () => { const sourceConfig = { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, } as unknown as OpenClawConfig; const resolvedConfig = { - talk: {}, + talk: { + providers: { + elevenlabs: {}, + }, + }, } as unknown as OpenClawConfig; expect(() => @@ -46,9 +62,11 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => { sourceConfig, resolvedConfig, commandName: "memory search", - targetIds: new Set(["talk.apiKey"]), + targetIds: new Set(["talk.providers.*.apiKey"]), }), - ).toThrow(/memory search: talk\.apiKey is unresolved in the active runtime snapshot/); + ).toThrow( + /memory search: talk\.providers\.elevenlabs\.apiKey is unresolved in the active runtime snapshot/, + ); }); it("skips unresolved refs that are marked inactive by runtime warnings", () => { diff --git a/src/secrets/configure-plan.test.ts b/src/secrets/configure-plan.test.ts index d8b360becbe..40d193c2ec4 100644 --- a/src/secrets/configure-plan.test.ts +++ b/src/secrets/configure-plan.test.ts @@ -12,7 +12,11 @@ describe("secrets configure plan helpers", () => { it("builds configure candidates from supported configure targets", () => { const config = { talk: { - apiKey: "plain", // pragma: allowlist secret + providers: { + elevenlabs: { + apiKey: "plain", // pragma: allowlist secret + }, + }, }, channels: { telegram: { @@ -23,7 +27,7 @@ describe("secrets configure plan helpers", () => { const candidates = buildConfigureCandidates(config); const paths = candidates.map((entry) => entry.path); - expect(paths).toContain("talk.apiKey"); + expect(paths).toContain("talk.providers.elevenlabs.apiKey"); expect(paths).toContain("channels.telegram.botToken"); }); @@ -84,10 +88,14 @@ describe("secrets configure plan helpers", () => { const candidates = buildConfigureCandidatesForScope({ config: { talk: { - apiKey: { - source: "env", - provider: "default", - id: "TALK_API_KEY", + providers: { + elevenlabs: { + apiKey: { + source: "env", + provider: "default", + id: "TALK_API_KEY", + }, + }, }, }, } as OpenClawConfig, @@ -113,7 +121,7 @@ describe("secrets configure plan helpers", () => { expect(candidates).toEqual( expect.arrayContaining([ expect.objectContaining({ - path: "talk.apiKey", + path: "talk.providers.elevenlabs.apiKey", existingRef: { source: "env", provider: "default", @@ -152,25 +160,24 @@ describe("secrets configure plan helpers", () => { } as OpenClawConfig, }); - const legacy = candidates.find((entry) => entry.path === "talk.apiKey"); const normalized = candidates.find( (entry) => entry.path === "talk.providers.elevenlabs.apiKey", ); - expect(legacy?.isDerived).not.toBe(true); expect(normalized?.isDerived).toBe(true); }); it("reports configure change presence and builds deterministic plan shape", () => { const selected = new Map([ [ - "talk.apiKey", + "talk.providers.elevenlabs.apiKey", { - type: "talk.apiKey", - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], - label: "talk.apiKey", + type: "talk.providers.*.apiKey", + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + label: "talk.providers.elevenlabs.apiKey", configFile: "openclaw.json" as const, expectedResolvedValue: "string" as const, + providerId: "elevenlabs", ref: { source: "env" as const, provider: "default", @@ -198,7 +205,7 @@ describe("secrets configure plan helpers", () => { generatedAt: "2026-02-28T00:00:00.000Z", }); expect(plan.targets).toHaveLength(1); - expect(plan.targets[0]?.path).toBe("talk.apiKey"); + expect(plan.targets[0]?.path).toBe("talk.providers.elevenlabs.apiKey"); expect(plan.providerUpserts).toBeDefined(); expect(plan.options).toEqual({ scrubEnv: true, diff --git a/src/secrets/exec-secret-ref-id-parity.test.ts b/src/secrets/exec-secret-ref-id-parity.test.ts index f0911cf55f7..f9099c9d6d8 100644 --- a/src/secrets/exec-secret-ref-id-parity.test.ts +++ b/src/secrets/exec-secret-ref-id-parity.test.ts @@ -42,9 +42,10 @@ describe("exec SecretRef id parity", () => { generatedBy: "manual", targets: [ { - type: "talk.apiKey", - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + type: "talk.providers.*.apiKey", + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + providerId: "elevenlabs", ref: { source: "exec", provider: "vault", id }, }, ], diff --git a/src/secrets/plan.test.ts b/src/secrets/plan.test.ts index ec4d2c8dcba..165494bfe4e 100644 --- a/src/secrets/plan.test.ts +++ b/src/secrets/plan.test.ts @@ -58,9 +58,10 @@ describe("secrets plan validation", () => { generatedBy: "manual", targets: [ { - type: "talk.apiKey", - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + type: "talk.providers.*.apiKey", + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + providerId: "elevenlabs", ref: { source: "env", provider: "default", id: "TALK_API_KEY" }, }, ], @@ -112,9 +113,10 @@ describe("secrets plan validation", () => { generatedBy: "manual", targets: [ { - type: "talk.apiKey", - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + type: "talk.providers.*.apiKey", + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + providerId: "elevenlabs", ref: { source: "exec", provider: "vault", id }, }, ], @@ -132,9 +134,10 @@ describe("secrets plan validation", () => { generatedBy: "manual", targets: [ { - type: "talk.apiKey", - path: "talk.apiKey", - pathSegments: ["talk", "apiKey"], + type: "talk.providers.*.apiKey", + path: "talk.providers.elevenlabs.apiKey", + pathSegments: ["talk", "providers", "elevenlabs", "apiKey"], + providerId: "elevenlabs", ref: { source: "exec", provider: "vault", id }, }, ], diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 6c647370718..7086499d6c1 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -312,17 +312,6 @@ const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, - { - id: "talk.apiKey", - targetType: "talk.apiKey", - configFile: "openclaw.json", - pathPattern: "talk.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, { id: "talk.providers.*.apiKey", targetType: "talk.providers.*.apiKey", diff --git a/src/secrets/target-registry-test-helpers.ts b/src/secrets/target-registry-test-helpers.ts index b4b7a92aefc..88292fd3ae9 100644 --- a/src/secrets/target-registry-test-helpers.ts +++ b/src/secrets/target-registry-test-helpers.ts @@ -2,8 +2,5 @@ export function canonicalizeSecretTargetCoverageId(id: string): string { if (id === "tools.web.x_search.apiKey") { return "plugins.entries.xai.config.webSearch.apiKey"; } - if (id === "talk.apiKey") { - return "talk.providers.elevenlabs.apiKey"; - } return id; } diff --git a/src/secrets/target-registry.test.ts b/src/secrets/target-registry.test.ts index 78e9e5f1cfe..e594557c820 100644 --- a/src/secrets/target-registry.test.ts +++ b/src/secrets/target-registry.test.ts @@ -84,7 +84,11 @@ describe("secret target registry", () => { const targets = discoverConfigSecretTargetsByIds( { talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + providers: { + elevenlabs: { + apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, + }, + }, }, gateway: { remote: { @@ -92,12 +96,12 @@ describe("secret target registry", () => { }, }, } as unknown as OpenClawConfig, - new Set(["talk.apiKey"]), + new Set(["talk.providers.*.apiKey"]), ); expect(targets).toHaveLength(1); - expect(targets[0]?.entry.id).toBe("talk.apiKey"); - expect(targets[0]?.path).toBe("talk.apiKey"); + expect(targets[0]?.entry.id).toBe("talk.providers.*.apiKey"); + expect(targets[0]?.path).toBe("talk.providers.elevenlabs.apiKey"); }); it("resolves config targets by exact path including sibling ref metadata", () => {