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:
Danny Kim
2026-09-23 20:15:29 +00:00
committed by temeddix
parent ba9e0c0787
commit e56ea246bf
27 changed files with 2218 additions and 701 deletions
+160
View File
@@ -0,0 +1,160 @@
import { assertEquals, assertRejects } from "jsr:@std/assert@1";
import {
agentEnvironment,
runProfiles,
sanitizedEnvironment,
} from "./profiles.ts";
Deno.test("falls through only after usage exhaustion", async () => {
const visited: string[] = [];
const result = await runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
if (profile.type === "claude") {
throw new Error("You've hit your usage limit");
}
return Promise.resolve("done");
},
);
assertEquals(result, "done");
assertEquals(visited, ["claude", "codex"]);
});
Deno.test("recognizes Claude's weekly limit message", async () => {
const result = await runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
if (profile.type === "claude") {
throw new Error("You've hit your limit · resets Monday");
}
return Promise.resolve("fallback");
},
);
assertEquals(result, "fallback");
});
Deno.test("recognizes provider API exhaustion status", async () => {
const results = await Promise.all(
["API Error: 402 Insufficient funds", "HTTP error: 429"]
.map((message) =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
if (profile.type === "claude") throw new Error(message);
return Promise.resolve("fallback");
},
)
),
);
assertEquals(results, ["fallback", "fallback"]);
});
Deno.test("preserves an ordinary agent failure", async () => {
const visited: string[] = [];
await assertRejects(
() =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
throw new Error("tests failed");
},
),
Error,
"tests failed",
);
assertEquals(visited, ["claude"]);
});
Deno.test("does not mistake incidental quota text for exhaustion", async () => {
const visited: string[] = [];
await assertRejects(
() =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
visited.push(profile.type);
throw new Error("quota parser test failed at line 429");
},
),
Error,
"quota parser test failed",
);
assertEquals(visited, ["claude"]);
});
Deno.test("exposes only the selected profile credential to the agent", () => {
const base = {
PATH: "/usr/bin",
HOME: "/home/bot",
UNRELATED_SECRET: "must-not-reach-agent",
BOT_AGENTS_MASTER_KEY: "master",
PROFILES: "contains every secret",
REVIEWER_TOKEN: "reviewer",
CLAUDE_CODE_OAUTH_TOKEN: "old-claude",
ANTHROPIC_API_KEY: "old-anthropic-key",
ANTHROPIC_AUTH_TOKEN: "old-anthropic-token",
OPENAI_API_KEY: "old-openai",
CODEX_API_KEY: "old-codex-key",
CODEX_ACCESS_TOKEN: "old-codex-token",
};
const profile = {
index: 0,
type: "claude" as const,
model: "claude-sonnet-5",
uid: 20_000,
gid: 20_000,
home: "/runtime/profile-0",
tokenFile: "/runtime/profile-0/token",
};
assertEquals(
agentEnvironment(base, profile, "selected"),
{
PATH: "/usr/bin",
HOME: "/runtime/profile-0",
CLAUDE_CODE_OAUTH_TOKEN: "selected",
},
);
assertEquals(sanitizedEnvironment(base), {
PATH: "/usr/bin",
HOME: "/home/bot",
});
});
Deno.test("uses a private Codex home without inventing an API credential", () => {
const profile = {
index: 1,
type: "codex" as const,
model: "gpt-5.6-terra",
uid: 20_001,
gid: 20_001,
home: "/runtime/profile-1",
tokenFile: null,
};
assertEquals(agentEnvironment({ PATH: "/bin" }, profile), {
PATH: "/bin",
HOME: "/runtime/profile-1",
CODEX_HOME: "/runtime/profile-1/.codex",
});
});