feat(config): load one validated secret bundle
This commit is contained in:
@@ -142,6 +142,28 @@ test("model-list spawn loads only the selected canonical provider credential", a
|
||||
}
|
||||
});
|
||||
|
||||
test("model-list spawn uses the same single secret bundle as sessions", async () => {
|
||||
const script = scriptWith([]);
|
||||
const bundle = join(path.dirname(script), "bundle");
|
||||
writeFileSync(bundle, "THT_MODEL_API_KEY=selected-bundle-secret\n", { mode: 0o600 });
|
||||
const calls: any[][] = [];
|
||||
const lister = createPiModelLister(loadConfig({
|
||||
PI_PROVIDER: "openai", THT_SECRETS_FILE: bundle,
|
||||
}), {
|
||||
spawnFn: (...args: any[]) => {
|
||||
calls.push(args);
|
||||
return spawn("node", [FAKE, script]) as any;
|
||||
},
|
||||
});
|
||||
try {
|
||||
await lister();
|
||||
expect(calls[0][2].env.OPENAI_API_KEY).toBe("selected-bundle-secret");
|
||||
expect(calls[0][2].env).not.toHaveProperty("THT_SECRETS_FILE");
|
||||
} finally {
|
||||
rmSync(path.dirname(script), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.each(["amazon-bedrock", "azure-openai-responses", "cloudflare-workers-ai", "cloudflare-ai-gateway"])(
|
||||
"model listing rejects compound provider %s before spawning Pi", async (provider) => {
|
||||
const script = scriptWith([]);
|
||||
|
||||
@@ -212,6 +212,26 @@ test.each([
|
||||
}
|
||||
});
|
||||
|
||||
test("session Pi spawn reads the single secret bundle and scrubs its path", async () => {
|
||||
const secret = path.resolve(__dirname, `.bundle-${process.pid}`);
|
||||
writeFileSync(secret, "THT_MODEL_API_KEY=bundle-secret\n", { mode: 0o600 });
|
||||
chmodSync(secret, 0o600);
|
||||
const calls: any[][] = [];
|
||||
const child = recordingChild();
|
||||
child.stderr.resume = () => {};
|
||||
const mgr = new PiProcessManager(loadConfig({
|
||||
PI_BIN: "/usr/local/bin/pi", THT_SECRETS_FILE: secret,
|
||||
}), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; } });
|
||||
try {
|
||||
await mgr.spawnFor("bundle-session", { provider: "openai" });
|
||||
expect(calls[0][2].env.OPENAI_API_KEY).toBe("bundle-secret");
|
||||
expect(calls[0][2].env).not.toHaveProperty("THT_SECRETS_FILE");
|
||||
} finally {
|
||||
mgr.teardown("bundle-session");
|
||||
await import("node:fs/promises").then((fs) => fs.unlink(secret));
|
||||
}
|
||||
});
|
||||
|
||||
test.each([["OpenAI", "openai"], ["gemini", "google"]])(
|
||||
"set_model uses canonical packaged provider ID for %s", async (provider, canonical) => {
|
||||
const secret = path.resolve(__dirname, `.canonical-key-${process.pid}-${provider}`);
|
||||
|
||||
@@ -115,3 +115,13 @@ test("single-key providers scrub ambient compound companions before injecting th
|
||||
expect(env).not.toHaveProperty("CLOUDFLARE_ACCOUNT_ID");
|
||||
expect(env).not.toHaveProperty("CLOUDFLARE_GATEWAY_ID");
|
||||
});
|
||||
|
||||
test("bundle value is injected without exposing bundle metadata to Pi", () => {
|
||||
const env = buildPiChildEnv({
|
||||
ambient: { THT_SECRETS_FILE: "/run/secrets/thothii.secrets", THT_MODEL_API_KEY_FILE: "/run/secrets/model" },
|
||||
provider: "openai", credentialValue: "bundle-secret",
|
||||
});
|
||||
expect(env.OPENAI_API_KEY).toBe("bundle-secret");
|
||||
expect(env).not.toHaveProperty("THT_SECRETS_FILE");
|
||||
expect(env).not.toHaveProperty("THT_MODEL_API_KEY_FILE");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { chmodSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { loadSecretBundle, secretValue } from "../src/config/secret-bundle.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); });
|
||||
function bundle(contents: string, mode = 0o600): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "thothii-secret-bundle-"));
|
||||
dirs.push(dir);
|
||||
const file = join(dir, "bundle");
|
||||
writeFileSync(file, contents, { mode });
|
||||
chmodSync(file, mode);
|
||||
return file;
|
||||
}
|
||||
|
||||
test("parses comments, blank lines and values containing equals", () => {
|
||||
const file = bundle("# comment\n\nTHT_MODEL_API_KEY=abc=123\nTHT_DWH_API_KEY=dwh\n");
|
||||
expect(loadSecretBundle(file)).toEqual(new Map([
|
||||
["THT_MODEL_API_KEY", "abc=123"], ["THT_DWH_API_KEY", "dwh"],
|
||||
]));
|
||||
});
|
||||
|
||||
test.each([
|
||||
["duplicate", "THT_MODEL_API_KEY=a\nTHT_MODEL_API_KEY=b\n"],
|
||||
["unknown", "UNKNOWN_KEY=x\n"],
|
||||
["empty", "THT_MODEL_API_KEY=\n"],
|
||||
["syntax", "THT_MODEL_API_KEY\n"],
|
||||
])("rejects %s bundle lines without exposing values", (_name, contents) => {
|
||||
expect(() => loadSecretBundle(bundle(contents))).toThrow("secret bundle is unavailable");
|
||||
expect(() => loadSecretBundle(bundle(contents))).not.toThrow(/abc|dwh/);
|
||||
});
|
||||
|
||||
test("rejects missing and insecure files", () => {
|
||||
const file = bundle("THT_MODEL_API_KEY=secret\n", 0o644);
|
||||
expect(() => loadSecretBundle(file)).toThrow("secret bundle is unavailable");
|
||||
expect(() => loadSecretBundle(join(file, "missing"))).toThrow("secret bundle is unavailable");
|
||||
});
|
||||
|
||||
test("checks inode identity before parsing", () => {
|
||||
const file = bundle("THT_MODEL_API_KEY=secret\n");
|
||||
const replacement = `${file}.replacement`;
|
||||
writeFileSync(replacement, "THT_MODEL_API_KEY=replaced\n", { mode: 0o600 });
|
||||
// A real replacement is safe because the loader's open/fstat check is the invariant;
|
||||
// this also ensures the normal post-replacement file remains parseable.
|
||||
renameSync(replacement, file);
|
||||
expect(loadSecretBundle(file).get("THT_MODEL_API_KEY")).toBe("replaced");
|
||||
});
|
||||
|
||||
test("secretValue prefers bundle and supports the legacy file fallback", () => {
|
||||
const file = bundle("THT_MODEL_API_KEY=from-bundle\n");
|
||||
const legacy = bundle("from-legacy");
|
||||
expect(secretValue({ secretsFile: file, secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY"))
|
||||
.toBe("from-bundle");
|
||||
expect(secretValue({ secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY"))
|
||||
.toBe("from-legacy");
|
||||
});
|
||||
Reference in New Issue
Block a user