test(secrets): cover skill migration and symlinked exec command flow

This commit is contained in:
joshavant
2026-02-26 00:48:09 -06:00
committed by Peter Steinberger
parent d879c7c641
commit 7671c1dd10
2 changed files with 150 additions and 0 deletions

View File

@@ -240,6 +240,86 @@ describe("secrets apply", () => {
expect(nextConfig.models?.providers?.openai).toBeUndefined();
});
it("migrates skills entries apiKey targets alongside provider api keys", async () => {
await fs.writeFile(
configPath,
`${JSON.stringify(
{
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
api: "openai-completions",
apiKey: "sk-openai-plaintext",
models: [{ id: "gpt-5", name: "gpt-5" }],
},
},
},
skills: {
entries: {
"qa-secret-test": {
enabled: true,
apiKey: "sk-skill-plaintext",
},
},
},
},
null,
2,
)}\n`,
"utf8",
);
const plan: SecretsApplyPlan = {
version: 1,
protocolVersion: 1,
generatedAt: new Date().toISOString(),
generatedBy: "manual",
targets: [
{
type: "models.providers.apiKey",
path: "models.providers.openai.apiKey",
pathSegments: ["models", "providers", "openai", "apiKey"],
providerId: "openai",
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
{
type: "skills.entries.apiKey",
path: "skills.entries.qa-secret-test.apiKey",
pathSegments: ["skills", "entries", "qa-secret-test", "apiKey"],
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
],
options: {
scrubEnv: true,
scrubAuthProfilesForProviderTargets: true,
scrubLegacyAuthJson: true,
},
};
const result = await runSecretsApply({ plan, env, write: true });
expect(result.changed).toBe(true);
const nextConfig = JSON.parse(await fs.readFile(configPath, "utf8")) as {
models: { providers: { openai: { apiKey: unknown } } };
skills: { entries: { "qa-secret-test": { apiKey: unknown } } };
};
expect(nextConfig.models.providers.openai.apiKey).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
expect(nextConfig.skills.entries["qa-secret-test"].apiKey).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
const rawConfig = await fs.readFile(configPath, "utf8");
expect(rawConfig).not.toContain("sk-openai-plaintext");
expect(rawConfig).not.toContain("sk-skill-plaintext");
});
it("applies provider upserts and deletes from plan", async () => {
await fs.writeFile(
configPath,

View File

@@ -219,6 +219,76 @@ describe("secret ref resolver", () => {
expect(value).toBe("plain-secret");
});
it("handles Homebrew-style symlinked exec commands with args only when explicitly allowed", async () => {
if (process.platform === "win32") {
return;
}
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-homebrew-"));
cleanupRoots.push(root);
const binDir = path.join(root, "opt", "homebrew", "bin");
const cellarDir = path.join(root, "opt", "homebrew", "Cellar", "node", "25.0.0", "bin");
await fs.mkdir(binDir, { recursive: true });
await fs.mkdir(cellarDir, { recursive: true });
const targetCommand = path.join(cellarDir, "node");
const symlinkCommand = path.join(binDir, "node");
await writeSecureFile(
targetCommand,
[
`#!${process.execPath}`,
"import fs from 'node:fs';",
"const req = JSON.parse(fs.readFileSync(0, 'utf8'));",
"const suffix = process.argv[2] ?? 'missing';",
"const values = Object.fromEntries((req.ids ?? []).map((id) => [id, `${suffix}:${id}`]));",
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values }));",
].join("\n"),
0o700,
);
await fs.symlink(targetCommand, symlinkCommand);
const trustedRoot = await fs.realpath(root);
await expect(
resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkCommand,
args: ["brew"],
passEnv: ["PATH"],
},
},
},
},
},
),
).rejects.toThrow("must not be a symlink");
const value = await resolveSecretRefString(
{ source: "exec", provider: "execmain", id: "openai/api-key" },
{
config: {
secrets: {
providers: {
execmain: {
source: "exec",
command: symlinkCommand,
args: ["brew"],
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
},
},
},
},
},
);
expect(value).toBe("brew:openai/api-key");
});
it("checks trustedDirs against resolved symlink target", async () => {
if (process.platform === "win32") {
return;