openclaw/src/secrets/exec-secret-ref-id-parity.test.ts
2026-03-18 15:36:32 +00:00

203 lines
6.5 KiB
TypeScript

import AjvPkg from "ajv";
import { describe, expect, it } from "vitest";
import { validateConfigObjectRaw } from "../config/validation.js";
import { SecretRefSchema as GatewaySecretRefSchema } from "../gateway/protocol/schema/primitives.js";
import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js";
import {
INVALID_EXEC_SECRET_REF_IDS,
VALID_EXEC_SECRET_REF_IDS,
} from "../test-utils/secret-ref-test-vectors.js";
import { isSecretsApplyPlan } from "./plan.js";
import { isValidExecSecretRefId } from "./ref-contract.js";
import { materializePathTokens, parsePathPattern } from "./target-registry-pattern.js";
import { listSecretTargetRegistryEntries } from "./target-registry.js";
describe("exec SecretRef id parity", () => {
const Ajv = AjvPkg as unknown as new (opts?: object) => import("ajv").default;
const ajv = new Ajv({ allErrors: true, strict: false });
const validateGatewaySecretRef = ajv.compile(GatewaySecretRefSchema);
const pluginSdkSecretInput = buildSecretInputSchema();
function configAcceptsExecRef(id: string): boolean {
const result = validateConfigObjectRaw({
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "exec", provider: "vault", id },
models: [{ id: "gpt-5", name: "gpt-5" }],
},
},
},
});
return result.ok;
}
function planAcceptsExecRef(id: string): boolean {
return isSecretsApplyPlan({
version: 1,
protocolVersion: 1,
generatedAt: "2026-03-10T00:00:00.000Z",
generatedBy: "manual",
targets: [
{
type: "talk.apiKey",
path: "talk.apiKey",
pathSegments: ["talk", "apiKey"],
ref: { source: "exec", provider: "vault", id },
},
],
});
}
for (const id of [...VALID_EXEC_SECRET_REF_IDS, ...INVALID_EXEC_SECRET_REF_IDS]) {
it(`keeps config/plan/gateway/plugin parity for exec id "${id}"`, () => {
const expected = isValidExecSecretRefId(id);
expect(configAcceptsExecRef(id)).toBe(expected);
expect(planAcceptsExecRef(id)).toBe(expected);
expect(validateGatewaySecretRef({ source: "exec", provider: "vault", id })).toBe(expected);
expect(
pluginSdkSecretInput.safeParse({ source: "exec", provider: "vault", id }).success,
).toBe(expected);
});
}
function classifyTargetClass(id: string): string {
if (id.startsWith("auth-profiles.")) {
return "auth-profiles";
}
if (id.startsWith("agents.")) {
return "agents";
}
if (id.startsWith("channels.")) {
return "channels";
}
if (id.startsWith("cron.")) {
return "cron";
}
if (id.startsWith("gateway.auth.")) {
return "gateway.auth";
}
if (id.startsWith("gateway.remote.")) {
return "gateway.remote";
}
if (id.startsWith("messages.")) {
return "messages";
}
if (id.startsWith("models.providers.") && id.includes(".headers.")) {
return "models.headers";
}
if (id.startsWith("models.providers.")) {
return "models.apiKey";
}
if (id.startsWith("skills.entries.")) {
return "skills";
}
if (id.startsWith("talk.")) {
return "talk";
}
if (id.startsWith("tools.web.fetch.")) {
return "tools.web.fetch";
}
if (id.startsWith("plugins.entries.") && id.includes(".config.webSearch.apiKey")) {
return "tools.web.search";
}
if (id.startsWith("tools.web.search.")) {
return "tools.web.search";
}
return "unclassified";
}
function samplePathSegments(pathPattern: string): string[] {
const tokens = parsePathPattern(pathPattern);
const captures = tokens.flatMap((token) => {
if (token.kind === "literal") {
return [];
}
return [token.kind === "array" ? "0" : "sample"];
});
const segments = materializePathTokens(tokens, captures);
if (!segments) {
throw new Error(`failed to sample path segments for pattern "${pathPattern}"`);
}
return segments;
}
const registryPlanTargets = listSecretTargetRegistryEntries().filter(
(entry) => entry.includeInPlan,
);
const unclassifiedTargetIds = registryPlanTargets
.filter((entry) => classifyTargetClass(entry.id) === "unclassified")
.map((entry) => entry.id);
const sampledTargetsByClass = [
...new Set(registryPlanTargets.map((entry) => classifyTargetClass(entry.id))),
]
.toSorted((a, b) => a.localeCompare(b))
.map((className) => {
const candidates = registryPlanTargets
.filter((entry) => classifyTargetClass(entry.id) === className)
.toSorted((a, b) => a.id.localeCompare(b.id));
const selected = candidates[0];
if (!selected) {
throw new Error(`missing sampled target for class "${className}"`);
}
const pathSegments = samplePathSegments(selected.pathPattern);
return {
className,
id: selected.id,
type: selected.targetType,
configFile: selected.configFile,
pathSegments,
};
});
function planAcceptsExecRefForSample(params: {
type: string;
configFile: "openclaw.json" | "auth-profiles.json";
pathSegments: string[];
id: string;
}): boolean {
return isSecretsApplyPlan({
version: 1,
protocolVersion: 1,
generatedAt: "2026-03-10T00:00:00.000Z",
generatedBy: "manual",
targets: [
{
type: params.type,
path: params.pathSegments.join("."),
pathSegments: params.pathSegments,
ref: { source: "exec", provider: "vault", id: params.id },
...(params.configFile === "auth-profiles.json" ? { agentId: "main" } : {}),
},
],
});
}
it("derives sampled class coverage from target registry metadata", () => {
expect(unclassifiedTargetIds).toEqual([]);
expect(sampledTargetsByClass.length).toBeGreaterThan(0);
});
for (const sample of sampledTargetsByClass) {
it(`rejects traversal-segment exec ids for sampled class "${sample.className}" (example: "${sample.id}")`, () => {
expect(
planAcceptsExecRefForSample({
type: sample.type,
configFile: sample.configFile,
pathSegments: sample.pathSegments,
id: "vault/openai/apiKey",
}),
).toBe(true);
expect(
planAcceptsExecRefForSample({
type: sample.type,
configFile: sample.configFile,
pathSegments: sample.pathSegments,
id: "vault/../apiKey",
}),
).toBe(false);
});
}
});