From 219e2c55b2ce3b96d477b060fce7141446a24d61 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 00:26:39 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(local-ci):=20#110=20=E2=80=94=20per-leg?= =?UTF-8?q?=20TMPDIR=20so=20concurrent=20legs=20cannot=20fail=20each=20oth?= =?UTF-8?q?er's=20temp-root=20hygiene=20scans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four concurrent legs shared the system /tmp, so cli.test.ts's hygiene assertion (snapshot agent-bundle-artifact-* under os.tmpdir(), fail on new ones) could see a sibling leg's legitimate in-flight artifact-inspection directory and fail. Each leg now gets a private TMPDIR under the run's scratch root (.worktrees/local-ci/tmp/), recreated every run, so every leg's temp traffic — and the test's scan — is naturally scoped. The assertion keeps its strictness: a directory leaked by the leg's own process tree still lands in the leg's temp root and still fails its scan. Co-authored-by: Zack Jackson --- docs/local-ci.md | 13 ++++++++++--- scripts/local-ci.mjs | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/local-ci.md b/docs/local-ci.md index 7bf1fea6b..6786bc277 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -20,9 +20,16 @@ examples/release/micro-eval gates, so it is a fast signal, not a merge gate. ## What it runs Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted -changes are not covered — the runner warns), with its own `node_modules`. -Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs -for warm caches, and can be recreated with `--fresh`. +changes are not covered — the runner warns), with its own `node_modules` and +its own `TMPDIR` (`.worktrees/local-ci/tmp/`, recreated every run). +The private temp root keeps concurrent legs from observing each other's +temp traffic: suites that assert temp-root hygiene (for example +`cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*` +directories) only ever see their own leg's directories, so a sibling leg's +in-flight work cannot fail them — while a directory the leg itself leaks +still fails its own scan. Legs live under `.worktrees/local-ci/` +(gitignored), are reused across runs for warm caches, and can be recreated +with `--fresh`. | Local leg | Node | Steps | Mirrors hosted job | | --- | --- | --- | --- | diff --git a/scripts/local-ci.mjs b/scripts/local-ci.mjs index 04f264ca6..90003a563 100644 --- a/scripts/local-ci.mjs +++ b/scripts/local-ci.mjs @@ -24,7 +24,12 @@ * be shared across Node ABIs. The shared pnpm store is content-addressed * (and side-effects caches are keyed by engine), so concurrent per-leg * installs stay cheap. Legs live under .worktrees/local-ci/ (gitignored) and - * are reused across runs for warm caches; `--fresh` recreates them. + * are reused across runs for warm caches; `--fresh` recreates them. Each leg + * also gets a private TMPDIR (.worktrees/local-ci/tmp/, recreated every + * run): concurrent legs would otherwise share /tmp, and suites that assert + * temp-root hygiene (cli.test.ts scans os.tmpdir() for leaked + * agent-bundle-artifact-* directories) would see a sibling leg's in-flight + * temp traffic and fail on it (#110). */ import { spawn } from 'node:child_process'; import { execFile as executeFile } from 'node:child_process'; @@ -380,11 +385,20 @@ const main = async () => { const directory = join(legsRoot, plan.name); await ensureLegWorktree(directory, sha); const syntheticBinDirectory = await createSyntheticBinDirectory(plan, pnpmEntrypoint); + // Private per-leg temp root (see the isolation model above). Recreating + // it keeps every run's hygiene scans free of a crashed prior run's + // leftovers, while a leak WITHIN a run still fails its own leg's scan. + const temporaryDirectory = join(legsRoot, 'tmp', plan.name); + await rm(temporaryDirectory, { recursive: true, force: true }); + await mkdir(temporaryDirectory, { recursive: true }); legs.push({ ...plan, directory, syntheticBinDirectory, - environment: buildLegEnvironment(syntheticBinDirectory, plan.environmentOverrides), + environment: buildLegEnvironment(syntheticBinDirectory, { + ...plan.environmentOverrides, + TMPDIR: temporaryDirectory, + }), }); } From b1261a35d0cb3c854432c10ad55a2fe02d390f12 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 00:44:25 +0000 Subject: [PATCH 2/2] fix: relocate per-leg TMPDIR to short system-temp path (AF_UNIX 108-byte limit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome creates AF_UNIX sockets inside TMPDIR; the repo-nested .worktrees/local-ci/tmp/ path overflowed the kernel's 108-byte sun_path limit (126 chars measured) and crashed every browser integration test at launch on all three verify legs. Keep the per-leg private TMPDIR (#110 isolation unchanged) but place it at os.tmpdir()/abci--, where is SHA-256 of the repo root — stable per repo so reruns reuse (and rm -rf + mkdir reset) it, and concurrent runs from different checkouts cannot collide. Longest leg path is 32 chars, leaving ~35 bytes of socket-name headroom. --- docs/local-ci.md | 10 +++++++--- scripts/local-ci.mjs | 25 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/local-ci.md b/docs/local-ci.md index 6786bc277..6ce8a8da9 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -21,9 +21,13 @@ examples/release/micro-eval gates, so it is a fast signal, not a merge gate. Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted changes are not covered — the runner warns), with its own `node_modules` and -its own `TMPDIR` (`.worktrees/local-ci/tmp/`, recreated every run). -The private temp root keeps concurrent legs from observing each other's -temp traffic: suites that assert temp-root hygiene (for example +its own `TMPDIR` (`/abci--`, where `` is +derived from the repo root path; recreated every run). The temp roots live +under the short system temp directory rather than the repo worktree because +Chrome creates AF_UNIX sockets inside `TMPDIR` and the kernel caps socket +paths at 108 bytes; the hash keeps concurrent runs from different checkouts +from colliding. The private temp root keeps concurrent legs from observing +each other's temp traffic: suites that assert temp-root hygiene (for example `cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*` directories) only ever see their own leg's directories, so a sibling leg's in-flight work cannot fail them — while a directory the leg itself leaks diff --git a/scripts/local-ci.mjs b/scripts/local-ci.mjs index 90003a563..25264e7a0 100644 --- a/scripts/local-ci.mjs +++ b/scripts/local-ci.mjs @@ -25,17 +25,22 @@ * (and side-effects caches are keyed by engine), so concurrent per-leg * installs stay cheap. Legs live under .worktrees/local-ci/ (gitignored) and * are reused across runs for warm caches; `--fresh` recreates them. Each leg - * also gets a private TMPDIR (.worktrees/local-ci/tmp/, recreated every - * run): concurrent legs would otherwise share /tmp, and suites that assert - * temp-root hygiene (cli.test.ts scans os.tmpdir() for leaked - * agent-bundle-artifact-* directories) would see a sibling leg's in-flight - * temp traffic and fail on it (#110). + * also gets a private TMPDIR (os.tmpdir()/abci--, where + * is derived from the repo root path; recreated every run): concurrent legs + * would otherwise share /tmp, and suites that assert temp-root hygiene + * (cli.test.ts scans os.tmpdir() for leaked agent-bundle-artifact-* + * directories) would see a sibling leg's in-flight temp traffic and fail on + * it (#110). The temp roots deliberately live under the SYSTEM temp + * directory, not the repo worktree: Chrome creates AF_UNIX sockets inside + * TMPDIR, and the kernel caps socket paths at 108 bytes — a repo-nested + * TMPDIR overflows that and crashes every browser test at launch. */ +import { createHash } from 'node:crypto'; import { spawn } from 'node:child_process'; import { execFile as executeFile } from 'node:child_process'; import { existsSync } from 'node:fs'; import { chmod, mkdir, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; -import { availableParallelism, homedir } from 'node:os'; +import { availableParallelism, homedir, tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; @@ -388,7 +393,13 @@ const main = async () => { // Private per-leg temp root (see the isolation model above). Recreating // it keeps every run's hygiene scans free of a crashed prior run's // leftovers, while a leak WITHIN a run still fails its own leg's scan. - const temporaryDirectory = join(legsRoot, 'tmp', plan.name); + // It must be a SHORT path under the system temp root (never under the + // repo worktree): Chrome creates AF_UNIX sockets in TMPDIR and the + // kernel's sun_path limit is 108 bytes. The hash keys the directory to + // this repo root, so concurrent runs from different checkouts cannot + // collide while reruns from the same checkout reuse (and reset) it. + const repositoryHash = createHash('sha256').update(repositoryRoot).digest('hex').slice(0, 8); + const temporaryDirectory = join(tmpdir(), `abci-${repositoryHash}-${plan.name}`); await rm(temporaryDirectory, { recursive: true, force: true }); await mkdir(temporaryDirectory, { recursive: true }); legs.push({