feat(config): load one validated secret bundle

This commit is contained in:
2026-07-12 11:06:19 +02:00
parent b539303002
commit 5fe74612fb
9 changed files with 255 additions and 2 deletions
+22
View File
@@ -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([]);
+20
View File
@@ -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}`);
+10
View File
@@ -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");
});
+58
View 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");
});