Skip to content

Commit 8bf9618

Browse files
fix(local-ci): per-leg TMPDIR so concurrent legs cannot fail each other's temp-root hygiene scans (#112)
* fix(local-ci): #110 — per-leg TMPDIR so concurrent legs cannot fail each other's temp-root hygiene scans 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/<leg>), 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 <ScriptedAlchemy@users.noreply.github.com> * fix: relocate per-leg TMPDIR to short system-temp path (AF_UNIX 108-byte limit) Chrome creates AF_UNIX sockets inside TMPDIR; the repo-nested .worktrees/local-ci/tmp/<leg> 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-<hash8>-<leg>, where <hash8> 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. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
1 parent 9f61396 commit 8bf9618

2 files changed

Lines changed: 42 additions & 6 deletions

File tree

‎docs/local-ci.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,20 @@ examples/release/micro-eval gates, so it is a fast signal, not a merge gate.
2020
## What it runs
2121

2222
Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted
23-
changes are not covered — the runner warns), with its own `node_modules`.
24-
Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs
25-
for warm caches, and can be recreated with `--fresh`.
23+
changes are not covered — the runner warns), with its own `node_modules` and
24+
its own `TMPDIR` (`<system tmp>/abci-<hash8>-<leg>`, where `<hash8>` is
25+
derived from the repo root path; recreated every run). The temp roots live
26+
under the short system temp directory rather than the repo worktree because
27+
Chrome creates AF_UNIX sockets inside `TMPDIR` and the kernel caps socket
28+
paths at 108 bytes; the hash keeps concurrent runs from different checkouts
29+
from colliding. The private temp root keeps concurrent legs from observing
30+
each other's temp traffic: suites that assert temp-root hygiene (for example
31+
`cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*`
32+
directories) only ever see their own leg's directories, so a sibling leg's
33+
in-flight work cannot fail them — while a directory the leg itself leaks
34+
still fails its own scan. Legs live under `.worktrees/local-ci/`
35+
(gitignored), are reused across runs for warm caches, and can be recreated
36+
with `--fresh`.
2637

2738
| Local leg | Node | Steps | Mirrors hosted job |
2839
| --- | --- | --- | --- |

‎scripts/local-ci.mjs‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,23 @@
2424
* be shared across Node ABIs. The shared pnpm store is content-addressed
2525
* (and side-effects caches are keyed by engine), so concurrent per-leg
2626
* installs stay cheap. Legs live under .worktrees/local-ci/ (gitignored) and
27-
* are reused across runs for warm caches; `--fresh` recreates them.
27+
* are reused across runs for warm caches; `--fresh` recreates them. Each leg
28+
* also gets a private TMPDIR (os.tmpdir()/abci-<hash8>-<leg>, where <hash8>
29+
* is derived from the repo root path; recreated every run): concurrent legs
30+
* would otherwise share /tmp, and suites that assert temp-root hygiene
31+
* (cli.test.ts scans os.tmpdir() for leaked agent-bundle-artifact-*
32+
* directories) would see a sibling leg's in-flight temp traffic and fail on
33+
* it (#110). The temp roots deliberately live under the SYSTEM temp
34+
* directory, not the repo worktree: Chrome creates AF_UNIX sockets inside
35+
* TMPDIR, and the kernel caps socket paths at 108 bytes — a repo-nested
36+
* TMPDIR overflows that and crashes every browser test at launch.
2837
*/
38+
import { createHash } from 'node:crypto';
2939
import { spawn } from 'node:child_process';
3040
import { execFile as executeFile } from 'node:child_process';
3141
import { existsSync } from 'node:fs';
3242
import { chmod, mkdir, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
33-
import { availableParallelism, homedir } from 'node:os';
43+
import { availableParallelism, homedir, tmpdir } from 'node:os';
3444
import { dirname, join, resolve } from 'node:path';
3545
import { fileURLToPath } from 'node:url';
3646
import { promisify } from 'node:util';
@@ -380,11 +390,26 @@ const main = async () => {
380390
const directory = join(legsRoot, plan.name);
381391
await ensureLegWorktree(directory, sha);
382392
const syntheticBinDirectory = await createSyntheticBinDirectory(plan, pnpmEntrypoint);
393+
// Private per-leg temp root (see the isolation model above). Recreating
394+
// it keeps every run's hygiene scans free of a crashed prior run's
395+
// leftovers, while a leak WITHIN a run still fails its own leg's scan.
396+
// It must be a SHORT path under the system temp root (never under the
397+
// repo worktree): Chrome creates AF_UNIX sockets in TMPDIR and the
398+
// kernel's sun_path limit is 108 bytes. The hash keys the directory to
399+
// this repo root, so concurrent runs from different checkouts cannot
400+
// collide while reruns from the same checkout reuse (and reset) it.
401+
const repositoryHash = createHash('sha256').update(repositoryRoot).digest('hex').slice(0, 8);
402+
const temporaryDirectory = join(tmpdir(), `abci-${repositoryHash}-${plan.name}`);
403+
await rm(temporaryDirectory, { recursive: true, force: true });
404+
await mkdir(temporaryDirectory, { recursive: true });
383405
legs.push({
384406
...plan,
385407
directory,
386408
syntheticBinDirectory,
387-
environment: buildLegEnvironment(syntheticBinDirectory, plan.environmentOverrides),
409+
environment: buildLegEnvironment(syntheticBinDirectory, {
410+
...plan.environmentOverrides,
411+
TMPDIR: temporaryDirectory,
412+
}),
388413
});
389414
}
390415

0 commit comments

Comments
 (0)