Skip to content

Commit 2e9ca5b

Browse files
fix(secrets): register secret targets for installed-origin plugins
The secret target registry filtered plugin-derived entries to bundled-origin records only, so when the Exa web providers moved into an installed plugin package their config targets vanished: the gateway's isKnownSecretTargetId rejected the CLI-discovered target and infer web search failed with an unresolved SecretRef before provider I/O (regression of #82621/#82798). Web-provider and config-contract entries now come from every active plugin manifest record, matching what channel entries already did; entries stay manifest-scoped (web-provider contract + sensitive hint, or declared secretInput paths), so non-bundled origins cannot widen target paths beyond their own declared contracts. Fixes #104320
1 parent b5db7d7 commit 2e9ca5b

2 files changed

Lines changed: 40 additions & 10 deletions

File tree

src/secrets/target-registry-data.current-snapshot.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,29 @@ describe("getSecretTargetRegistry metadata reuse", () => {
3232
expect(call.allowWorkspaceScopedCurrent).not.toBe(true);
3333
}
3434
});
35+
it("registers secret targets for installed-origin plugins (#104320)", async () => {
36+
// The Exa web providers moved from bundled origin to an installed plugin
37+
// package; the gateway's known-target registry must keep covering them.
38+
metadataMocks.resolvePluginMetadataSnapshot.mockReturnValue({
39+
plugins: [
40+
{
41+
id: "exa",
42+
origin: "global",
43+
channels: [],
44+
contracts: { webSearchProviders: ["exa"] },
45+
configUiHints: { "webSearch.apiKey": { sensitive: true } },
46+
configContracts: {
47+
secretInputs: { paths: [{ path: "webSearch.apiKey" }] },
48+
},
49+
},
50+
],
51+
} as never);
52+
const { getSecretTargetRegistry } = await import("./target-registry-data.js");
53+
const { isKnownSecretTargetId } = await import("./target-registry-query.js");
54+
55+
const ids = getSecretTargetRegistry().map((entry) => entry.id);
56+
57+
expect(ids).toContain("plugins.entries.exa.config.webSearch.apiKey");
58+
expect(isKnownSecretTargetId("plugins.entries.exa.config.webSearch.apiKey")).toBe(true);
59+
});
3560
});

src/secrets/target-registry-data.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ function hasWebProviderContract(
4949
return (plugin.contracts?.[contract]?.length ?? 0) > 0;
5050
}
5151

52-
function listBundledWebProviderSecretTargetRegistryEntries(
53-
bundledPlugins: readonly PluginManifestRecord[],
52+
function listPluginWebProviderSecretTargetRegistryEntries(
53+
plugins: readonly PluginManifestRecord[],
5454
): SecretTargetRegistryEntry[] {
5555
const entries: SecretTargetRegistryEntry[] = [];
56-
for (const record of bundledPlugins) {
56+
for (const record of plugins) {
5757
for (const config of WEB_PROVIDER_SECRET_CONFIGS) {
5858
if (
5959
hasWebProviderContract(record, config.contract) &&
@@ -66,12 +66,12 @@ function listBundledWebProviderSecretTargetRegistryEntries(
6666
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
6767
}
6868

69-
function listBundledPluginConfigSecretTargetRegistryEntries(
70-
bundledPlugins: readonly Pick<PluginManifestRecord, "id" | "configContracts">[],
69+
function listPluginConfigSecretTargetRegistryEntries(
70+
plugins: readonly Pick<PluginManifestRecord, "id" | "configContracts">[],
7171
): SecretTargetRegistryEntry[] {
7272
const entries: SecretTargetRegistryEntry[] = [];
7373
const seen = new Set<string>();
74-
for (const record of bundledPlugins) {
74+
for (const record of plugins) {
7575
const secretInputs = record.configContracts?.secretInputs?.paths ?? [];
7676
for (const secretInput of secretInputs) {
7777
const entry = createPluginOpenClawConfigSecretTargetEntry(record.id, secretInput.path);
@@ -505,13 +505,18 @@ function loadSecretTargetRegistryFromPluginMetadata(params: {
505505
env: params.env,
506506
...(params.preferPersisted !== undefined ? { preferPersisted: params.preferPersisted } : {}),
507507
}).plugins;
508-
const bundledPlugins = plugins.filter((record) => record.origin === "bundled");
509508
const channelPlugins = plugins.filter((record) => record.channels.length > 0);
509+
// Installed/workspace plugins own secret targets exactly like bundled ones
510+
// (#104320: the Exa split moved web providers out of bundled origin and their
511+
// targets vanished from the gateway's known-target registry). Entries stay
512+
// manifest-scoped — web-provider contract + sensitive hint, or declared
513+
// secretInput paths — so a non-bundled origin cannot widen target paths
514+
// beyond its own declared contracts.
510515
return [
511516
...CORE_SECRET_TARGET_REGISTRY,
512-
...listBundledWebProviderSecretTargetRegistryEntries(bundledPlugins),
513-
...listBundledPluginConfigSecretTargetRegistryEntries([
514-
...bundledPlugins,
517+
...listPluginWebProviderSecretTargetRegistryEntries(plugins),
518+
...listPluginConfigSecretTargetRegistryEntries([
519+
...plugins,
515520
...listSourceBundledPluginConfigContractRecords(),
516521
]),
517522
...listChannelSecretTargetRegistryEntries(channelPlugins),

0 commit comments

Comments
 (0)