203 lines
6.5 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
});
|