Replace NodeJS dict aliases with maps and named field reads #24

Merged
temeddix merged 20 commits from honest-dict-types into main 2026-10-20 05:01:51 +00:00
19 changed files with 293 additions and 157 deletions
Showing only changes of commit d09806daad - Show all commits
+116 -70
View File
@@ -1,7 +1,7 @@
/**
* `project` — lint rules this repository holds its own source to, on top of
* the Oxlint categories in `vite.config.ts`. Its rules of its own are
* `no-disable-comments` and `no-asset-imports`.
* `project` — this repository's changes to the Oxlint categories in
* `vite.config.ts`. Its rules of its own are `no-disable-comments` and
* `no-asset-imports`.
*
* Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry.
*
@@ -14,7 +14,7 @@ import type { DummyRuleMap } from "vite-plus/lint";
/** The source code both hosts hand a rule, with the comment list. */
interface ProjectSourceCode extends TextSourceCode {
getAllComments(): TSESTree.Comment[];
getAllComments: () => TSESTree.Comment[];
}
/** ESLint's default rule types, reporting TypeScript-ESTree nodes. */
@@ -161,11 +161,16 @@ const noAssetImportsRule: ProjectRule = {
};
/**
* This repository's own rule list, spread by `vite.config.ts`: rules that
* sit outside the Oxlint categories, plus the template's extras. Built-in
* Oxlint rule ids sit here too, so `vite.config.ts` lists no rule by name.
* This repository's changes to the Oxlint categories in `vite.config.ts`,
* which turn every non-nursery category on: its own rules, the category
* rules it turns off, and options that differ from a rule's defaults.
*/
const rules: DummyRuleMap = {
// Its own rules.
"project/no-disable-comments": "error",
"project/no-asset-imports": "error",
// Options.
// The explicit-types template's `camelcase` stand-in: no `_` inside a name
// once leading and trailing ones are trimmed, unless it has no lowercase
// ASCII letter (UPPER_SNAKE).
@@ -174,31 +179,6 @@ const rules: DummyRuleMap = {
"^_*[^_]*_*$|^[^a-z]*$",
{ properties: false, classFields: false, onlyDeclarations: false, ignoreDestructuring: true },
],
// Rules outside the categories.
"no-array-constructor": "error",
"no-case-declarations": "error",
"no-empty": "error",
"no-fallthrough": "error",
"no-inner-declarations": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-var": "error",
"prefer-const": "error",
"require-await": "error",
"require-yield": "error",
"typescript/adjacent-overload-signatures": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-empty-interface": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error",
// On Node, these come from an explicit `node:` import instead of the global scope.
"no-restricted-globals": [
"error",
@@ -209,45 +189,111 @@ const rules: DummyRuleMap = {
"setImmediate",
"clearImmediate",
],
// The explicit-types template's extras.
"import/no-cycle": "error",
// `../` is banned in every file; anything above a file needs a `#` alias.
"import/no-relative-parent-imports": "error",
"typescript/consistent-type-imports": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-non-null-assertion": "error",
// No disable comments at all.
"project/no-disable-comments": "error",
"project/no-asset-imports": "error",
"unicorn/no-abusive-eslint-disable": "error",
// Size and complexity limits match Deno's: this repo's `deno lint` ran the
// `recommended` set with no config file, so no size or complexity rule
// applied and none applies here. Each Oxlint limit rule, from a category or
// not, is off:
// - `complexity`: Deno had no `max-complexity`.
// - `max-lines`: Deno had no `max-file-lines`.
// - `max-params`: Deno had no `max-params`.
// - `id-length`: Deno had no `max-identifier-length`.
// - `no-nested-ternary`: Deno had no `max-nest-ternaries`.
// - `max-lines-per-function`, `max-classes-per-file`, `max-depth`,
// `max-nested-callbacks`, `max-statements`, `import/max-dependencies`,
// `unicorn/max-nested-calls`: Deno had none of these.
complexity: "off",
"max-lines": "off",
"max-params": "off",
"id-length": "off",
"no-nested-ternary": "off",
"max-lines-per-function": "off",
"max-classes-per-file": "off",
"max-depth": "off",
"max-nested-callbacks": "off",
"max-statements": "off",
"import/max-dependencies": "off",
"unicorn/max-nested-calls": "off",
// Pedantic, but Node's and the web's own types (`Response`, `Stats`,
// `RequestInit`) are mutable, so no parameter of theirs can pass; the
// explicit-types template and the app repos turn it off too.
// Single capital type parameters, which explicit-types requires, may be one letter.
"id-length": ["error", { exceptionPatterns: ["^[A-Z]$"] }],
"one-var": ["error", "never"],
"no-use-before-define": ["error", { functions: false }],
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"unicorn/import-style": ["error", { styles: { "node:path": { named: true } } }],
// Each limit is the smallest value the current code passes.
complexity: ["error", 26],
"max-depth": ["error", 6],
"max-lines": ["error", { max: 2048, skipBlankLines: false, skipComments: false }],
"max-lines-per-function": ["error", 96],
"max-params": ["error", 4],
"max-statements": ["error", 32],
"import/max-dependencies": ["error", { max: 13 }],
"unicorn/max-nested-calls": ["error", { max: 4 }],
// Collides with another rule, a plugin config, or Oxfmt.
// Collides with `#private`: members are public unless marked so.
"typescript/explicit-member-accessibility": "off",
// Collides with `typescript/prefer-readonly-parameter-types` being off.
"typescript/prefer-readonly": "off",
// Collides with Node's and the web's mutable types (`Response`, `Stats`).
"typescript/prefer-readonly-parameter-types": "off",
// Collides with explicit-types `no-utility-types`, which bans `Record`.
"typescript/consistent-indexed-object-style": "off",
// Collides with explicitly annotated declarations.
"typescript/no-inferrable-types": "off",
// Collides with import-structure `enforce-import-order`.
"sort-imports": "off",
// Collides with import-structure `enforce-mod-file` barrels.
"oxc/no-barrel-file": "off",
// Collides with exporting each declaration where it stands.
"import/group-exports": "off",
// Collides with exporting each declaration where it stands.
"import/exports-last": "off",
// Collides with `import/no-default-export`.
"import/prefer-default-export": "off",
// Collides with `unicorn/prefer-ternary`.
"no-ternary": "off",
// Collides with `require-await` and `promise-function-async`.
"oxc/no-async-await": "off",
// Collides with `typescript/prefer-optional-chain`.
"oxc/no-optional-chaining": "off",
// Collides with file mode checks such as `mode & 0o077`.
"no-bitwise": "off",
// Collides with explicit-types `no-optional-null`, which makes `undefined` the absent value.
"no-undefined": "off",
// Collides with `typescript/no-floating-promises`, which `void` satisfies.
"no-void": "off",
// Collides with JSON and Node APIs, which return `null`.
"unicorn/no-null": "off",
// Collides with `import/no-default-export`.
"import/no-named-export": "off",
// Collides with `typescript/consistent-type-imports`.
"import/consistent-type-specifier-style": "off",
// Collides with `require-await`, which fails an `async` wrapper with no `await`.
"typescript/promise-function-async": "off",
// Collides with explicit-types `no-undefined-type`: a `let` that `try`
// assigns would need `T | undefined`.
"init-declarations": "off",
// Collides with `no-await-in-loop`: sequential steps chain through `reduce`.
"unicorn/no-array-reduce": "off",
// Collides with literal Gitea Actions `${{ }}` expressions in workflow strings.
"no-template-curly-in-string": "off",
// Node code: this action runs on Node.
// Node built-ins are the action's runtime.
"import/no-nodejs-modules": "off",
// Command-line entry points set their exit status.
"unicorn/no-process-exit": "off",
// The action reports to the job log through the console.
"no-console": "off",
// PENDING USER: union members are values in profiles.json, the Gitea API,
// and posted comments; renaming them changes behavior.
"explicit-types/upper-snake-string-unions": "off",
// PENDING USER: a fitting `name` needs a constructor and changes logged
// error names.
"unicorn/custom-error-definition": "off",
// Too much churn.
// Literal numbers read better than a constant per use.
"no-magic-numbers": "off",
// Keys follow meaning, not the alphabet.
"sort-keys": "off",
// Comments may start with code or lowercase identifiers.
"capitalized-comments": "off",
// explicit-types `require-return-type` covers return types.
"typescript/explicit-function-return-type": "off",
// explicit-types `require-return-type` covers return types.
"typescript/explicit-module-boundary-types": "off",
// Empty callbacks are legitimate no-ops.
"no-empty-function": "off",
// Methods may not need `this`.
"class-methods-use-this": "off",
// Braces only where a case declares.
"unicorn/switch-case-braces": "off",
// Plain property reads are fine.
"prefer-destructuring": "off",
// Object rest and spread are idiomatic.
"oxc/no-rest-spread-properties": "off",
// Short numbers need no separators.
"unicorn/numeric-separators-style": "off",
// `continue` keeps loop bodies flat.
"no-continue": "off",
};
/**
+10 -8
View File
@@ -19,10 +19,10 @@ test("retries once after an invalid refresh token", async () => {
() =>
Promise.resolve({
code: 1,
error: attempts++ === 0 ? "ERROR: invalid_refresh_token" : "still failed",
error: (attempts += 1) === 1 ? "ERROR: invalid_refresh_token" : "still failed",
}),
() => {
relogins++;
relogins += 1;
return Promise.resolve();
},
);
@@ -39,14 +39,14 @@ test("does not retry an unrelated failure", async () => {
let relogins = 0;
await retryInvalidToken(
() => {
attempts++;
attempts += 1;
return Promise.resolve({
code: 1,
error: "rate limited",
});
},
() => {
relogins++;
relogins += 1;
return Promise.resolve();
},
);
@@ -87,11 +87,11 @@ test("re-logs in after the job 9837 revoked-session stderr", async () => {
const result = await retryInvalidToken(
() =>
Promise.resolve({
code: attempts++ > 0 ? 0 : 1,
code: (attempts += 1) > 1 ? 0 : 1,
error: attempts === 1 ? REVOKED_CODEX.join("\n") : "",
}),
() => {
relogins++;
relogins += 1;
return Promise.resolve();
},
);
@@ -144,7 +144,8 @@ test.skipIf(IS_WINDOWS)("stages and persists only a regular private Codex login"
const staged = await stageCodexAuth(canonical, home, currentUid(), currentGid());
expect(await readFile(staged, "utf8")).toStrictEqual(AUTH);
expect((await stat(staged)).mode & 0o777).toBe(0o600);
const stagedStat = await stat(staged);
expect(stagedStat.mode & 0o777).toBe(0o600);
const refreshed = JSON.stringify({
auth_mode: "chatgpt",
@@ -153,7 +154,8 @@ test.skipIf(IS_WINDOWS)("stages and persists only a regular private Codex login"
await writeFile(staged, refreshed);
await persistCodexAuth(staged, canonical);
expect(await readFile(canonical, "utf8")).toStrictEqual(refreshed);
expect((await stat(canonical)).mode & 0o777).toBe(0o600);
const canonicalStat = await stat(canonical);
expect(canonicalStat.mode & 0o777).toBe(0o600);
} finally {
await rm(root, { recursive: true });
}
+3 -1
View File
@@ -121,7 +121,9 @@ export async function stageCodexAuth(
await chmod(stagedPath, 0o600);
await chown(stagedPath, uid, gid);
} catch (error) {
if (!isNotFound(error)) throw error;
if (!isNotFound(error)) {
throw error;
}
}
return stagedPath;
}
+12 -4
View File
@@ -49,7 +49,9 @@ export async function collectOpenCodeOutput(
export function openCodeError(output: string): Error | null {
let failure: Error | null = null;
for (const line of output.split("\n")) {
if (line.trim() === "") continue;
if (line.trim() === "") {
continue;
}
let event: unknown;
try {
event = JSON.parse(line);
@@ -80,12 +82,18 @@ export function openCodeError(output: string): Error | null {
export function parseOpenCodeEvents(output: string): string {
let result: string | null = null;
for (const line of output.split("\n")) {
if (line.trim() === "") continue;
if (line.trim() === "") {
continue;
}
const event: unknown = JSON.parse(line);
const part = field(event, "part");
if (field(event, "type") !== "text" || !isObject(part)) continue;
if (field(event, "type") !== "text" || !isObject(part)) {
continue;
}
const text = field(part, "text");
if (typeof text === "string" && text.trim() !== "") result = text;
if (typeof text === "string" && text.trim() !== "") {
result = text;
}
}
if (result === null) {
throw openCodeError(output) ?? new Error("OpenCode produced no text output");
+3 -1
View File
@@ -8,7 +8,9 @@ export { type Fetcher, type PostContext, postRuntime } from "./post.ts";
function required(name: string): string {
const value = process.env[name];
if (value === undefined || value === "") throw new Error(`${name} is not set`);
if (value === undefined || value === "") {
throw new Error(`${name} is not set`);
}
return value;
}
+12 -4
View File
@@ -45,9 +45,15 @@ async function writeArtifacts(
review?: string,
anchors?: unknown,
): Promise<void> {
if (!outcome.ok) return;
if (review !== undefined) await writePrivate(outcome.reviewPath, review);
if (anchors !== undefined) await writePrivate(outcome.anchorsPath, JSON.stringify(anchors));
if (!outcome.ok) {
return;
}
if (review !== undefined) {
await writePrivate(outcome.reviewPath, review);
}
if (anchors !== undefined) {
await writePrivate(outcome.anchorsPath, JSON.stringify(anchors));
}
}
function manifestFor(runtimeDir: string, profileHome: string): RuntimeManifest {
@@ -106,7 +112,9 @@ const missing = async (path: string): Promise<boolean> => {
await stat(path);
return false;
} catch (error) {
if (isNotFound(error)) return true;
if (isNotFound(error)) {
return true;
}
throw error;
}
};
+23 -10
View File
@@ -22,12 +22,12 @@ interface Anchor {
const ANSI = new RegExp(`${String.fromCodePoint(27)}\\[[0-9;]*[A-Za-z]`, "gu");
const stripAnsi = (text: string): string => text.replace(ANSI, "");
const VERDICTS: Map<string, [event: string, mark: string]> = new Map([
const VERDICTS = new Map<string, [event: string, mark: string]>([
["Approved", ["APPROVED", "✅"]],
["Changes requested", ["REQUEST_CHANGES", "🛑"]],
]);
const PROFILE_TYPES: Set<unknown> = new Set(["claude", "codex", "opencode"]);
const FAILURE_REASONS: Set<unknown> = new Set(["sign-in failed", "usage exhausted", "failed"]);
const PROFILE_TYPES = new Set<unknown>(["claude", "codex", "opencode"]);
const FAILURE_REASONS = new Set<unknown>(["sign-in failed", "usage exhausted", "failed"]);
const isProfileType = (value: unknown): value is ProfileFailure["type"] => PROFILE_TYPES.has(value);
const isFailureReason = (value: unknown): value is FailureReason => FAILURE_REASONS.has(value);
const failureComment = (failures: ProfileFailure[]): string =>
@@ -40,7 +40,9 @@ class GiteaRequestError extends Error {}
function parseAnchors(text: string, sourcePath: string): Anchor[] {
const entries: unknown = JSON.parse(text);
if (!Array.isArray(entries)) throw new TypeError(`${sourcePath}: not an array`);
if (!Array.isArray(entries)) {
throw new TypeError(`${sourcePath}: not an array`);
}
return entries.map((entry: unknown, index) => {
const at = `${sourcePath}[${index}]`;
if (!isObject(entry)) {
@@ -116,12 +118,16 @@ async function readAgentArtifact(
name: string,
): Promise<string | null> {
const profile = manifest.profiles.find((entry) => join(entry.home, name) === path);
if (profile === undefined) throw new Error(`${name}: outside profile home`);
if (profile === undefined) {
throw new Error(`${name}: outside profile home`);
}
let stat: Stats;
try {
stat = await lstat(path);
} catch (error) {
if (isNotFound(error)) return null;
if (isNotFound(error)) {
return null;
}
throw error;
}
if (
@@ -143,7 +149,8 @@ export async function postRuntime(
): Promise<void> {
const manifest = await readRuntimeManifest(manifestPath);
try {
const reviewerToken = (await readFile(manifest.reviewerFile, "utf8")).trim();
const reviewerText = await readFile(manifest.reviewerFile, "utf8");
const reviewerToken = reviewerText.trim();
const request = async (path: string, body: unknown): Promise<void> => {
let response: Response;
try {
@@ -208,20 +215,26 @@ export async function postRuntime(
})),
});
await postReview(anchors).catch(async (error: unknown) => {
if (anchors.length === 0) throw error;
if (anchors.length === 0) {
throw error;
}
console.error(
`inline comments rejected: ${error instanceof Error ? error.message : String(error)}`,
);
await postReview([]);
});
} catch (error) {
if (error instanceof GiteaRequestError) throw error;
if (error instanceof GiteaRequestError) {
throw error;
}
await postComment(failureComment([{ ...outcome.profile, reason: "failed" }]));
throw error;
}
} finally {
await rm(manifest.runtimeDir, { recursive: true }).catch((error: unknown) => {
if (!isNotFound(error)) throw error;
if (!isNotFound(error)) {
throw error;
}
});
}
}
+3 -1
View File
@@ -5,7 +5,9 @@ import { prepareRuntime } from "#runtime";
const env = (name: string): string => {
const value = process.env[name];
if (value === undefined || value === "") throw new Error(`${name} is not set`);
if (value === undefined || value === "") {
throw new Error(`${name} is not set`);
}
return value;
};
+3 -1
View File
@@ -96,7 +96,9 @@ test.skipIf(!IS_ROOT)("cleanup leaves no runnable daemonized process", async ()
await killProfileProcesses(selected.uid);
const status = await readFile(`/proc/${pid}/status`, "utf8").catch((error: unknown) => {
if (isNotFound(error)) return null;
if (isNotFound(error)) {
return null;
}
throw error;
});
expect(status === null || !isRunnableProfileProcess(status, selected.uid)).toBe(true);
+8 -4
View File
@@ -6,8 +6,8 @@ import { isNotFound } from "#common";
import type { RuntimeProfile } from "#runtime";
export function isRunnableProfileProcess(status: string, uid: number): boolean {
const owner = /^Uid:\s+(\d+)/mu.exec(status)?.[1];
const state = /^State:\s+(\w)/mu.exec(status)?.[1];
const owner = /^Uid:\s+(?<uid>\d+)/mu.exec(status)?.[1];
const state = /^State:\s+(?<state>\w)/mu.exec(status)?.[1];
return owner !== undefined && Number(owner) === uid && state !== "Z";
}
@@ -33,12 +33,16 @@ export async function killProfileProcesses(uid: number, attempt = 0): Promise<vo
throw new Error(`profile UID ${uid} still owns processes after cleanup`);
}
const ids = await profileProcessIds(uid);
if (ids.length === 0) return;
if (ids.length === 0) {
return;
}
for (const pid of ids) {
try {
process.kill(pid, "SIGKILL");
} catch (error) {
if (!isNotFound(error)) throw error;
if (!isNotFound(error)) {
throw error;
}
}
}
await sleep(10);
+22 -19
View File
@@ -62,7 +62,9 @@ test("recognizes provider API exhaustion status", async () => {
{ type: "codex", model: "gpt-5.6-terra" },
],
(profile) => {
if (profile.type === "claude") throw new Error(message);
if (profile.type === "claude") {
throw new Error(message);
}
return Promise.resolve("fallback");
},
),
@@ -84,7 +86,9 @@ test("falls through after a sign-in failure and records each try", async () => {
if (profile.type === "codex") {
throw new AgentAuthError("codex sign-in failed");
}
if (profile.type === "claude") throw new Error("API Error: 429");
if (profile.type === "claude") {
throw new Error("API Error: 429");
}
return Promise.resolve("fallback");
},
failures,
@@ -116,7 +120,8 @@ test("throws the last sign-in failure when every profile fails", async () => {
),
).rejects.toThrow(
expect.toSatisfy(
(e: unknown) => e instanceof AgentAuthError && e.message.includes("claude sign-in failed"),
(error: unknown) =>
error instanceof AgentAuthError && error.message.includes("claude sign-in failed"),
),
);
expect(failures.map(({ reason }) => reason)).toStrictEqual(["sign-in failed", "sign-in failed"]);
@@ -157,22 +162,20 @@ test("does not mistake incidental quota text for exhaustion", async () => {
});
test("exposes only the selected profile credential to the agent", () => {
const base = new Map(
Object.entries({
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 base = new Map([
["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,
+16 -6
View File
@@ -38,12 +38,16 @@ function isUsageExhausted(error: unknown): boolean {
}
function failureReason(error: unknown): FailureReason {
if (error instanceof AgentAuthError) return "sign-in failed";
if (error instanceof AgentAuthError) {
return "sign-in failed";
}
return isUsageExhausted(error) ? "usage exhausted" : "failed";
}
export function failureFooter(failures: ProfileFailure[]): string {
if (failures.length === 0) return "> Profile: none";
if (failures.length === 0) {
return "> Profile: none";
}
return `> Tried: ${failures
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${reason})`)
.join(", ")}`;
@@ -66,7 +70,9 @@ async function attemptFrom<T, P extends ProfileIdentity>(
} catch (error) {
const reason = failureReason(error);
failures.push({ type: profile.type, model: profile.model, reason });
if (reason === "failed" || index === profiles.length - 1) throw error;
if (reason === "failed" || index === profiles.length - 1) {
throw error;
}
console.error(`${profile.type} ${reason}; trying next profile`);
return attemptFrom(profiles, attempt, failures, index + 1);
}
@@ -85,7 +91,7 @@ export function runProfiles<T, P extends ProfileIdentity>(
export function sanitizedEnvironment(
environment: ReadonlyMap<string, string>,
): Map<string, string> {
const result: Map<string, string> = new Map();
const result = new Map<string, string>();
for (const name of [
"PATH",
"HOME",
@@ -113,7 +119,9 @@ export function sanitizedEnvironment(
"NODE_EXTRA_CA_CERTS",
]) {
const value = environment.get(name);
if (value !== undefined) result.set(name, value);
if (value !== undefined) {
result.set(name, value);
}
}
return result;
}
@@ -125,7 +133,9 @@ export function agentEnvironment(
): Map<string, string> {
const result = sanitizedEnvironment(environment);
result.set("HOME", profile.home);
if (profile.type === "codex") result.set("CODEX_HOME", `${profile.home}/.codex`);
if (profile.type === "codex") {
result.set("CODEX_HOME", `${profile.home}/.codex`);
}
if (token !== undefined && token !== "") {
const tokenName = {
claude: "CLAUDE_CODE_OAUTH_TOKEN",
+23 -13
View File
@@ -58,12 +58,14 @@ interface RenderedPrompt {
const env = (name: string): string => {
const value = process.env[name];
if (value === undefined) throw new Error(`${name} is not set`);
if (value === undefined) {
throw new Error(`${name} is not set`);
}
return value;
};
function definedEnvironment(): Map<string, string> {
const result: Map<string, string> = new Map();
const result = new Map<string, string>();
for (const [name, value] of Object.entries(process.env)) {
if (value !== undefined) {
result.set(name, value);
@@ -107,11 +109,14 @@ async function runRoot(command: string, args: string[]): Promise<void> {
const code = await exitCode(
spawn(command, args, { env: Object.fromEntries(PROCESS_ENV), stdio: "ignore" }),
);
if (code !== 0) throw new Error(`${command} ${args[0]} exited with ${code}`);
if (code !== 0) {
throw new Error(`${command} ${args[0]} exited with ${code}`);
}
}
async function configureGitAuthor(): Promise<void> {
const user: unknown = await (await gitea("user")).json();
const userResponse = await gitea("user");
const user: unknown = await userResponse.json();
const login = field(user, "login");
const email = field(user, "email");
if (typeof login !== "string" || typeof email !== "string") {
@@ -140,7 +145,9 @@ async function runAgentCommand(
stdio: "ignore",
});
const code = await exitCode(child);
if (code !== 0) throw new Error(`${command} ${args[0]} exited with ${code}`);
if (code !== 0) {
throw new Error(`${command} ${args[0]} exited with ${code}`);
}
}
async function prepareProfile(profile: RuntimeProfile): Promise<void> {
@@ -172,16 +179,15 @@ function formatComment(comment: unknown): string {
async function renderPrompt(profile: RuntimeProfile): Promise<RenderedPrompt> {
const reviewPath = await makeProfileFile(profile, "review.md");
const anchorsPath = await makeProfileFile(profile, "anchors.json");
const comments: unknown = await (
await gitea(`repos/${REPO}/issues/${INDEX}/comments?limit=100`)
).json();
const commentsResponse = await gitea(`repos/${REPO}/issues/${INDEX}/comments?limit=100`);
const comments: unknown = await commentsResponse.json();
if (!Array.isArray(comments)) {
throw new TypeError("comments: unexpected Gitea response");
}
const values: Map<string, string> = new Map([
const values = new Map<string, string>([
["COMMENT", env("COMMENT")],
["ISSUE_COMMENTS", comments.map((comment: unknown) => formatComment(comment)).join("\n")],
["CODE_RULES", await (await gitea(RULES_PATH)).text()],
["CODE_RULES", await gitea(RULES_PATH).then((response) => response.text())],
["EVENT_NAME", EVENT],
["GITEA_API_URL", API],
["GITEA_REPOSITORY", REPO],
@@ -192,7 +198,7 @@ async function renderPrompt(profile: RuntimeProfile): Promise<RenderedPrompt> {
const template = await readFile(new URL("../../prompt.md", import.meta.url), "utf8");
return {
prompt: template.replaceAll(
/\$\{(\w+)\}/gu,
/\$\{(?<name>\w+)\}/gu,
(match: string, name: string) => values.get(name) ?? match,
),
reviewPath,
@@ -367,7 +373,9 @@ async function runCodex(
gid: profile.gid,
stdio: "ignore",
});
if ((await exitCode(check)) !== 0) await codexDeviceLogin(profile, environment);
if ((await exitCode(check)) !== 0) {
await codexDeviceLogin(profile, environment);
}
}
const outputPath = await makeProfileFile(profile, "result.md");
const attempt = async (): Promise<CommandResult> => {
@@ -478,7 +486,9 @@ async function runProfile(profile: RuntimeProfile): Promise<AttemptResult> {
await persistCodexAuth(stagedCodexAuth, CODEX_AUTH);
}
}
if (resultPath !== null) body = await readProfileFile(profile, resultPath);
if (resultPath !== null) {
body = await readProfileFile(profile, resultPath);
}
if (body === null || reviewPath === null || anchorsPath === null) {
throw new Error("agent attempt produced no result");
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { decryptCredential, encryptCredential, parseCredentials } from "./creden
const MASTER_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8";
const OTHER_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
const NONCE = Uint8Array.from({ length: 12 }, (_, index) => index + 32);
const NONCE = Uint8Array.from({ length: 12 }, (_value, index) => index + 32);
const ENCRYPTED = "v1:ICEiIyQlJicoKSor:oV_FAgnsN3p1FyegJxwGVdyjfyPio6qDeSUwbA";
test("decrypts a known AES-256-GCM credential", async () => {
+6 -2
View File
@@ -47,7 +47,9 @@ export async function decryptCredential(encrypted: string, masterKey: string): P
throw new Error("credential must use v1:<nonce>:<ciphertext>");
}
const nonce = decodeBase64Url(encodedNonce);
if (nonce.length !== 12) throw new Error("credential nonce must be 12 bytes");
if (nonce.length !== 12) {
throw new Error("credential nonce must be 12 bytes");
}
const plaintext = await webcrypto.subtle.decrypt(
{ name: "AES-GCM", iv: buffer(nonce) },
await importMasterKey(masterKey, "decrypt"),
@@ -61,7 +63,9 @@ export async function encryptCredential(
masterKey: string,
nonce: Uint8Array = webcrypto.getRandomValues(new Uint8Array(12)),
): Promise<string> {
if (nonce.length !== 12) throw new Error("AES-GCM nonce must be 12 bytes");
if (nonce.length !== 12) {
throw new Error("AES-GCM nonce must be 12 bytes");
}
const ciphertext = await webcrypto.subtle.encrypt(
{ name: "AES-GCM", iv: buffer(nonce) },
await importMasterKey(masterKey, "encrypt"),
+8 -7
View File
@@ -49,7 +49,8 @@ test.skipIf(!IS_ROOT)("prepares root-only reviewer and isolated profile credenti
assert(claude.tokenFile !== null);
expect(await readFile(claude.tokenFile, "utf8")).toBe("secret-token");
await expectPrivate(claude.tokenFile, claude.uid, 0o600);
expect((await stat(codex.home)).uid).toStrictEqual(codex.uid);
const codexHome = await stat(codex.home);
expect(codexHome.uid).toStrictEqual(codex.uid);
expect(codex.tokenFile).toBe(null);
const ownToken = spawnSync("/bin/cat", [claude.tokenFile], {
@@ -83,12 +84,12 @@ test.skipIf(!IS_ROOT)("removes prepared credentials when output publication fail
process.execPath,
[fileURLToPath(new URL("../prepare/index.ts", import.meta.url))],
{
env: {
ACTION_PATH: action,
RUNNER_TEMP: root,
BOT_AGENTS_MASTER_KEY: MASTER_KEY,
GITHUB_OUTPUT: action,
},
env: Object.fromEntries([
["ACTION_PATH", action],
["RUNNER_TEMP", root],
["BOT_AGENTS_MASTER_KEY", MASTER_KEY],
["GITHUB_OUTPUT", action],
]),
},
);
+4 -2
View File
@@ -35,9 +35,11 @@ export interface CommandResult {
// This process's environment as a map from variable name to value.
export function inheritedEnvironment(): Map<string, string> {
const result: Map<string, string> = new Map();
const result = new Map<string, string>();
for (const [name, value] of Object.entries(process.env)) {
if (value !== undefined) result.set(name, value);
if (value !== undefined) {
result.set(name, value);
}
}
return result;
}
+3 -2
View File
@@ -82,7 +82,8 @@ test("the source protection script stages immutable action files", async () => {
const result = await runCommand("/bin/bash", ["scripts/protect-source.sh"], environment);
assert(result.success, result.stderr);
const path = (await readFile(outputFile, "utf8")).trim().slice(5);
const output = await readFile(outputFile, "utf8");
const path = output.trim().slice(5);
const files = [
"src/prepare/index.ts",
"src/run/index.ts",
@@ -106,7 +107,7 @@ test("the source protection script stages immutable action files", async () => {
test("module-relative runtime assets exist", async () => {
const module = new URL("../src/run/index.ts", import.meta.url);
const source = await readFile(module, "utf8");
const assets = [...source.matchAll(/new URL\("([^"]+)", import\.meta\.url\)/gu)].map(
const assets = [...source.matchAll(/new URL\("(?<asset>[^"]+)", import\.meta\.url\)/gu)].map(
([, asset = ""]) => new URL(asset, module),
);
const stats = await Promise.all(assets.map((asset) => stat(asset)));
+17 -1
View File
@@ -12,13 +12,29 @@ export default defineConfig({
"eslint-plugin-import-structure",
"./scripts/project-lint.ts",
],
categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" },
categories: {
correctness: "error",
suspicious: "error",
pedantic: "error",
perf: "error",
style: "error",
restriction: "error",
nursery: "off",
},
rules: {
...explicitTypes.configs.concrete.rules,
...explicitTypes.configs.naming.rules,
...explicitTypes.configs.absence.rules,
...importStructure.configs.modules.rules,
...projectLint.configs.project.rules,
},
overrides: [
{
// Vite+ and Oxlint load these through their default export.
files: ["vite.config.ts", "scripts/project-lint.ts"],
rules: { "import/no-default-export": "off" },
},
],
options: {
typeAware: true,
typeCheck: true,