feat(backend): let pi self-authenticate providers from its own auth store

The backend injects a single managed model key (THT_MODEL_API_KEY[_FILE]) as
the selected provider's env var, but that key belongs to one provider — so
selecting a second cloud provider (e.g. DeepSeek while the managed key is zai's)
forced the wrong key onto it and failed auth. This is why the model could not be
switched to DeepSeek.

When the selected provider is present in pi's own auth store
(~/.pi/agent/auth.json), skip injection and let pi resolve that provider's key
itself. Deployments without an auth store (containers) yield an empty set, so the
managed-key injection stays authoritative and fail-fast there. authProviders is
injectable into PiProcessManager for deterministic tests.

Verified live: GLM 5.2, DeepSeek V4 Flash, and aritmolab Qwen3.6 all operate through the ThothII model selector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 18:26:18 +02:00
co-authored by Claude Opus 4.8
parent bd7fa6d2c1
commit c5fd03ed84
5 changed files with 120 additions and 2 deletions
+29 -1
View File
@@ -306,7 +306,12 @@ test.each([
child.stderr.resume = () => {};
const mgr = new PiProcessManager(loadConfig({
PI_BIN: "/usr/local/bin/pi", THT_MODEL_API_KEY_FILE: secret,
}), { spawnFn: (...args: any[]) => { calls.push(args); return child as any; } });
}), {
spawnFn: (...args: any[]) => { calls.push(args); return child as any; },
// Empty auth store: exercise the managed-key injection path deterministically,
// independent of whatever providers the developer's ~/.pi/agent/auth.json holds.
authProviders: () => new Set(),
});
try {
await mgr.spawnFor("credential-session", { provider });
const env = calls[0][2].env;
@@ -320,6 +325,29 @@ test.each([
}
});
test("skips managed-key injection for a provider present in pi's auth store", async () => {
const secret = path.resolve(__dirname, `.model-key-${process.pid}-authskip`);
writeFileSync(secret, "provider-secret", { mode: 0o600 });
const calls: any[][] = [];
const child = recordingChild();
child.stderr.resume = () => {};
const mgr = new PiProcessManager(loadConfig({
PI_BIN: "/usr/local/bin/pi", THT_MODEL_API_KEY_FILE: secret,
}), {
spawnFn: (...args: any[]) => { calls.push(args); return child as any; },
authProviders: () => new Set(["deepseek"]),
});
try {
await mgr.spawnFor("authskip-session", { provider: "deepseek" });
const env = calls[0][2].env;
// pi resolves deepseek from its own auth store, so no key is forced onto it.
expect(env.DEEPSEEK_API_KEY).toBeUndefined();
} finally {
mgr.teardown("authskip-session");
await import("node:fs/promises").then((fs) => fs.unlink(secret));
}
});
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 });