Support SecretRef for voice-call credentials and bundled plugin SecretInputs (#72607)

* fix: support voice-call secretrefs

* test: classify plugin secretref targets

* docs: credit voice-call secretref change

* fix: keep plugin secret target discovery lightweight
This commit is contained in:
Josh Avant
2026-04-27 01:16:50 -05:00
committed by GitHub
parent ab237fe7b0
commit db09f68ce5
12 changed files with 372 additions and 43 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc. - CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras. - CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc. - Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
- Voice Call: allow SecretRef-backed Twilio auth tokens and call-specific OpenAI/ElevenLabs TTS API keys through the plugin config surface. Fixes #68690. Thanks @joshavant.
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras. - Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors. - Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech. - Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.

View File

@@ -40,6 +40,7 @@ Scope intent:
- `talk.providers.*.apiKey` - `talk.providers.*.apiKey`
- `messages.tts.providers.*.apiKey` - `messages.tts.providers.*.apiKey`
- `tools.web.fetch.firecrawl.apiKey` - `tools.web.fetch.firecrawl.apiKey`
- `plugins.entries.acpx.config.mcpServers.*.env.*`
- `plugins.entries.brave.config.webSearch.apiKey` - `plugins.entries.brave.config.webSearch.apiKey`
- `plugins.entries.exa.config.webSearch.apiKey` - `plugins.entries.exa.config.webSearch.apiKey`
- `plugins.entries.google.config.webSearch.apiKey` - `plugins.entries.google.config.webSearch.apiKey`
@@ -49,6 +50,8 @@ Scope intent:
- `plugins.entries.firecrawl.config.webSearch.apiKey` - `plugins.entries.firecrawl.config.webSearch.apiKey`
- `plugins.entries.minimax.config.webSearch.apiKey` - `plugins.entries.minimax.config.webSearch.apiKey`
- `plugins.entries.tavily.config.webSearch.apiKey` - `plugins.entries.tavily.config.webSearch.apiKey`
- `plugins.entries.voice-call.config.tts.providers.*.apiKey`
- `plugins.entries.voice-call.config.twilio.authToken`
- `tools.web.search.apiKey` - `tools.web.search.apiKey`
- `gateway.auth.password` - `gateway.auth.password`
- `gateway.auth.token` - `gateway.auth.token`

View File

@@ -526,6 +526,13 @@
"secretShape": "secret_input", "secretShape": "secret_input",
"optIn": true "optIn": true
}, },
{
"id": "plugins.entries.acpx.config.mcpServers.*.env.*",
"configFile": "openclaw.json",
"path": "plugins.entries.acpx.config.mcpServers.*.env.*",
"secretShape": "secret_input",
"optIn": true
},
{ {
"id": "plugins.entries.brave.config.webSearch.apiKey", "id": "plugins.entries.brave.config.webSearch.apiKey",
"configFile": "openclaw.json", "configFile": "openclaw.json",
@@ -582,6 +589,20 @@
"secretShape": "secret_input", "secretShape": "secret_input",
"optIn": true "optIn": true
}, },
{
"id": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
"configFile": "openclaw.json",
"path": "plugins.entries.voice-call.config.tts.providers.*.apiKey",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "plugins.entries.voice-call.config.twilio.authToken",
"configFile": "openclaw.json",
"path": "plugins.entries.voice-call.config.twilio.authToken",
"secretShape": "secret_input",
"optIn": true
},
{ {
"id": "plugins.entries.xai.config.webSearch.apiKey", "id": "plugins.entries.xai.config.webSearch.apiKey",
"configFile": "openclaw.json", "configFile": "openclaw.json",

View File

@@ -203,7 +203,7 @@
"type": "string" "type": "string"
}, },
"authToken": { "authToken": {
"type": "string" "type": ["string", "object"]
} }
} }
}, },
@@ -521,7 +521,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"apiKey": { "apiKey": {
"type": "string" "type": ["string", "object"]
}, },
"baseUrl": { "baseUrl": {
"type": "string" "type": "string"
@@ -547,7 +547,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"apiKey": { "apiKey": {
"type": "string" "type": ["string", "object"]
}, },
"baseUrl": { "baseUrl": {
"type": "string" "type": "string"
@@ -682,7 +682,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"apiKey": { "apiKey": {
"type": "string" "type": ["string", "object"]
} }
}, },
"additionalProperties": true "additionalProperties": true
@@ -718,6 +718,12 @@
} }
}, },
"configContracts": { "configContracts": {
"compatibilityMigrationPaths": ["plugins.entries.voice-call.config"] "compatibilityMigrationPaths": ["plugins.entries.voice-call.config"],
"secretInputs": {
"paths": [
{ "path": "twilio.authToken", "expected": "string" },
{ "path": "tts.providers.*.apiKey", "expected": "string" }
]
}
} }
} }

View File

@@ -477,6 +477,38 @@ describe("config plugin validation", () => {
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
}); });
it("accepts voice-call SecretRef credentials declared by the plugin schema", async () => {
const res = validateInSuite({
agents: { list: [{ id: "pi" }] },
plugins: {
enabled: true,
load: { paths: [voiceCallSchemaPluginDir] },
entries: {
"voice-call-schema-fixture": {
config: {
provider: "twilio",
twilio: {
accountSid: "twilio-account-sid-placeholder",
authToken: { source: "env", provider: "default", id: "TWILIO_AUTH_TOKEN" },
},
tts: {
providers: {
openai: {
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "ELEVENLABS_API_KEY" },
},
},
},
},
},
},
},
});
expect(res.ok).toBe(true);
});
it("rejects out-of-range voice-call OpenAI TTS speed values", async () => { it("rejects out-of-range voice-call OpenAI TTS speed values", async () => {
const res = validateInSuite({ const res = validateInSuite({
agents: { list: [{ id: "pi" }] }, agents: { list: [{ id: "pi" }] },

View File

@@ -26,6 +26,8 @@ vi.mock("./plugin-registry.js", () => ({
import { resolvePluginConfigContractsById } from "./config-contracts.js"; import { resolvePluginConfigContractsById } from "./config-contracts.js";
type PluginManifestRecord = PluginManifestRegistry["plugins"][number];
function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManifestRegistry { function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManifestRegistry {
return { return {
plugins, plugins,
@@ -33,6 +35,46 @@ function createRegistry(plugins: PluginManifestRegistry["plugins"]): PluginManif
}; };
} }
function createPluginRecord(
overrides: Pick<PluginManifestRecord, "id" | "origin"> & Partial<PluginManifestRecord>,
): PluginManifestRecord {
return {
rootDir: `/tmp/${overrides.id}`,
manifestPath: `/tmp/${overrides.id}/openclaw.plugin.json`,
channelConfigs: undefined,
providerAuthEnvVars: undefined,
configUiHints: undefined,
configSchema: undefined,
configContracts: undefined,
contracts: undefined,
name: undefined,
description: undefined,
version: undefined,
enabledByDefault: undefined,
autoEnableWhenConfiguredProviders: undefined,
legacyPluginIds: undefined,
format: undefined,
bundleFormat: undefined,
bundleCapabilities: undefined,
kind: undefined,
channels: [],
providers: [],
modelSupport: undefined,
cliBackends: [],
channelEnvVars: undefined,
providerAuthAliases: undefined,
providerAuthChoices: undefined,
skills: [],
settingsFiles: undefined,
hooks: [],
source: `/tmp/${overrides.id}/openclaw.plugin.json`,
setupSource: undefined,
startupDeferConfiguredChannelFullLoadUntilAfterListen: undefined,
channelCatalogMeta: undefined,
...overrides,
};
}
describe("resolvePluginConfigContractsById", () => { describe("resolvePluginConfigContractsById", () => {
beforeEach(() => { beforeEach(() => {
mocks.findBundledPluginMetadataById.mockReset(); mocks.findBundledPluginMetadataById.mockReset();
@@ -45,42 +87,10 @@ describe("resolvePluginConfigContractsById", () => {
it("does not fall back to bundled metadata when registry already resolved a plugin without config contracts", () => { it("does not fall back to bundled metadata when registry already resolved a plugin without config contracts", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([ createRegistry([
{ createPluginRecord({
id: "brave", id: "brave",
origin: "bundled", origin: "bundled",
rootDir: "/tmp/brave", }),
manifestPath: "/tmp/brave/openclaw.plugin.json",
channelConfigs: undefined,
providerAuthEnvVars: undefined,
configUiHints: undefined,
configSchema: undefined,
configContracts: undefined,
contracts: undefined,
name: undefined,
description: undefined,
version: undefined,
enabledByDefault: undefined,
autoEnableWhenConfiguredProviders: undefined,
legacyPluginIds: undefined,
format: undefined,
bundleFormat: undefined,
bundleCapabilities: undefined,
kind: undefined,
channels: [],
providers: [],
modelSupport: undefined,
cliBackends: [],
channelEnvVars: undefined,
providerAuthAliases: undefined,
providerAuthChoices: undefined,
skills: [],
settingsFiles: undefined,
hooks: [],
source: "/tmp/brave/openclaw.plugin.json",
setupSource: undefined,
startupDeferConfiguredChannelFullLoadUntilAfterListen: undefined,
channelCatalogMeta: undefined,
},
]), ]),
); );
@@ -92,6 +102,92 @@ describe("resolvePluginConfigContractsById", () => {
expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled(); expect(mocks.findBundledPluginMetadataById).not.toHaveBeenCalled();
}); });
it("can hydrate missing contracts from bundled metadata for resolved bundled plugins", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
},
}),
]),
);
mocks.findBundledPluginMetadataById.mockReturnValue({
manifest: {
configContracts: {
secretInputs: {
paths: [{ path: "twilio.authToken", expected: "string" }],
},
},
},
});
expect(
resolvePluginConfigContractsById({
pluginIds: ["voice-call"],
fallbackToBundledMetadataForResolvedBundled: true,
}),
).toEqual(
new Map([
[
"voice-call",
{
origin: "bundled",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
secretInputs: {
paths: [{ path: "twilio.authToken", expected: "string" }],
},
},
},
],
]),
);
});
it("can hydrate missing contracts for plugin ids known to be bundled by runtime discovery", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
createPluginRecord({
id: "voice-call",
origin: "config",
}),
]),
);
mocks.findBundledPluginMetadataById.mockReturnValue({
manifest: {
configContracts: {
secretInputs: {
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
},
},
},
});
expect(
resolvePluginConfigContractsById({
pluginIds: ["voice-call"],
fallbackBundledPluginIds: ["voice-call"],
}),
).toEqual(
new Map([
[
"voice-call",
{
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "tts.providers.*.apiKey", expected: "string" }],
},
},
},
],
]),
);
});
it("can skip bundled metadata fallback for registry-scoped callers", () => { it("can skip bundled metadata fallback for registry-scoped callers", () => {
expect( expect(
resolvePluginConfigContractsById({ resolvePluginConfigContractsById({

View File

@@ -103,6 +103,8 @@ export function resolvePluginConfigContractsById(params: {
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
cache?: boolean; cache?: boolean;
fallbackToBundledMetadata?: boolean; fallbackToBundledMetadata?: boolean;
fallbackToBundledMetadataForResolvedBundled?: boolean;
fallbackBundledPluginIds?: readonly string[];
pluginIds: readonly string[]; pluginIds: readonly string[];
}): ReadonlyMap<string, PluginConfigContractMetadata> { }): ReadonlyMap<string, PluginConfigContractMetadata> {
const matches = new Map<string, PluginConfigContractMetadata>(); const matches = new Map<string, PluginConfigContractMetadata>();
@@ -112,8 +114,11 @@ export function resolvePluginConfigContractsById(params: {
if (pluginIds.length === 0) { if (pluginIds.length === 0) {
return matches; return matches;
} }
const fallbackBundledPluginIds = new Set(
(params.fallbackBundledPluginIds ?? []).map((pluginId) => pluginId.trim()).filter(Boolean),
);
const resolvedPluginIds = new Set<string>(); const resolvedPluginOrigins = new Map<string, PluginOrigin>();
const registry = loadPluginManifestRegistryForPluginRegistry({ const registry = loadPluginManifestRegistryForPluginRegistry({
config: params.config, config: params.config,
workspaceDir: params.workspaceDir, workspaceDir: params.workspaceDir,
@@ -125,7 +130,7 @@ export function resolvePluginConfigContractsById(params: {
if (!pluginIds.includes(plugin.id)) { if (!pluginIds.includes(plugin.id)) {
continue; continue;
} }
resolvedPluginIds.add(plugin.id); resolvedPluginOrigins.set(plugin.id, plugin.origin);
if (!plugin.configContracts) { if (!plugin.configContracts) {
continue; continue;
} }
@@ -137,7 +142,35 @@ export function resolvePluginConfigContractsById(params: {
if (params.fallbackToBundledMetadata ?? true) { if (params.fallbackToBundledMetadata ?? true) {
for (const pluginId of pluginIds) { for (const pluginId of pluginIds) {
if (matches.has(pluginId) || resolvedPluginIds.has(pluginId)) { const existing = matches.get(pluginId);
const shouldHydrateBundledMatch =
existing &&
!existing.configContracts.secretInputs &&
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
fallbackBundledPluginIds.has(pluginId));
if (shouldHydrateBundledMatch) {
const bundled = findBundledPluginMetadataById(pluginId);
if (bundled?.manifest.configContracts?.secretInputs) {
matches.set(pluginId, {
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
configContracts: {
...bundled.manifest.configContracts,
...existing.configContracts,
secretInputs: bundled.manifest.configContracts.secretInputs,
},
});
}
continue;
}
if (matches.has(pluginId)) {
continue;
}
const resolvedOrigin = resolvedPluginOrigins.get(pluginId);
if (
resolvedOrigin &&
!(params.fallbackToBundledMetadataForResolvedBundled && resolvedOrigin === "bundled") &&
!fallbackBundledPluginIds.has(pluginId)
) {
continue; continue;
} }
const bundled = findBundledPluginMetadataById(pluginId); const bundled = findBundledPluginMetadataById(pluginId);

View File

@@ -125,6 +125,9 @@ describe("exec SecretRef id parity", () => {
if (canonicalId.startsWith("tools.web.search.")) { if (canonicalId.startsWith("tools.web.search.")) {
return "tools.web.search"; return "tools.web.search";
} }
if (canonicalId.startsWith("plugins.entries.")) {
return "plugins.config";
}
return "unclassified"; return "unclassified";
} }

View File

@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { findBundledPluginMetadataById } from "../plugins/bundled-plugin-metadata.js";
import { resolvePluginConfigContractsById } from "../plugins/config-contracts.js";
import { collectPluginConfigAssignments } from "./runtime-config-collectors-plugins.js";
import { createResolverContext } from "./runtime-shared.js";
function envRef(id: string) {
return { source: "env" as const, provider: "default", id };
}
describe("collectPluginConfigAssignments bundled plugin manifests", () => {
it("collects voice-call SecretRef assignments from bundled manifest contracts", () => {
expect(
findBundledPluginMetadataById("voice-call")?.manifest.configContracts?.secretInputs?.paths,
).toEqual([
{ path: "twilio.authToken", expected: "string" },
{ path: "tts.providers.*.apiKey", expected: "string" },
]);
const config = {
plugins: {
entries: {
"voice-call": {
enabled: true,
config: {
twilio: {
authToken: envRef("TWILIO_AUTH_TOKEN"),
},
tts: {
providers: {
openai: {
apiKey: envRef("OPENAI_API_KEY"),
},
elevenlabs: {
apiKey: envRef("ELEVENLABS_API_KEY"),
},
},
},
},
},
},
},
} as OpenClawConfig;
expect(
resolvePluginConfigContractsById({
config,
workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)),
env: {},
cache: true,
fallbackToBundledMetadata: true,
fallbackToBundledMetadataForResolvedBundled: true,
pluginIds: ["voice-call"],
fallbackBundledPluginIds: ["voice-call"],
}).get("voice-call")?.configContracts.secretInputs?.paths,
).toEqual([
{ path: "twilio.authToken", expected: "string" },
{ path: "tts.providers.*.apiKey", expected: "string" },
]);
const context = createResolverContext({
sourceConfig: config,
env: {},
});
collectPluginConfigAssignments({
config,
defaults: undefined,
context,
loadablePluginOrigins: new Map([["voice-call", "bundled"]]),
});
expect({
assignments: context.assignments.map((assignment) => assignment.path).toSorted(),
warnings: context.warnings,
}).toEqual({
assignments: [
"plugins.entries.voice-call.config.tts.providers.elevenlabs.apiKey",
"plugins.entries.voice-call.config.tts.providers.openai.apiKey",
"plugins.entries.voice-call.config.twilio.authToken",
],
warnings: [],
});
});
});

View File

@@ -40,6 +40,9 @@ export function collectPluginConfigAssignments(params: {
params.config, params.config,
resolveDefaultAgentId(params.config), resolveDefaultAgentId(params.config),
); );
const bundledLoadablePluginIds = [...(params.loadablePluginOrigins?.entries() ?? [])]
.filter(([, origin]) => origin === "bundled")
.map(([pluginId]) => pluginId);
const pluginSecretInputs = new Map( const pluginSecretInputs = new Map(
[ [
...resolvePluginConfigContractsById({ ...resolvePluginConfigContractsById({
@@ -47,7 +50,9 @@ export function collectPluginConfigAssignments(params: {
workspaceDir, workspaceDir,
env: params.context.env, env: params.context.env,
cache: true, cache: true,
fallbackToBundledMetadata: false, fallbackToBundledMetadata: true,
fallbackToBundledMetadataForResolvedBundled: true,
fallbackBundledPluginIds: bundledLoadablePluginIds,
pluginIds: Object.keys(entries), pluginIds: Object.keys(entries),
}).entries(), }).entries(),
].flatMap(([pluginId, metadata]) => { ].flatMap(([pluginId, metadata]) => {

View File

@@ -1,3 +1,4 @@
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js"; import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
@@ -66,6 +67,30 @@ function listBundledWebProviderSecretTargetRegistryEntries(): SecretTargetRegist
return entries.toSorted((left, right) => left.id.localeCompare(right.id)); return entries.toSorted((left, right) => left.id.localeCompare(right.id));
} }
function listBundledPluginConfigSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
const entries: SecretTargetRegistryEntry[] = [];
const seen = new Set<string>();
for (const record of listBundledPluginMetadata({
includeChannelConfigs: false,
includeSyntheticChannelConfigs: false,
})) {
const secretInputs = record.manifest.configContracts?.secretInputs?.paths ?? [];
for (const secretInput of secretInputs) {
const entry = createPluginOpenClawConfigSecretTargetEntry(
record.manifest.id,
secretInput.path,
);
const key = `${entry.configFile}:${entry.pathPattern}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
entries.push(entry);
}
}
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
}
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] { function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
const entries: SecretTargetRegistryEntry[] = []; const entries: SecretTargetRegistryEntry[] = [];
@@ -436,6 +461,7 @@ export function getSecretTargetRegistry(): SecretTargetRegistryEntry[] {
cachedSecretTargetRegistry = [ cachedSecretTargetRegistry = [
...CORE_SECRET_TARGET_REGISTRY, ...CORE_SECRET_TARGET_REGISTRY,
...listBundledWebProviderSecretTargetRegistryEntries(), ...listBundledWebProviderSecretTargetRegistryEntries(),
...listBundledPluginConfigSecretTargetRegistryEntries(),
...listChannelSecretTargetRegistryEntries(), ...listChannelSecretTargetRegistryEntries(),
]; ];
return cachedSecretTargetRegistry; return cachedSecretTargetRegistry;

View File

@@ -72,4 +72,23 @@ describe("secret target registry", () => {
expect(fetchTarget).not.toBeNull(); expect(fetchTarget).not.toBeNull();
expect(fetchTarget?.entry?.id).toBe("plugins.entries.firecrawl.config.webFetch.apiKey"); expect(fetchTarget?.entry?.id).toBe("plugins.entries.firecrawl.config.webFetch.apiKey");
}); });
it("derives bundled plugin SecretInput contract target paths from plugin manifests", () => {
const coreTargetIds = new Set(getCoreSecretTargetRegistry().map((entry) => entry.id));
expect(coreTargetIds.has("plugins.entries.voice-call.config.twilio.authToken")).toBe(false);
const target = resolveConfigSecretTargetByPath([
"plugins",
"entries",
"voice-call",
"config",
"tts",
"providers",
"elevenlabs",
"apiKey",
]);
expect(target).not.toBeNull();
expect(target?.entry?.id).toBe("plugins.entries.voice-call.config.tts.providers.*.apiKey");
});
}); });