Skip to content

Commit 90d13e0

Browse files
committed
fix(secrets): preserve plugin policy boundaries
1 parent b9a3097 commit 90d13e0

8 files changed

Lines changed: 87 additions & 41 deletions

File tree

docs/plugins/sdk-subpaths.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ usage endpoint failed or returned no usable usage data.
227227
| `plugin-sdk/channel-secret-runtime` | Deprecated broad secret-contract surface (`collectSimpleChannelFieldAssignments`, `getChannelSurface`, `pushAssignment`, secret target types); prefer the focused subpaths below |
228228
| `plugin-sdk/channel-secret-basic-runtime` | Narrow secret-contract exports for non-TTS channel/plugin secret surfaces |
229229
| `plugin-sdk/channel-secret-tts-runtime` | Narrow nested channel TTS secret assignment helpers |
230-
| `plugin-sdk/secret-ref-runtime` | Narrow SecretRef typing, resolution, and apply-plan target helpers for secret-contract/config parsing |
230+
| `plugin-sdk/secret-ref-runtime` | Narrow SecretRef typing, resolution, and plan-target path lookup for secret-contract/config parsing |
231231
| `plugin-sdk/secret-provider-integration` | Type-only SecretRef provider integration manifest and preset contracts for plugins that publish external secret provider presets |
232232
| `plugin-sdk/security-runtime` | Deprecated broad barrel for trust, DM gating, root-bounded file/path helpers including create-only writes, sync/async atomic file replacement, sibling temp writes, cross-device move fallback, private file-store helpers, symlink-parent guards, external-content, sensitive text redaction, constant-time secret comparison, and secret-collection helpers; prefer focused security/SSRF/secret subpaths |
233233
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |

extensions/vault/src/cli.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import path from "node:path";
33
import { createInterface } from "node:readline/promises";
44
import { fileURLToPath } from "node:url";
55
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
6-
import {
7-
resolveConfigSecretTargetByPath,
8-
resolvePlanTargetAgainstRegistry,
9-
} from "openclaw/plugin-sdk/secret-ref-runtime";
6+
import { resolveSecretPlanTargetByPath } from "openclaw/plugin-sdk/secret-ref-runtime";
107
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
118
import { parseVaultSecretId } from "../vault-secret-id.js";
129

@@ -100,7 +97,6 @@ const VAULT_PROVIDER_ALIAS = "vault";
10097
const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
10198
const MODEL_PROVIDER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
10299
const FORBIDDEN_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
103-
const AUTH_PROFILE_TARGET_TYPES = ["auth-profiles.api_key.key", "auth-profiles.token.token"];
104100

105101
function writeLine(message = ""): void {
106102
process.stdout.write(`${message}\n`);
@@ -283,16 +279,6 @@ function parseTargetSpecifier(value: string): {
283279
};
284280
}
285281

286-
function resolveAuthProfileSecretTargetByPath(pathSegments: string[]) {
287-
for (const type of AUTH_PROFILE_TARGET_TYPES) {
288-
const resolved = resolvePlanTargetAgainstRegistry({ type, pathSegments });
289-
if (resolved) {
290-
return resolved;
291-
}
292-
}
293-
return null;
294-
}
295-
296282
function createConfigSecretTarget(params: {
297283
providerAlias: string;
298284
path: string;
@@ -308,14 +294,15 @@ function createConfigSecretTarget(params: {
308294
) {
309295
throw new Error(`Invalid --target config path: ${params.path}`);
310296
}
311-
const resolved = params.agentId
312-
? resolveAuthProfileSecretTargetByPath(pathSegments)
313-
: resolveConfigSecretTargetByPath(pathSegments);
297+
const resolved = resolveSecretPlanTargetByPath({
298+
configFile: params.agentId ? "auth-profiles.json" : "openclaw.json",
299+
pathSegments,
300+
});
314301
if (!resolved) {
315302
throw new Error(`Unknown or unsupported Vault setup target path: ${params.path}`);
316303
}
317304
return {
318-
type: resolved.entry.targetType,
305+
type: resolved.targetType,
319306
path: normalizedPath,
320307
pathSegments,
321308
...(params.agentId ? { agentId: params.agentId } : {}),

scripts/plugin-sdk-surface-report.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
195195
),
196196
publicExports: readPluginSdkSurfaceBudgetEnv(
197197
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
198-
10468,
198+
10467,
199199
env,
200200
),
201201
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
202202
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
203-
5224,
203+
5223,
204204
env,
205205
),
206206
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
// Narrow shared secret-ref helpers for plugin config and secret-contract paths.
22

3+
import { resolveSecretPlanTargetByPath as resolveSecretPlanTargetByPathInternal } from "../secrets/target-registry-query.js";
4+
35
export { coerceSecretRef } from "../config/types.secrets.js";
46
export type { SecretInput, SecretRef } from "../config/types.secrets.js";
57
export { resolveSecretRefValues } from "../secrets/resolve.js";
68
export { applyResolvedAssignments, createResolverContext } from "../secrets/runtime-shared.js";
7-
export {
8-
resolveConfigSecretTargetByPath,
9-
resolvePlanTargetAgainstRegistry,
10-
} from "../secrets/target-registry-query.js";
11-
export type { ResolvedPlanTarget } from "../secrets/target-registry-types.js";
9+
10+
export type ResolvedSecretPlanTarget = {
11+
targetType: string;
12+
providerId?: string;
13+
accountId?: string;
14+
};
15+
16+
export function resolveSecretPlanTargetByPath(params: {
17+
configFile: "openclaw.json" | "auth-profiles.json";
18+
pathSegments: string[];
19+
}): ResolvedSecretPlanTarget | null {
20+
const resolved = resolveSecretPlanTargetByPathInternal(params);
21+
if (!resolved) {
22+
return null;
23+
}
24+
return {
25+
targetType: resolved.entry.targetType,
26+
...(resolved.providerId ? { providerId: resolved.providerId } : {}),
27+
...(resolved.accountId ? { accountId: resolved.accountId } : {}),
28+
};
29+
}

src/secrets/apply.test.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,7 +1282,7 @@ describe("secrets apply", () => {
12821282
);
12831283
});
12841284

1285-
it("adds plugin-managed exec provider owners to restrictive plugin allowlists", async () => {
1285+
it("does not widen restrictive plugin allowlists for plugin-managed exec provider upserts", async () => {
12861286
await writeJsonFile(fixture.configPath, {
12871287
plugins: {
12881288
allow: ["openai"],
@@ -1302,17 +1302,14 @@ describe("secrets apply", () => {
13021302
targets: [],
13031303
});
13041304

1305-
const nextConfig = (await applyTesting.projectConfigForTest({
1306-
plan,
1307-
env: fixture.env,
1308-
})) as {
1309-
plugins?: {
1310-
allow?: string[];
1311-
entries?: Record<string, unknown>;
1312-
};
1313-
};
1314-
expect(nextConfig.plugins?.allow).toEqual(["openai", "vault"]);
1315-
expect(nextConfig.plugins?.entries?.vault).toEqual({ enabled: true });
1305+
await expect(
1306+
applyTesting.projectConfigForTest({
1307+
plan,
1308+
env: fixture.env,
1309+
}),
1310+
).rejects.toThrow(
1311+
'Cannot apply plugin-managed SecretRef provider "vault" because plugins.allow does not include "vault". Add the plugin to plugins.allow before applying this plan.',
1312+
);
13161313
});
13171314

13181315
it("preserves normalized restrictive plugin allowlist entries for plugin-managed exec provider upserts", async () => {

src/secrets/apply.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ function applyProviderPlanMutations(params: {
249249
params.config.plugins.allow.length > 0 &&
250250
!hasPluginPolicyId(params.config.plugins.allow, pluginId)
251251
) {
252-
params.config.plugins.allow = [...params.config.plugins.allow, pluginId];
253-
changed = true;
252+
throw new Error(
253+
`Cannot apply plugin-managed SecretRef provider "${pluginId}" because plugins.allow does not include "${pluginId}". Add the plugin to plugins.allow before applying this plan.`,
254+
);
254255
}
255256
params.config.plugins.entries ??= {};
256257
if (previousEntry?.enabled === true) {

src/secrets/target-registry-query.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import type {
1414
DiscoveredConfigSecretTarget,
1515
ResolvedPlanTarget,
16+
SecretTargetConfigFile,
1617
SecretTargetRegistryEntry,
1718
} from "./target-registry-types.js";
1819

@@ -380,6 +381,32 @@ function resolvePlanTargetAgainstEntries(
380381
return null;
381382
}
382383

384+
/**
385+
* Resolves a plan-capable secret target by owning config document and concrete path.
386+
*/
387+
export function resolveSecretPlanTargetByPath(params: {
388+
configFile: SecretTargetConfigFile;
389+
pathSegments: string[];
390+
}): ResolvedPlanTarget | null {
391+
if (params.configFile === "openclaw.json") {
392+
return resolveConfigSecretTargetByPath(params.pathSegments);
393+
}
394+
for (const entry of getCompiledSecretTargetRegistryState().authProfilesCompiledSecretTargets) {
395+
if (!entry.includeInPlan) {
396+
continue;
397+
}
398+
const matched = matchPathTokens(params.pathSegments, entry.pathTokens);
399+
if (!matched) {
400+
continue;
401+
}
402+
const resolved = toResolvedPlanTarget(entry, params.pathSegments, matched.captures);
403+
if (resolved) {
404+
return resolved;
405+
}
406+
}
407+
return null;
408+
}
409+
383410
/**
384411
* Resolves an openclaw.json config path to the matching plan-capable secrets target.
385412
*/

src/secrets/target-registry.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getCoreSecretTargetRegistry } from "./target-registry-data.js";
1010
import {
1111
discoverConfigSecretTargetsByIds,
1212
resolveConfigSecretTargetByPath,
13+
resolveSecretPlanTargetByPath,
1314
} from "./target-registry.js";
1415

1516
describe("secret target registry", () => {
@@ -61,6 +62,21 @@ describe("secret target registry", () => {
6162
expect(target).toBeNull();
6263
});
6364

65+
it("resolves plan targets by owning config document", () => {
66+
const configTarget = resolveSecretPlanTargetByPath({
67+
configFile: "openclaw.json",
68+
pathSegments: ["models", "providers", "openai", "apiKey"],
69+
});
70+
const authProfileTarget = resolveSecretPlanTargetByPath({
71+
configFile: "auth-profiles.json",
72+
pathSegments: ["profiles", "openai:default", "key"],
73+
});
74+
75+
expect(configTarget?.entry.targetType).toBe("models.providers.apiKey");
76+
expect(configTarget?.providerId).toBe("openai");
77+
expect(authProfileTarget?.entry.targetType).toBe("auth-profiles.api_key.key");
78+
});
79+
6480
it("derives bundled web provider api key target paths from plugin manifests", () => {
6581
const coreTargetIds = new Set(getCoreSecretTargetRegistry().map((entry) => entry.id));
6682
expect(coreTargetIds.has("plugins.entries.exa.config.webSearch.apiKey")).toBe(false);

0 commit comments

Comments
 (0)