Centralize encrypted bot profiles (#14)
Centralize ordered bot profiles and encrypted credentials in `bot-agents`. - Exposes one action input: `master-key`. - Uses Gitea’s automatic job token internally; dependent repositories provide no author/reviewer/profile inputs. - Keeps encrypted Claude and reviewer credentials in `profiles.yml`; tokenless Codex uses its persisted subscription login. - Falls back only on recognized quota exhaustion, with a distinct UID/home per profile and descendant cleanup before fallback. - Separates decryption, agent execution, and reviewer posting so privileged credentials never enter an agent process. - Installs `obra/superpowers` with the standard Skills CLI; removes custom discovery code. - Adds root-level isolation, rollback, symlink, fallback, posting, YAML, and self-workflow CI coverage. Checks: formatting, lint, all entrypoint type checks, 35 tests as root, diff check, secret scan, and repository diagnostics. Reviewed-on: #14 Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { assertEquals, assertRejects } from "jsr:@std/assert@1";
|
||||
import {
|
||||
codexAuthentication,
|
||||
persistCodexAuth,
|
||||
retryInvalidToken,
|
||||
stageCodexAuth,
|
||||
} from "./auth.ts";
|
||||
|
||||
Deno.test("retries once after an invalid refresh token", async () => {
|
||||
let attempts = 0;
|
||||
let relogins = 0;
|
||||
const result = await retryInvalidToken(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
status: { success: false },
|
||||
error: attempts++ === 0 ? "invalid_refresh_token" : "still failed",
|
||||
}),
|
||||
() => {
|
||||
relogins++;
|
||||
return Promise.resolve();
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals({ attempts, relogins, error: result.error }, {
|
||||
attempts: 2,
|
||||
relogins: 1,
|
||||
error: "still failed",
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("does not retry an unrelated failure", async () => {
|
||||
let attempts = 0;
|
||||
let relogins = 0;
|
||||
await retryInvalidToken(
|
||||
() => {
|
||||
attempts++;
|
||||
return Promise.resolve({
|
||||
status: { success: false },
|
||||
error: "rate limited",
|
||||
});
|
||||
},
|
||||
() => {
|
||||
relogins++;
|
||||
return Promise.resolve();
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals({ attempts, relogins }, { attempts: 1, relogins: 0 });
|
||||
});
|
||||
|
||||
Deno.test("selects API-key login only for a configured Codex token", () => {
|
||||
assertEquals(codexAuthentication("selected-key"), {
|
||||
kind: "api-key",
|
||||
token: "selected-key",
|
||||
});
|
||||
assertEquals(codexAuthentication(undefined), { kind: "persistent" });
|
||||
});
|
||||
|
||||
const AUTH = JSON.stringify({
|
||||
auth_mode: "chatgpt",
|
||||
tokens: {
|
||||
access_token: "access",
|
||||
refresh_token: "refresh",
|
||||
id_token: "id",
|
||||
account_id: "account",
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test("stages and persists only a regular private Codex login", async () => {
|
||||
if (Deno.build.os === "windows") return;
|
||||
const root = await Deno.makeTempDir();
|
||||
try {
|
||||
const canonical = `${root}/persistent/auth.json`;
|
||||
await Deno.mkdir(`${root}/persistent`);
|
||||
await Deno.writeTextFile(canonical, AUTH, { mode: 0o600 });
|
||||
const home = `${root}/profile`;
|
||||
await Deno.mkdir(home);
|
||||
|
||||
const staged = await stageCodexAuth(
|
||||
canonical,
|
||||
home,
|
||||
Deno.uid()!,
|
||||
Deno.gid()!,
|
||||
);
|
||||
assertEquals(await Deno.readTextFile(staged), AUTH);
|
||||
assertEquals((await Deno.stat(staged)).mode! & 0o777, 0o600);
|
||||
|
||||
const refreshed = JSON.stringify({
|
||||
...JSON.parse(AUTH),
|
||||
tokens: { ...JSON.parse(AUTH).tokens, access_token: "refreshed" },
|
||||
});
|
||||
await Deno.writeTextFile(staged, refreshed);
|
||||
await persistCodexAuth(staged, canonical);
|
||||
assertEquals(await Deno.readTextFile(canonical), refreshed);
|
||||
assertEquals((await Deno.stat(canonical)).mode! & 0o777, 0o600);
|
||||
} finally {
|
||||
await Deno.remove(root, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("rejects a symlink or malformed Codex login", async () => {
|
||||
if (Deno.build.os === "windows") return;
|
||||
const root = await Deno.makeTempDir();
|
||||
try {
|
||||
const canonical = `${root}/auth.json`;
|
||||
const malformed = `${root}/malformed.json`;
|
||||
const linked = `${root}/linked.json`;
|
||||
await Deno.writeTextFile(canonical, AUTH, { mode: 0o600 });
|
||||
await Deno.writeTextFile(malformed, '{"tokens":{}}', { mode: 0o600 });
|
||||
await Deno.symlink(canonical, linked);
|
||||
|
||||
await assertRejects(
|
||||
() => persistCodexAuth(linked, canonical),
|
||||
Error,
|
||||
"regular file",
|
||||
);
|
||||
await assertRejects(
|
||||
() => persistCodexAuth(malformed, canonical),
|
||||
Error,
|
||||
"invalid Codex login",
|
||||
);
|
||||
} finally {
|
||||
await Deno.remove(root, { recursive: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user