Format the TypeScript sources with Oxfmt

This commit is contained in:
Danny Kim
2026-10-16 02:05:57 +09:00
parent f001a71c61
commit 9b55e98d8c
17 changed files with 410 additions and 625 deletions
+23 -22
View File
@@ -1,13 +1,9 @@
import { dirname, join } from "jsr:@std/path@1";
export type CodexAuthentication =
| { kind: "api-key"; token: string }
| { kind: "persistent" };
export type CodexAuthentication = { kind: "api-key"; token: string } | { kind: "persistent" };
export function codexAuthentication(token?: string): CodexAuthentication {
return token === undefined
? { kind: "persistent" }
: { kind: "api-key", token };
return token === undefined ? { kind: "persistent" } : { kind: "api-key", token };
}
// Codex logs revoked, expired, or reused ChatGPT sign-ins and rejected API keys
@@ -22,17 +18,22 @@ export function isCodexAuthFailure(error: string): boolean {
// Claude Code reports a rejected CLAUDE_CODE_OAUTH_TOKEN as an is_error
// stream-json result, e.g. "Failed to authenticate. API Error: 401 OAuth
// access token is invalid." with api_error_status 401.
export function isClaudeAuthFailure(
result: { api_error_status?: number | null; result?: string },
): boolean {
return result.api_error_status === 401 ||
/failed to authenticate|oauth (?:access )?token (?:is invalid|has expired)|invalid api key|authentication_error|please run \/login|not logged in/i
.test(result.result ?? "");
export function isClaudeAuthFailure(result: {
api_error_status?: number | null;
result?: string;
}): boolean {
return (
result.api_error_status === 401 ||
/failed to authenticate|oauth (?:access )?token (?:is invalid|has expired)|invalid api key|authentication_error|please run \/login|not logged in/i.test(
result.result ?? "",
)
);
}
export async function retryInvalidToken<
T extends { status: { success: boolean }; error: string },
>(attempt: () => Promise<T>, relogin: () => Promise<void>): Promise<T> {
export async function retryInvalidToken<T extends { status: { success: boolean }; error: string }>(
attempt: () => Promise<T>,
relogin: () => Promise<void>,
): Promise<T> {
let result = await attempt();
if (!result.status.success && isCodexAuthFailure(result.error)) {
await relogin();
@@ -59,9 +60,12 @@ async function readCodexAuth(path: string): Promise<string> {
const record = value as Record<string, unknown> | null;
const tokens = record?.tokens as Record<string, unknown> | null;
if (
record === null || typeof record !== "object" ||
typeof record.auth_mode !== "string" || record.auth_mode === "" ||
tokens === null || typeof tokens !== "object" ||
record === null ||
typeof record !== "object" ||
typeof record.auth_mode !== "string" ||
record.auth_mode === "" ||
tokens === null ||
typeof tokens !== "object" ||
!["access_token", "refresh_token", "id_token", "account_id"].every(
(name) => typeof tokens[name] === "string" && tokens[name] !== "",
)
@@ -94,10 +98,7 @@ export async function stageCodexAuth(
return stagedPath;
}
export async function persistCodexAuth(
stagedPath: string,
canonicalPath: string,
): Promise<void> {
export async function persistCodexAuth(stagedPath: string, canonicalPath: string): Promise<void> {
const text = await readCodexAuth(stagedPath);
const directory = dirname(canonicalPath);
await Deno.mkdir(directory, { recursive: true, mode: 0o700 });
+20 -37
View File
@@ -15,9 +15,7 @@ Deno.test("retries once after an invalid refresh token", async () => {
() =>
Promise.resolve({
status: { success: false },
error: attempts++ === 0
? "ERROR: invalid_refresh_token"
: "still failed",
error: attempts++ === 0 ? "ERROR: invalid_refresh_token" : "still failed",
}),
() => {
relogins++;
@@ -25,11 +23,14 @@ Deno.test("retries once after an invalid refresh token", async () => {
},
);
assertEquals({ attempts, relogins, error: result.error }, {
assertEquals(
{ attempts, relogins, error: result.error },
{
attempts: 2,
relogins: 1,
error: "still failed",
});
},
);
});
Deno.test("does not retry an unrelated failure", async () => {
@@ -61,14 +62,12 @@ const REVOKED_CODEX = [
];
Deno.test("recognizes each Codex sign-in failure line", () => {
for (
const line of [
for (const line of [
...REVOKED_CODEX,
`2026-10-12T02:28:05Z ERROR codex_login::auth::manager: Failed to refresh token status=401 Unauthorized detail=TokenErrorDetail { error_code: Some("refresh_token_expired"), .. }`,
`ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again. (refresh_token_reused)`,
"ERROR: unexpected status 401 Unauthorized: Incorrect API key provided: sk-inval*d123., auth error: 401, auth error code: invalid_api_key",
]
) {
]) {
assertEquals(isCodexAuthFailure(`exec\n${line}\n`), true, line);
}
});
@@ -76,8 +75,7 @@ Deno.test("recognizes each Codex sign-in failure line", () => {
Deno.test("ignores auth words outside Codex error lines", () => {
assertEquals(
isCodexAuthFailure(
"exec\ncurl returned 401 Unauthorized\ntoken_revoked in fixture\n" +
"ERROR: tests failed\n",
"exec\ncurl returned 401 Unauthorized\ntoken_revoked in fixture\n" + "ERROR: tests failed\n",
),
false,
);
@@ -98,32 +96,30 @@ Deno.test("re-logs in after the job 9837 revoked-session stderr", async () => {
},
);
assertEquals({ relogins, success: result.status.success }, {
assertEquals(
{ relogins, success: result.status.success },
{
relogins: 1,
success: true,
});
},
);
});
Deno.test("recognizes a rejected Claude OAuth token", () => {
assertEquals(
isClaudeAuthFailure({
api_error_status: 401,
result:
"Failed to authenticate. API Error: 401 OAuth access token is invalid.",
result: "Failed to authenticate. API Error: 401 OAuth access token is invalid.",
}),
true,
);
assertEquals(
isClaudeAuthFailure({
result:
"OAuth token has expired. Please obtain a new token or refresh your existing token.",
result: "OAuth token has expired. Please obtain a new token or refresh your existing token.",
}),
true,
);
assertEquals(
isClaudeAuthFailure({ api_error_status: 429, result: "API Error: 429" }),
false,
);
assertEquals(isClaudeAuthFailure({ api_error_status: 429, result: "API Error: 429" }), false);
});
Deno.test("selects API-key login only for a configured Codex token", () => {
@@ -154,12 +150,7 @@ Deno.test("stages and persists only a regular private Codex login", async () =>
const home = `${root}/profile`;
await Deno.mkdir(home);
const staged = await stageCodexAuth(
canonical,
home,
Deno.uid()!,
Deno.gid()!,
);
const staged = await stageCodexAuth(canonical, home, Deno.uid()!, Deno.gid()!);
assertEquals(await Deno.readTextFile(staged), AUTH);
assertEquals((await Deno.stat(staged)).mode! & 0o777, 0o600);
@@ -187,16 +178,8 @@ Deno.test("rejects a symlink or malformed Codex login", async () => {
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",
);
await assertRejects(() => persistCodexAuth(linked, canonical), Error, "regular file");
await assertRejects(() => persistCodexAuth(malformed, canonical), Error, "invalid Codex login");
} finally {
await Deno.remove(root, { recursive: true });
}
+11 -35
View File
@@ -19,36 +19,22 @@ function decodeBase64Url(value: string): Uint8Array {
}
function encodeBase64Url(value: Uint8Array): string {
const binary = Array.from(value, (byte) => String.fromCharCode(byte)).join(
"",
);
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_")
.replaceAll("=", "");
const binary = Array.from(value, (byte) => String.fromCharCode(byte)).join("");
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
const buffer = (value: Uint8Array): ArrayBuffer =>
Uint8Array.from(value).buffer;
const buffer = (value: Uint8Array): ArrayBuffer => Uint8Array.from(value).buffer;
async function importMasterKey(
encoded: string,
usage: KeyUsage,
): Promise<CryptoKey> {
async function importMasterKey(encoded: string, usage: KeyUsage): Promise<CryptoKey> {
const bytes = decodeBase64Url(encoded);
if (bytes.length !== 32) {
throw new Error("master-key must be 32 bytes encoded as base64url");
}
return await crypto.subtle.importKey("raw", buffer(bytes), "AES-GCM", false, [
usage,
]);
return await crypto.subtle.importKey("raw", buffer(bytes), "AES-GCM", false, [usage]);
}
export async function decryptCredential(
encrypted: string,
masterKey: string,
): Promise<string> {
const [version, encodedNonce, encodedCiphertext, extra] = encrypted.split(
":",
);
export async function decryptCredential(encrypted: string, masterKey: string): Promise<string> {
const [version, encodedNonce, encodedCiphertext, extra] = encrypted.split(":");
if (version !== "v1" || !encodedNonce || !encodedCiphertext || extra) {
throw new Error("credential must use v1:<nonce>:<ciphertext>");
}
@@ -73,15 +59,10 @@ export async function encryptCredential(
await importMasterKey(masterKey, "encrypt"),
new TextEncoder().encode(plaintext),
);
return `v1:${encodeBase64Url(nonce)}:${
encodeBase64Url(new Uint8Array(ciphertext))
}`;
return `v1:${encodeBase64Url(nonce)}:${encodeBase64Url(new Uint8Array(ciphertext))}`;
}
export async function parseCredentials(
text: string,
masterKey: string,
): Promise<BotCredentials> {
export async function parseCredentials(text: string, masterKey: string): Promise<BotCredentials> {
const value: unknown = parse(text);
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error("credentials must be a mapping");
@@ -112,17 +93,12 @@ export async function parseCredentials(
}
const { type, model, token } = profile;
if (type !== "claude" && type !== "codex" && type !== "opencode") {
throw new Error(
`${at}.type: expected \"claude\", \"codex\", or \"opencode\"`,
);
throw new Error(`${at}.type: expected \"claude\", \"codex\", or \"opencode\"`);
}
if (model !== undefined && (typeof model !== "string" || model === "")) {
throw new Error(`${at}.model: expected a model name`);
}
if (
(type === "claude" || type === "opencode") &&
typeof token !== "string"
) {
if ((type === "claude" || type === "opencode") && typeof token !== "string") {
throw new Error(`${at}.token: expected an encrypted credential`);
}
if (token !== undefined && typeof token !== "string") {
+6 -28
View File
@@ -1,9 +1,5 @@
import { assertEquals, assertRejects } from "jsr:@std/assert@1";
import {
decryptCredential,
encryptCredential,
parseCredentials,
} from "./credentials.ts";
import { decryptCredential, encryptCredential, parseCredentials } from "./credentials.ts";
const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
const OTHER_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
@@ -11,17 +7,11 @@ const NONCE = Uint8Array.from({ length: 12 }, (_, index) => index + 32);
const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA";
Deno.test("decrypts a known AES-256-GCM credential", async () => {
assertEquals(
await decryptCredential(ENCRYPTED, MASTER_KEY),
"secret-token",
);
assertEquals(await decryptCredential(ENCRYPTED, MASTER_KEY), "secret-token");
});
Deno.test("encrypts a credential with a fresh supplied nonce", async () => {
assertEquals(
await encryptCredential("secret-token", MASTER_KEY, NONCE),
ENCRYPTED,
);
assertEquals(await encryptCredential("secret-token", MASTER_KEY, NONCE), ENCRYPTED);
});
Deno.test("rejects a credential encrypted by another master key", async () => {
@@ -30,11 +20,7 @@ Deno.test("rejects a credential encrypted by another master key", async () => {
Deno.test("rejects a credential with a nonstandard nonce", async () => {
await assertRejects(
() =>
decryptCredential(
"v1:AA:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA",
MASTER_KEY,
),
() => decryptCredential("v1:AA:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA", MASTER_KEY),
Error,
"nonce must be 12 bytes",
);
@@ -75,17 +61,9 @@ profiles:
});
Deno.test("rejects malformed central credentials", async () => {
await assertRejects(() => parseCredentials("profiles: []", MASTER_KEY), Error, "reviewer-token");
await assertRejects(
() => parseCredentials("profiles: []", MASTER_KEY),
Error,
"reviewer-token",
);
await assertRejects(
() =>
parseCredentials(
`reviewer-token: ${ENCRYPTED}\nprofiles:\n - type: claude`,
MASTER_KEY,
),
() => parseCredentials(`reviewer-token: ${ENCRYPTED}\nprofiles:\n - type: claude`, MASTER_KEY),
Error,
"profiles[0].token",
);
+4 -15
View File
@@ -14,10 +14,7 @@ export function agentCommand(
});
}
export function isRunnableProfileProcess(
status: string,
uid: number,
): boolean {
export function isRunnableProfileProcess(status: string, uid: number): boolean {
const owner = status.match(/^Uid:\s+(\d+)/m)?.[1];
const state = status.match(/^State:\s+(\w)/m)?.[1];
return owner !== undefined && Number(owner) === uid && state !== "Z";
@@ -27,9 +24,7 @@ async function profileProcessIds(uid: number): Promise<number[]> {
const ids: number[] = [];
for await (const entry of Deno.readDir("/proc")) {
if (!entry.isDirectory || !/^\d+$/.test(entry.name)) continue;
const status = await Deno.readTextFile(`/proc/${entry.name}/status`).catch(
() => null,
);
const status = await Deno.readTextFile(`/proc/${entry.name}/status`).catch(() => null);
if (status !== null && isRunnableProfileProcess(status, uid)) {
ids.push(Number(entry.name));
}
@@ -53,15 +48,9 @@ export async function killProfileProcesses(uid: number): Promise<void> {
throw new Error(`profile UID ${uid} still owns processes after cleanup`);
}
export async function readProfileFile(
profile: RuntimeProfile,
path: string,
): Promise<string> {
export async function readProfileFile(profile: RuntimeProfile, path: string): Promise<string> {
const stat = await Deno.lstat(path);
if (
!stat.isFile || stat.isSymlink || stat.uid !== profile.uid ||
(stat.mode! & 0o077) !== 0
) {
if (!stat.isFile || stat.isSymlink || stat.uid !== profile.uid || (stat.mode! & 0o077) !== 0) {
throw new Error(`${path}: expected a profile-owned regular private file`);
}
return await Deno.readTextFile(path);
+16 -21
View File
@@ -69,12 +69,12 @@ Deno.test({
const own = await agentCommand(selected, "/bin/cat", [`${homeA}/token`], {
cwd: "/",
}).output();
const sibling = await agentCommand(selected, "/bin/cat", [
`${homeB}/token`,
], { cwd: "/" }).output();
const reviewer = await agentCommand(selected, "/bin/cat", [
`${root}/reviewer`,
], { cwd: "/" }).output();
const sibling = await agentCommand(selected, "/bin/cat", [`${homeB}/token`], {
cwd: "/",
}).output();
const reviewer = await agentCommand(selected, "/bin/cat", [`${root}/reviewer`], {
cwd: "/",
}).output();
assertEquals(new TextDecoder().decode(own.stdout), "own");
assertEquals(own.success, true);
@@ -92,25 +92,22 @@ Deno.test({
async fn() {
if (Deno.uid() !== 0) return;
const selected = profile(20_010, "/tmp");
const launched = await agentCommand(selected, "/bin/sh", [
"-c",
"sleep 60 >/dev/null 2>&1 & echo $!",
], { cwd: "/" }).output();
const launched = await agentCommand(
selected,
"/bin/sh",
["-c", "sleep 60 >/dev/null 2>&1 & echo $!"],
{ cwd: "/" },
).output();
const pid = Number(new TextDecoder().decode(launched.stdout).trim());
assertEquals(Number.isInteger(pid), true);
await killProfileProcesses(selected.uid);
const status = await Deno.readTextFile(`/proc/${pid}/status`).catch(
(error) => {
const status = await Deno.readTextFile(`/proc/${pid}/status`).catch((error) => {
if (error instanceof Deno.errors.NotFound) return null;
throw error;
},
);
assertEquals(
status === null || !isRunnableProfileProcess(status, selected.uid),
true,
);
});
assertEquals(status === null || !isRunnableProfileProcess(status, selected.uid), true);
},
});
@@ -120,9 +117,7 @@ Deno.test({
async fn() {
if (Deno.uid() !== 0) return;
const selected = profile(20_020, "/tmp");
const result = await agentCommand(selected, "/usr/bin/cat", [
`/proc/${Deno.pid}/environ`,
], {
const result = await agentCommand(selected, "/usr/bin/cat", [`/proc/${Deno.pid}/environ`], {
cwd: "/",
env: { PARENT_SECRET: "must-not-cross-boundary" },
clearEnv: true,
+5 -16
View File
@@ -1,15 +1,7 @@
import { AgentAuthError } from "./profiles.ts";
export function openCodeArgs(model: string, prompt: string): string[] {
return [
"run",
"--format",
"json",
"--auto",
"--model",
model,
prompt,
];
return ["run", "--format", "json", "--auto", "--model", model, prompt];
}
type ChunkWriter = (chunk: Uint8Array) => void | Promise<void>;
@@ -20,10 +12,7 @@ export async function collectOpenCodeOutput(
writeStdout: ChunkWriter,
writeStderr: ChunkWriter,
): Promise<{ stdout: string; stderr: string }> {
const collect = async (
stream: ReadableStream<Uint8Array>,
write: ChunkWriter,
) => {
const collect = async (stream: ReadableStream<Uint8Array>, write: ChunkWriter) => {
const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
@@ -68,7 +57,8 @@ export function openCodeError(output: string): Error | undefined {
const detail = `opencode ${typeof name === "string" ? name : "error"}${
typeof statusCode === "number" ? ` (API error ${statusCode})` : ""
}: ${typeof message === "string" ? message : "unknown error"}`;
failure = statusCode === 401 || statusCode === 403
failure =
statusCode === 401 || statusCode === 403
? new AgentAuthError("opencode sign-in failed", {
cause: new Error(detail),
})
@@ -89,8 +79,7 @@ export function parseOpenCodeEvents(output: string): string {
if (typeof text === "string" && text.trim() !== "") result = text;
}
if (result === undefined) {
throw openCodeError(output) ??
new Error("OpenCode produced no text output");
throw openCodeError(output) ?? new Error("OpenCode produced no text output");
}
return result;
}
+8 -17
View File
@@ -1,9 +1,4 @@
import {
assertEquals,
assertInstanceOf,
assertRejects,
assertThrows,
} from "jsr:@std/assert@1";
import { assertEquals, assertInstanceOf, assertRejects, assertThrows } from "jsr:@std/assert@1";
import {
collectOpenCodeOutput,
openCodeArgs,
@@ -13,9 +8,7 @@ import {
import { AgentAuthError } from "./profiles.ts";
Deno.test("builds a non-interactive auto-approved OpenCode command", () => {
assertEquals(
openCodeArgs("openrouter/qwen/qwen3.8-27b", "Review this"),
[
assertEquals(openCodeArgs("openrouter/qwen/qwen3.8-27b", "Review this"), [
"run",
"--format",
"json",
@@ -23,18 +16,19 @@ Deno.test("builds a non-interactive auto-approved OpenCode command", () => {
"--model",
"openrouter/qwen/qwen3.8-27b",
"Review this",
],
);
]);
});
Deno.test("returns the final visible OpenCode text event", () => {
assertEquals(
parseOpenCodeEvents([
parseOpenCodeEvents(
[
JSON.stringify({ type: "step_start", part: { type: "step-start" } }),
JSON.stringify({ type: "text", part: { type: "text", text: "draft" } }),
JSON.stringify({ type: "text", part: { type: "text", text: "done" } }),
JSON.stringify({ type: "step_finish", part: { type: "step-finish" } }),
].join("\n")),
].join("\n"),
),
"done",
);
});
@@ -113,8 +107,5 @@ Deno.test("surfaces an OpenCode error event instead of empty output", () => {
Error,
"API error 429",
);
assertEquals(
openCodeError('{"type":"text","part":{"text":"ok"}}'),
undefined,
);
assertEquals(openCodeError('{"type":"text","part":{"text":"ok"}}'), undefined);
});
+33 -66
View File
@@ -1,15 +1,6 @@
import { join } from "jsr:@std/path@1";
import {
readRuntimeManifest,
type RuntimeManifest,
type RuntimeOutcome,
} from "./runtime.ts";
import {
failureFooter,
type FailureReason,
type ProfileFailure,
withProfile,
} from "./profiles.ts";
import { readRuntimeManifest, type RuntimeManifest, type RuntimeOutcome } from "./runtime.ts";
import { failureFooter, type FailureReason, type ProfileFailure, withProfile } from "./profiles.ts";
export type PostContext = {
apiUrl: string;
@@ -75,27 +66,30 @@ async function readOutcome(path: string): Promise<RuntimeOutcome> {
return {
ok: false,
failures: failures.flatMap((entry: unknown) => {
const { type, model, reason } = (entry ?? {}) as Record<
string,
unknown
>;
return typeof type === "string" && PROFILE_TYPES.includes(type) &&
typeof model === "string" && typeof reason === "string" &&
const { type, model, reason } = (entry ?? {}) as Record<string, unknown>;
return typeof type === "string" &&
PROFILE_TYPES.includes(type) &&
typeof model === "string" &&
typeof reason === "string" &&
FAILURE_REASONS.includes(reason)
? [{
? [
{
type: type as ProfileFailure["type"],
model,
reason: reason as FailureReason,
}]
},
]
: [];
}),
};
}
if (
outcome.ok !== true || typeof outcome.body !== "string" ||
outcome.ok !== true ||
typeof outcome.body !== "string" ||
typeof outcome.reviewPath !== "string" ||
typeof outcome.anchorsPath !== "string" ||
typeof outcome.profile !== "object" || outcome.profile === null
typeof outcome.profile !== "object" ||
outcome.profile === null
) {
throw new Error("invalid agent outcome");
}
@@ -120,9 +114,7 @@ async function readAgentArtifact(
path: string,
name: string,
): Promise<string | null> {
const profile = manifest.profiles.find((entry) =>
join(entry.home, name) === path
);
const profile = manifest.profiles.find((entry) => join(entry.home, name) === path);
if (profile === undefined) throw new Error(`${name}: outside profile home`);
let stat: Deno.FileInfo;
try {
@@ -131,10 +123,7 @@ async function readAgentArtifact(
if (error instanceof Deno.errors.NotFound) return null;
throw error;
}
if (
!stat.isFile || stat.isSymlink || stat.uid !== profile.uid ||
(stat.mode! & 0o077) !== 0
) {
if (!stat.isFile || stat.isSymlink || stat.uid !== profile.uid || (stat.mode! & 0o077) !== 0) {
throw new Error(`${name}: expected a profile-owned regular private file`);
}
const text = await Deno.readTextFile(path);
@@ -148,8 +137,7 @@ export async function postRuntime(
): Promise<void> {
const manifest = await readRuntimeManifest(manifestPath);
try {
const reviewerToken = (await Deno.readTextFile(manifest.reviewerFile))
.trim();
const reviewerToken = (await Deno.readTextFile(manifest.reviewerFile)).trim();
const request = async (path: string, body: unknown): Promise<void> => {
let response: Response;
try {
@@ -166,16 +154,13 @@ export async function postRuntime(
throw new GiteaRequestError(`${path}: ${detail}`, { cause: error });
}
if (!response.ok) {
throw new GiteaRequestError(
`${path}: ${response.status} ${await response.text()}`,
);
throw new GiteaRequestError(`${path}: ${response.status} ${await response.text()}`);
}
};
const postComment = (body: string) =>
request(
`repos/${context.repository}/issues/${context.issueIndex}/comments`,
{ body: stripAnsi(body) },
);
request(`repos/${context.repository}/issues/${context.issueIndex}/comments`, {
body: stripAnsi(body),
});
let outcome: RuntimeOutcome;
try {
@@ -190,11 +175,7 @@ export async function postRuntime(
}
try {
const review = await readAgentArtifact(
manifest,
outcome.reviewPath,
"review.md",
);
const review = await readAgentArtifact(manifest, outcome.reviewPath, "review.md");
if (review === null) {
if (context.eventName === "pull_request") {
throw new Error("agent did not write a pull request review");
@@ -205,22 +186,13 @@ export async function postRuntime(
const [verdict, ...rest] = review.split("\n");
const [event, mark] = VERDICTS[verdict.trim()] ?? ["COMMENT", "💬"];
const anchorText = await readAgentArtifact(
manifest,
outcome.anchorsPath,
"anchors.json",
);
const anchors = anchorText === null
? []
: parseAnchors(anchorText, outcome.anchorsPath);
const anchorText = await readAgentArtifact(manifest, outcome.anchorsPath, "anchors.json");
const anchors = anchorText === null ? [] : parseAnchors(anchorText, outcome.anchorsPath);
const postReview = (entries: Anchor[]) =>
request(
`repos/${context.repository}/pulls/${context.issueIndex}/reviews`,
{
body: stripAnsi(withProfile(
[`${mark} ${verdict.trim()}`, ...rest].join("\n"),
outcome.profile,
)),
request(`repos/${context.repository}/pulls/${context.issueIndex}/reviews`, {
body: stripAnsi(
withProfile([`${mark} ${verdict.trim()}`, ...rest].join("\n"), outcome.profile),
),
event,
comments: entries.map(({ path, line, side, body }) => ({
path,
@@ -228,8 +200,7 @@ export async function postRuntime(
new_position: side === "new" ? line : 0,
old_position: side === "old" ? line : 0,
})),
},
);
});
await postReview(anchors).catch(async (error: Error) => {
if (anchors.length === 0) throw error;
console.error(`inline comments rejected: ${error.message}`);
@@ -237,17 +208,13 @@ export async function postRuntime(
});
} catch (error) {
if (error instanceof GiteaRequestError) throw error;
await postComment(
failureComment([{ ...outcome.profile, reason: "failed" }]),
);
await postComment(failureComment([{ ...outcome.profile, reason: "failed" }]));
throw error;
}
} finally {
await Deno.remove(manifest.runtimeDir, { recursive: true }).catch(
(error) => {
await Deno.remove(manifest.runtimeDir, { recursive: true }).catch((error) => {
if (!(error instanceof Deno.errors.NotFound)) throw error;
},
);
});
}
}
+39 -32
View File
@@ -53,7 +53,8 @@ async function fixture(
manifestPath,
reviewerFile,
outcomeFile,
profiles: [{
profiles: [
{
index: 0,
type: "claude",
model: "test",
@@ -61,7 +62,8 @@ async function fixture(
gid: Deno.gid()!,
home: profileHome,
tokenFile: null,
}],
},
],
};
await Deno.writeTextFile(manifestPath, JSON.stringify(manifest));
return {
@@ -93,10 +95,7 @@ Deno.test("posts a successful issue result and removes runtime credentials", asy
});
assertEquals(requests.length, 1);
assertEquals(
requests[0].url,
`${context.apiUrl}/repos/owner/repo/issues/7/comments`,
);
assertEquals(requests[0].url, `${context.apiUrl}/repos/owner/repo/issues/7/comments`);
assertEquals(JSON.parse(String(requests[0].init?.body)), {
body: "done\n\n> Profile: `codex` · `gpt-test`",
});
@@ -108,22 +107,22 @@ Deno.test("posts a successful issue result and removes runtime credentials", asy
});
Deno.test("retries a rejected inline review without anchors", async () => {
const result = await fixture(
{ ok: true, body: "ignored final response" },
"Approved\nReady",
[{ path: "run.ts", line: 1, side: "new", body: "Finding" }],
);
const result = await fixture({ ok: true, body: "ignored final response" }, "Approved\nReady", [
{ path: "run.ts", line: 1, side: "new", body: "Finding" },
]);
const bodies: unknown[] = [];
await postRuntime(result.manifestPath, {
await postRuntime(
result.manifestPath,
{
...context,
eventName: "pull_request",
}, (_url, init) => {
},
(_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(
new Response(null, { status: bodies.length === 1 ? 422 : 200 }),
return Promise.resolve(new Response(null, { status: bodies.length === 1 ? 422 : 200 }));
},
);
});
assertEquals((bodies[0] as { comments: unknown[] }).comments.length, 1);
assertEquals(
@@ -159,11 +158,14 @@ Deno.test("reports each tried profile and keeps the action failed", async () =>
"agent phase failed",
);
assertEquals(bodies, [{
body: "AI bot failed.\n\n> Tried: `codex` · `gpt-test` (sign-in failed), " +
assertEquals(bodies, [
{
body:
"AI bot failed.\n\n> Tried: `codex` · `gpt-test` (sign-in failed), " +
"`claude` · `claude-test` (usage exhausted), " +
"`opencode` · `openrouter/test` (failed)",
}]);
},
]);
assert(!JSON.stringify(bodies).includes("reviewer-secret"));
assert(await missing(result.runtimeDir));
});
@@ -193,10 +195,11 @@ Deno.test("drops failure entries that are not known labels", async () => {
"agent phase failed",
);
assertEquals(bodies, [{
body:
"AI bot failed.\n\n> Tried: `claude` · `claude-test` (sign-in failed)",
}]);
assertEquals(bodies, [
{
body: "AI bot failed.\n\n> Tried: `claude` · `claude-test` (sign-in failed)",
},
]);
});
Deno.test("reports a generic failure when the runner wrote no outcome", async () => {
@@ -223,10 +226,8 @@ Deno.test("removes runtime credentials when posting fails", async () => {
await assertRejects(
() =>
postRuntime(
result.manifestPath,
context,
() => Promise.resolve(new Response("failed", { status: 500 })),
postRuntime(result.manifestPath, context, () =>
Promise.resolve(new Response("failed", { status: 500 })),
),
Error,
"500",
@@ -278,19 +279,25 @@ Deno.test("reports an attributed failure when a pull request review is missing",
await assertRejects(
() =>
postRuntime(result.manifestPath, {
postRuntime(
result.manifestPath,
{
...context,
eventName: "pull_request",
}, (_url, init) => {
},
(_url, init) => {
bodies.push(JSON.parse(String(init?.body)));
return Promise.resolve(new Response(null, { status: 201 }));
}),
},
),
Error,
"did not write",
);
assertEquals(bodies, [{
assertEquals(bodies, [
{
body: "AI bot failed.\n\n> Tried: `codex` · `gpt-test` (failed)",
}]);
},
]);
assert(await missing(result.runtimeDir));
});
+4 -8
View File
@@ -12,14 +12,10 @@ const manifest = await prepareRuntime(
env("BOT_AGENTS_MASTER_KEY"),
);
try {
await Deno.writeTextFile(
env("GITHUB_OUTPUT"),
`manifest=${manifest.manifestPath}\n`,
{ append: true },
);
await Deno.writeTextFile(env("GITHUB_OUTPUT"), `manifest=${manifest.manifestPath}\n`, {
append: true,
});
} catch (error) {
await Deno.remove(manifest.runtimeDir, { recursive: true }).catch(() =>
undefined
);
await Deno.remove(manifest.runtimeDir, { recursive: true }).catch(() => undefined);
throw error;
}
+12 -27
View File
@@ -4,16 +4,11 @@ export type BotProfile = {
token?: string;
};
export function profileFooter(
profile: Pick<BotProfile, "type" | "model">,
): string {
export function profileFooter(profile: Pick<BotProfile, "type" | "model">): string {
return `> Profile: \`${profile.type}\` · \`${profile.model}\``;
}
export function withProfile(
body: string,
profile: Pick<BotProfile, "type" | "model">,
): string {
export function withProfile(body: string, profile: Pick<BotProfile, "type" | "model">): string {
return `${body}\n\n${profileFooter(profile)}`;
}
@@ -27,8 +22,9 @@ export type ProfileFailure = Pick<BotProfile, "type" | "model"> & {
export class AgentAuthError extends Error {}
function isUsageExhausted(error: unknown): boolean {
return /(?:usage|rate) limit|hit your (?:weekly )?limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/i
.test(String(error));
return /(?:usage|rate) limit|hit your (?:weekly )?limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/i.test(
String(error),
);
}
function failureReason(error: unknown): FailureReason {
@@ -38,19 +34,12 @@ function failureReason(error: unknown): FailureReason {
export function failureFooter(failures: ProfileFailure[]): string {
if (failures.length === 0) return "> Profile: none";
return `> Tried: ${
failures
.map(({ type, model, reason }) =>
`\`${type}\` · \`${model}\` (${reason})`
)
.join(", ")
}`;
return `> Tried: ${failures
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${reason})`)
.join(", ")}`;
}
export async function runProfiles<
T,
P extends Pick<BotProfile, "type" | "model">,
>(
export async function runProfiles<T, P extends Pick<BotProfile, "type" | "model">>(
profiles: P[],
attempt: (profile: P) => Promise<T>,
failures: ProfileFailure[] = [],
@@ -68,12 +57,9 @@ export async function runProfiles<
throw new Error("profiles must contain at least one profile");
}
export function sanitizedEnvironment(
environment: Record<string, string>,
): Record<string, string> {
export function sanitizedEnvironment(environment: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
for (
const name of [
for (const name of [
"PATH",
"HOME",
"LANG",
@@ -98,8 +84,7 @@ export function sanitizedEnvironment(
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"NODE_EXTRA_CA_CERTS",
]
) {
]) {
if (environment[name] !== undefined) result[name] = environment[name];
}
return result;
+8 -12
View File
@@ -55,8 +55,7 @@ Deno.test("recognizes Claude's weekly limit message", async () => {
Deno.test("recognizes provider API exhaustion status", async () => {
const results = await Promise.all(
["API Error: 402 Insufficient funds", "HTTP error: 429"]
.map((message) =>
["API Error: 402 Insufficient funds", "HTTP error: 429"].map((message) =>
runProfiles(
[
{ type: "claude", model: "claude-sonnet-5", token: "first" },
@@ -66,7 +65,7 @@ Deno.test("recognizes provider API exhaustion status", async () => {
if (profile.type === "claude") throw new Error(message);
return Promise.resolve("fallback");
},
)
),
),
);
@@ -120,10 +119,10 @@ Deno.test("throws the last sign-in failure when every profile fails", async () =
AgentAuthError,
"claude sign-in failed",
);
assertEquals(failures.map(({ reason }) => reason), [
"sign-in failed",
"sign-in failed",
]);
assertEquals(
failures.map(({ reason }) => reason),
["sign-in failed", "sign-in failed"],
);
});
Deno.test("preserves an ordinary agent failure", async () => {
@@ -191,14 +190,11 @@ Deno.test("exposes only the selected profile credential to the agent", () => {
tokenFile: "/runtime/profile-0/token",
};
assertEquals(
agentEnvironment(base, profile, "selected"),
{
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",
+53 -94
View File
@@ -10,11 +10,7 @@ import {
retryInvalidToken,
stageCodexAuth,
} from "./auth.ts";
import {
agentCommand,
killProfileProcesses,
readProfileFile,
} from "./isolation.ts";
import { agentCommand, killProfileProcesses, readProfileFile } from "./isolation.ts";
import {
AgentAuthError,
agentEnvironment,
@@ -29,11 +25,7 @@ import {
openCodeError,
parseOpenCodeEvents,
} from "./opencode.ts";
import {
readRuntimeManifest,
type RuntimeOutcome,
type RuntimeProfile,
} from "./runtime.ts";
import { readRuntimeManifest, type RuntimeOutcome, type RuntimeProfile } from "./runtime.ts";
type GiteaUser = { login: string; email: string };
type GiteaComment = { user: GiteaUser; created_at: string; body: string };
@@ -92,17 +84,16 @@ async function runRoot(command: string, args: string[]): Promise<void> {
async function configureGitAuthor(): Promise<void> {
const user: GiteaUser = await (await gitea("user")).json();
for (const [key, value] of [["name", user.login], ["email", user.email]]) {
for (const [key, value] of [
["name", user.login],
["email", user.email],
]) {
await runRoot("git", ["config", `user.${key}`, value]);
}
}
async function readProfileToken(
profile: RuntimeProfile,
): Promise<string | undefined> {
return profile.tokenFile === null
? undefined
: await Deno.readTextFile(profile.tokenFile);
async function readProfileToken(profile: RuntimeProfile): Promise<string | undefined> {
return profile.tokenFile === null ? undefined : await Deno.readTextFile(profile.tokenFile);
}
async function runAgentCommand(
@@ -119,9 +110,7 @@ async function runAgentCommand(
if (!success) throw new Error(`${command} ${args[0]} exited with ${code}`);
}
async function prepareProfile(
profile: RuntimeProfile,
): Promise<void> {
async function prepareProfile(profile: RuntimeProfile): Promise<void> {
await runAgentCommand(profile, "git", [
"config",
"--global",
@@ -144,15 +133,13 @@ async function renderPrompt(
): Promise<{ prompt: string; reviewPath: string; anchorsPath: string }> {
const reviewPath = await makeProfileFile(profile, "review.md");
const anchorsPath = await makeProfileFile(profile, "anchors.json");
const comments: GiteaComment[] = await (await gitea(
`repos/${REPO}/issues/${INDEX}/comments?limit=100`,
)).json();
const comments: GiteaComment[] = await (
await gitea(`repos/${REPO}/issues/${INDEX}/comments?limit=100`)
).json();
const values: Record<string, string> = {
COMMENT: env("COMMENT"),
ISSUE_COMMENTS: comments
.map((comment) =>
`## ${comment.user.login} at ${comment.created_at}\n\n${comment.body}\n`
)
.map((comment) => `## ${comment.user.login} at ${comment.created_at}\n\n${comment.body}\n`)
.join("\n"),
CODE_RULES: await (await gitea(RULES_PATH)).text(),
EVENT_NAME: EVENT,
@@ -162,25 +149,19 @@ async function renderPrompt(
REVIEW_PATH: reviewPath,
ANCHORS_PATH: anchorsPath,
};
const template = await Deno.readTextFile(
new URL("../prompt.md", import.meta.url),
);
const template = await Deno.readTextFile(new URL("../prompt.md", import.meta.url));
return {
prompt: template.replace(
/\$\{(\w+)\}/g,
(match, name) => values[name] ?? match,
),
prompt: template.replace(/\$\{(\w+)\}/g, (match, name) => values[name] ?? match),
reviewPath,
anchorsPath,
};
}
async function runClaude(
profile: RuntimeProfile,
token: string,
prompt: string,
): Promise<string> {
const claude = agentCommand(profile, "claude", [
async function runClaude(profile: RuntimeProfile, token: string, prompt: string): Promise<string> {
const claude = agentCommand(
profile,
"claude",
[
"--print",
"--dangerously-skip-permissions",
"--model",
@@ -189,7 +170,8 @@ async function runClaude(
"stream-json",
"--verbose",
prompt,
], {
],
{
cwd: WORKSPACE,
env: {
...agentEnvironment(PROCESS_ENV, profile, token),
@@ -198,7 +180,8 @@ async function runClaude(
clearEnv: true,
stdout: "piped",
stderr: "piped",
}).spawn();
},
).spawn();
let result:
| {
is_error?: boolean;
@@ -230,18 +213,14 @@ async function runClaude(
}
const [status] = await Promise.all([claude.status, copyError]);
if (result?.is_error) {
const failure = new Error(
result.result ?? `claude ended with ${result.subtype}`,
);
const failure = new Error(result.result ?? `claude ended with ${result.subtype}`);
if (isClaudeAuthFailure(result)) {
throw new AgentAuthError("claude sign-in failed", { cause: failure });
}
throw failure;
}
if (result?.result === undefined) {
throw new Error(
error.trim() || `claude ended with ${result?.subtype ?? "no result"}`,
);
throw new Error(error.trim() || `claude ended with ${result?.subtype ?? "no result"}`);
}
if (!status.success) {
throw new Error(error.trim() || `claude exited with ${status.code}`);
@@ -262,9 +241,9 @@ async function codexDeviceLogin(
}).spawn();
let shown = "";
const collect = (stream: ReadableStream<Uint8Array>): Promise<void> =>
stream.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream({ write: (chunk) => void (shown += chunk) }),
);
stream
.pipeThrough(new TextDecoderStream())
.pipeTo(new WritableStream({ write: (chunk) => void (shown += chunk) }));
const drained = Promise.all([collect(login.stdout), collect(login.stderr)]);
const status = login.status;
await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -299,19 +278,13 @@ async function codexApiLogin(
const writer = login.stdin.getWriter();
await writer.write(new TextEncoder().encode(`${token}\n`));
await writer.close();
const [status, error] = await Promise.all([
login.status,
new Response(login.stderr).text(),
]);
const [status, error] = await Promise.all([login.status, new Response(login.stderr).text()]);
if (!status.success) {
throw new Error(error.trim() || `codex login exited with ${status.code}`);
}
}
async function makeProfileFile(
profile: RuntimeProfile,
name: string,
): Promise<string> {
async function makeProfileFile(profile: RuntimeProfile, name: string): Promise<string> {
const path = `${profile.home}/${name}`;
await Deno.writeTextFile(path, "", { mode: 0o600 });
await Deno.chmod(path, 0o600);
@@ -324,17 +297,12 @@ async function runCodex(
prompt: string,
authentication: CodexAuthentication,
): Promise<string> {
const token = authentication.kind === "api-key"
? authentication.token
: undefined;
const token = authentication.kind === "api-key" ? authentication.token : undefined;
const environment = agentEnvironment(PROCESS_ENV, profile, token);
if (authentication.kind === "api-key") {
await codexApiLogin(profile, environment, authentication.token);
} else {
const loggedIn = await agentCommand(profile, "codex", [
"login",
"status",
], {
const loggedIn = await agentCommand(profile, "codex", ["login", "status"], {
cwd: WORKSPACE,
env: environment,
clearEnv: true,
@@ -343,7 +311,10 @@ async function runCodex(
}
const outputPath = await makeProfileFile(profile, "result.md");
const attempt = async () => {
const codex = agentCommand(profile, "codex", [
const codex = agentCommand(
profile,
"codex",
[
"exec",
"--model",
profile.model,
@@ -351,13 +322,15 @@ async function runCodex(
"--output-last-message",
outputPath,
prompt,
], {
],
{
cwd: WORKSPACE,
env: environment,
clearEnv: true,
stdout: "inherit",
stderr: "piped",
}).spawn();
},
).spawn();
const decoder = new TextDecoder();
let error = "";
for await (const chunk of codex.stderr) {
@@ -368,16 +341,15 @@ async function runCodex(
// Codex echoes the prompt, whose comments may quote auth errors.
return { status: await codex.status, error: error.replaceAll(prompt, "") };
};
const result = authentication.kind === "api-key"
const result =
authentication.kind === "api-key"
? await attempt()
: await retryInvalidToken(attempt, async () => {
await runAgentCommand(profile, "codex", ["logout"]);
await codexDeviceLogin(profile, environment);
});
if (!result.status.success) {
const failure = new Error(
result.error.trim() || `codex exited with ${result.status.code}`,
);
const failure = new Error(result.error.trim() || `codex exited with ${result.status.code}`);
if (isCodexAuthFailure(result.error)) {
throw new AgentAuthError("codex sign-in failed", { cause: failure });
}
@@ -391,18 +363,13 @@ async function runOpenCode(
token: string,
prompt: string,
): Promise<string> {
const process = agentCommand(
profile,
"opencode",
openCodeArgs(profile.model, prompt),
{
const process = agentCommand(profile, "opencode", openCodeArgs(profile.model, prompt), {
cwd: WORKSPACE,
env: agentEnvironment(PROCESS_ENV, profile, token),
clearEnv: true,
stdout: "piped",
stderr: "piped",
},
).spawn();
}).spawn();
const output = collectOpenCodeOutput(
process.stdout,
process.stderr,
@@ -416,17 +383,16 @@ async function runOpenCode(
const [status, streams] = await Promise.all([process.status, output]);
const error = streams.stderr.trim();
if (!status.success) {
throw openCodeError(streams.stdout) ??
new Error(error || `opencode exited with ${status.code}`);
throw (
openCodeError(streams.stdout) ?? new Error(error || `opencode exited with ${status.code}`)
);
}
return parseOpenCodeEvents(streams.stdout);
}
async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
const token = await readProfileToken(profile);
const authentication = profile.type === "codex"
? codexAuthentication(token)
: null;
const authentication = profile.type === "codex" ? codexAuthentication(token) : null;
let stagedCodexAuth: string | null = null;
let body: string | undefined;
let resultPath: string | null = null;
@@ -434,12 +400,7 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
let anchorsPath: string | undefined;
try {
if (authentication?.kind === "persistent") {
stagedCodexAuth = await stageCodexAuth(
CODEX_AUTH,
profile.home,
profile.uid,
profile.gid,
);
stagedCodexAuth = await stageCodexAuth(CODEX_AUTH, profile.home, profile.uid, profile.gid);
}
await prepareProfile(profile);
const rendered = await renderPrompt(profile);
@@ -459,9 +420,7 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
}
}
if (resultPath !== null) body = await readProfileFile(profile, resultPath);
if (
body === undefined || reviewPath === undefined || anchorsPath === undefined
) {
if (body === undefined || reviewPath === undefined || anchorsPath === undefined) {
throw new Error("agent attempt produced no result");
}
return {
@@ -484,7 +443,7 @@ try {
await configureGitAuthor();
await writeOutcome({
ok: true,
...await runProfiles(MANIFEST.profiles, runProfile, failures),
...(await runProfiles(MANIFEST.profiles, runProfile, failures)),
});
} catch (error) {
console.error(error);
+2 -9
View File
@@ -30,12 +30,7 @@ export type RuntimeOutcome =
}
| { ok: false; failures: ProfileFailure[] };
async function writePrivate(
path: string,
value: string,
uid: number,
gid: number,
): Promise<void> {
async function writePrivate(path: string, value: string, uid: number, gid: number): Promise<void> {
await Deno.writeTextFile(path, value, { mode: 0o600 });
await Deno.chmod(path, 0o600);
await Deno.chown(path, uid, gid);
@@ -97,8 +92,6 @@ export async function prepareRuntime(
}
}
export async function readRuntimeManifest(
path: string,
): Promise<RuntimeManifest> {
export async function readRuntimeManifest(path: string): Promise<RuntimeManifest> {
return JSON.parse(await Deno.readTextFile(path));
}
+2 -8
View File
@@ -21,14 +21,8 @@ Deno.test({
const manifest = await prepareRuntime(action, root, MASTER_KEY);
const serialized = await Deno.readTextFile(manifest.manifestPath);
assert(!serialized.includes("secret-token"));
assertEquals(
await Deno.readTextFile(manifest.reviewerFile),
"secret-token",
);
assertEquals(
(await Deno.stat(manifest.reviewerFile)).mode! & 0o777,
0o600,
);
assertEquals(await Deno.readTextFile(manifest.reviewerFile), "secret-token");
assertEquals((await Deno.stat(manifest.reviewerFile)).mode! & 0o777, 0o600);
assertEquals((await Deno.stat(manifest.reviewerFile)).uid, 0);
const [claude, codex] = manifest.profiles;
+14 -28
View File
@@ -4,10 +4,7 @@ import { parse } from "jsr:@std/yaml@1";
type Mapping = Record<string, unknown>;
const mapping = (value: unknown, at: string): Mapping => {
assert(
typeof value === "object" && value !== null && !Array.isArray(value),
at,
);
assert(typeof value === "object" && value !== null && !Array.isArray(value), at);
return value as Mapping;
};
@@ -25,7 +22,7 @@ async function yamlFiles(directory = "."): Promise<string[]> {
for await (const entry of Deno.readDir(directory)) {
if (entry.name === ".git" || entry.name === ".superpowers") continue;
const path = directory === "." ? entry.name : `${directory}/${entry.name}`;
if (entry.isDirectory) result.push(...await yamlFiles(path));
if (entry.isDirectory) result.push(...(await yamlFiles(path)));
if (entry.isFile && /\.ya?ml$/.test(entry.name)) result.push(path);
}
return result;
@@ -52,20 +49,14 @@ Deno.test("the action exposes only the master key and scopes it to preparation",
const post = steps.find((step) => step.id === "post");
const source = steps.find((step) => step.id === "source");
assert(checkout && source && prepare && agent && post);
assertEquals(
mapping(checkout.with, "checkout.with").token,
"${{ gitea.token }}",
);
assertEquals(mapping(checkout.with, "checkout.with").token, "${{ gitea.token }}");
assertEquals(
mapping(prepare.env, "prepare.env").BOT_AGENTS_MASTER_KEY,
"${{ inputs.master-key }}",
);
assertEquals(JSON.stringify(agent).includes("BOT_AGENTS_MASTER_KEY"), false);
assertEquals(JSON.stringify(post).includes("BOT_AGENTS_MASTER_KEY"), false);
assertEquals(
mapping(agent.env, "agent.env").GITEA_TOKEN,
"${{ gitea.token }}",
);
assertEquals(mapping(agent.env, "agent.env").GITEA_TOKEN, "${{ gitea.token }}");
assert(String(post.if).includes("always()"));
for (const step of [prepare, agent, post]) {
assert(String(step.run).includes("steps.source.outputs.path"));
@@ -88,15 +79,13 @@ Deno.test("the source protection script stages immutable action files", async ()
assert(output.success, new TextDecoder().decode(output.stderr));
const path = (await Deno.readTextFile(outputFile)).trim().slice(5);
for (
const file of [
for (const file of [
"src/prepare.ts",
"src/run.ts",
"src/post.ts",
"profiles.yml",
"prompt.md",
]
) {
]) {
const stat = await Deno.stat(`${path}/${file}`);
assert(stat.isFile, file);
assertEquals(stat.mode! & 0o022, 0, file);
@@ -109,9 +98,7 @@ Deno.test("the source protection script stages immutable action files", async ()
Deno.test("module-relative runtime assets exist", async () => {
const module = new URL("run.ts", import.meta.url);
const source = await Deno.readTextFile(module);
for (
const match of source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/g)
) {
for (const match of source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/g)) {
const asset = new URL(match[1], module);
const stat = await Deno.stat(asset);
assert(stat.isFile, asset.pathname);
@@ -130,8 +117,8 @@ Deno.test("the repository uses its trusted main action", async () => {
const steps = sequence(job.steps, "jobs.agent.steps");
const local = steps.findIndex((step) => step.uses === "./");
assertEquals(local, -1);
const trusted = steps.findIndex((step) =>
step.uses === "https://git.capsulizers.com/commons/bot-agents@main"
const trusted = steps.findIndex(
(step) => step.uses === "https://git.capsulizers.com/commons/bot-agents@main",
);
assert(trusted >= 0);
assertEquals(
@@ -145,7 +132,7 @@ Deno.test("CI checks every phase and the root isolation suite", async () => {
const workflow = await yaml(".gitea/workflows/check.yml");
const job = mapping(mapping(workflow.jobs, "jobs").deno, "jobs.deno");
const commands = sequence(job.steps, "jobs.deno.steps")
.map((step) => typeof step.run === "string" ? step.run : "")
.map((step) => (typeof step.run === "string" ? step.run : ""))
.join("\n");
for (const entrypoint of ["src/prepare.ts", "src/run.ts", "src/post.ts"]) {
assert(commands.includes(entrypoint), entrypoint);
@@ -178,11 +165,10 @@ Deno.test("profile preparation does not receive provider credentials", async ()
Deno.test("central profiles fall back from Codex to Claude to OpenCode", async () => {
const credentials = await yaml("profiles.yml");
const profiles = sequence(credentials.profiles, "profiles");
assertEquals(profiles.map((profile) => profile.type), [
"codex",
"claude",
"opencode",
]);
assertEquals(
profiles.map((profile) => profile.type),
["codex", "claude", "opencode"],
);
assertEquals(profiles[2].model, "openrouter/qwen/qwen3.8-27b");
assert(String(profiles[2].token).startsWith("v1:"));
});