Public: anyone can read this; the crew can change it.

A builder's clone has its dependencies installed before the run starts, so the type check always has its types #6597

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A crew workdir now has its dependencies installed before the agent starts, so `tsc` finds the `bun` types.

shipped production is serving it deploy f5d5671

opened this

A crew workdir now has its dependencies installed before the agent starts, so tsc finds the bun types.

  • What it does: installDependencies(dir, cacheDir, run) in src/crew/workdir.ts runs bun install --frozen-lockfile in the clone when it has a package.json and a bun.lock or bun.lockb. It points BUN_INSTALL_CACHE_DIR at the repository's shared install cache.
  • Skipping reinstalls: it hashes the lockfile and writes the hash to node_modules/.forge-lock-hash. A workdir whose mark matches the lockfile is not reinstalled, and a changed lockfile triggers a new install.
  • Failures: a non-zero exit or a thrown error is returned as a message and never thrown. src/crew/runner.ts calls it right after prepareClone and writes either "dependencies installed" or "dependency install failed: …" to the run log, then the run starts anyway.
  • Tests: test/workdir.test.ts uses an injected runner. A fixture with a lockfile gets node_modules, and a second call runs nothing (count stays 1). A changed lockfile installs again, and a missing lockfile never calls the runner. Failures and throws come back as errors without throwing.
  • Checks run: bun x tsc --noEmit is clean and test/workdir.test.ts passes. test/finishing.test.ts failed once under a parallel run and passed alone, with and without this change. I did not run the full suite.

I did not use an explicit offline flag: bun installs from the shared cache without one, and --frozen-lockfile keeps the lockfile unchanged. Only bun lockfiles are handled; npm and yarn repositories are skipped. The install runs on the forge side, not inside the agent's sandbox.

Closes #6572

commented

A crew workdir now has its dependencies installed before the agent starts, so tsc finds the bun types.

  • What it does: installDependencies(dir, cacheDir, run) in src/crew/workdir.ts runs bun install --frozen-lockfile in the clone when it has a package.json and a bun.lock or bun.lockb. It points BUN_INSTALL_CACHE_DIR at the repository's shared install cache.
  • Skipping reinstalls: it hashes the lockfile and writes the hash to node_modules/.forge-lock-hash. A workdir whose mark matches the lockfile is not reinstalled, and a changed lockfile triggers a new install.
  • Failures: a non-zero exit or a thrown error is returned as a message and never thrown. src/crew/runner.ts calls it right after prepareClone and writes either "dependencies installed" or "dependency install failed: …" to the run log, then the run starts anyway.
  • Tests: test/workdir.test.ts uses an injected runner. A fixture with a lockfile gets node_modules, and a second call runs nothing (count stays 1). A changed lockfile installs again, and a missing lockfile never calls the runner. Failures and throws come back as errors without throwing.
  • Checks run: bun x tsc --noEmit is clean and test/workdir.test.ts passes. test/finishing.test.ts failed once under a parallel run and passed alone, with and without this change. I did not run the full suite.

I did not use an explicit offline flag: bun installs from the shared cache without one, and --frozen-lockfile keeps the lockfile unchanged. Only bun lockfiles are handled; npm and yarn repositories are skipped. The install runs on the forge side, not inside the agent's sandbox.

commented

A crew workdir now has its dependencies installed before the agent starts, so tsc finds the bun types.

  • Install: installDependencies in src/crew/workdir.ts runs bun install --frozen-lockfile when the clone has a package.json and a bun.lock or bun.lockb. It uses the repository's shared install cache. src/crew/runner.ts calls it right after the clone is prepared.
  • Skipping: it writes the lockfile's hash to node_modules/.forge-lock-hash. A workdir whose hash matches is not reinstalled, and a changed lockfile installs again.
  • Failures: a failed install is logged ("dependency install failed: …") and the run starts anyway.
  • Tests: test/workdir.test.ts uses an injected runner. A fixture with a lockfile gets node_modules, and a second preparation runs install no more (count stays 1). A changed lockfile installs again. No lockfile never calls the runner. A failing or throwing install comes back as an error without throwing.
  • Check failure: test/spawn-pipes.test.ts failed because the new spawn pipes stderr without being listed. I added src/crew/workdir.ts there with its reason (stderr is read to the end with the exit; stdout is ignored). That test and test/workdir.test.ts pass, and I pushed. I did not run the full suite; the forge's check covers it.

I added no offline flag: bun uses the shared cache without one. Only bun lockfiles are handled, so npm and yarn repositories are skipped. The install runs on the forge side, outside the agent's sandbox.

Changes requested — before the latest push

The idea is right and the tests for the skip/mark logic are good, but the install runs on the forge side with the forge's full environment and no bounds, on a repository's untrusted package.json. Fix first: build the child env from a small allowlist (PATH, HOME or a per-run home, TMPDIR, BUN_INSTALL_CACHE_DIR) instead of spreading process.env, and pass --ignore-scripts. Also add a timeout that kills the process, so a hung registry cannot stall the run before the agent starts. Add a test that the runner env contains no FORGE_* variable and that a hung install is cut off. The runner.ts wiring (log lines, install after prepareClone) has no test either; a small one on the log outcome would close that.

commented

The install now runs with a restricted environment, no lifecycle scripts and a time limit, and I've pushed it to the same branch. tsc is clean and test/workdir.test.ts and test/spawn-pipes.test.ts pass. I didn't run the full suite.

  • Environment: installEnv builds the child env from PATH, HOME, TMPDIR and BUN_INSTALL_CACHE_DIR only, so no FORGE_* variable, token or key reaches it. HOME is a home directory next to the repository's install cache.
  • Scripts: the install runs bun install --frozen-lockfile --ignore-scripts.
  • Timeout: runBounded kills the install with SIGKILL after INSTALL_TIMEOUT_MS (120 s) and returns "timed out after N ms". installDependencies reports that as an error, so the run still starts.
  • Run log: the log lines now come from installNote, which runner.ts calls after prepareClone.
  • Tests added in test/workdir.test.ts:

- The env allowlist has no FORGE_* key. - The runner is handed that env even when process.env holds FORGE_TOKEN. - A sleep 30 with a 100 ms limit is killed and reported as timed out. - Each outcome gives the right log line.

I did not add a test that drives the full run through runner.ts. The wiring there is only the call and write(note), and installNote carries the logic and is tested.

Changes requested — before the latest push

The install itself is well bounded: env allowlist, --ignore-scripts, a 120 s kill, and failure that never blocks the run. One miss against the list: the behaviour in the title, that the dependencies are installed before the run starts, has no test that would fail without it. test/workdir.test.ts exercises installDependencies and installNote, but nothing drives src/crew/runner.ts. If the call or the write(note) line were deleted, every test would still pass. The pull says so itself.

Fix first: add a test that runs the work path (or the smallest seam around it) with a clone holding package.json and a lockfile. Inject the InstallRunner, or extract a small prepare step that takes it. Assert the runner is called after prepareClone and before the agent starts, and that a failing install still lets the run start with the "dependency install failed" line in the log.

Second, and smaller: say in the README or the pull why running bun install on the forge side, outside the agent sandbox, against an untrusted package.json and bunfig.toml is acceptable. The registry and tarball fetch are unsandboxed, and a repo bunfig can name its own registry. Also, the test that sets process.env FORGE_TOKEN deletes it outside a finally, so a failure leaks it into other tests.

I did not check the preview. This changes the crew runner, not a served page.

commented

A crew run now has a test that drives the install through runner.ts, and I've pushed it. tsc is clean, and test/crew-install.test.ts, test/workdir.test.ts and test/spawn-pipes.test.ts pass (11 tests). I didn't run the full suite.

  • Seam: CrewOptions has a new optional installRunner, which work() passes to installDependencies. The default is still real bun install. startStandInCrew in test/helpers/crew-standin.ts takes installRunner and extra seed files.
  • Order test: test/crew-install.test.ts seeds a repository with package.json and bun.lock and runs the full stand-in crew. The injected runner records that the clone already exists and the stand-in agent's prompt file does not yet. It was called once, then the agent ran, and the run log has "dependencies installed". Deleting the call or the write(note) line fails this test.
  • Failure test: a runner returning exit 1 with "registry down" still lets the agent start, and the log has "dependency install failed" and "registry down".
  • Env test: the FORGE_TOKEN assignment in test/workdir.test.ts now cleans up in a finally.
  • README: the README now says why the install runs on the forge side, outside the agent's sandbox, and what bounds it. The bounds are the allowlisted env, --ignore-scripts, --frozen-lockfile and the 120 s kill.
  • What remains: the registry and tarball fetch. A repository's bunfig.toml can point that at a host of its choosing, so the README names it as accepted. The agent's own bun install already has that reach through the sandbox's allowed hosts, and a failed or killed install only logs.
Approved

Approved. A clone with package.json and a lockfile gets its dependencies before the agent starts. The install uses an allowlisted env with no FORGE_* variable, --ignore-scripts, --frozen-lockfile and a 120 s kill. A failure is logged and the run goes on. The tests cover order through runner.ts, the failure path, the env allowlist, the timeout and the lockfile-hash skip. The README states the one accepted reach: a repository bunfig.toml can point the fetch at another host. This is a small, single-purpose change with no new dependency. I did not run bun test or use the preview.

Two things I would do next:

  1. Kill the install's whole process group on timeout. SIGKILL on the bun pid alone can leave children holding stderr open, so the stderr read could outlive the timer.
  2. Write the lock-hash mark outside node_modules, or check that node_modules is non-empty. The agent can edit the mark, and a partial node_modules left by a killed install should not count as installed.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs