Skip to content

perf: prebuild local sandbox images - #201

Merged
joshblack merged 6 commits into
mainfrom
feat/prebuild-sandbox-image
Sep 4, 2026
Merged

perf: prebuild local sandbox images#201
joshblack merged 6 commits into
mainfrom
feat/prebuild-sandbox-image

Conversation

@joshblack

@joshblack joshblack commented Sep 3, 2026

Copy link
Copy Markdown
Member

Follow up to #212.

This PR prebuilds a reusable local sandbox image through Dockerode before running trials. The configured Debian-based Node image remains the base layer, so compatible custom images with npm, apt-get, and a node user continue to work without publishing the derived sandbox image to a registry.

This is the third entry in stack 213. #199 adds the design system benchmark scenarios, #203 aligns the benchmark and experiment runner workflows, and #204 adds durable plan sharding.

Changelog

New

  • Add a local multi-stage sandbox image build containing npm, Copilot CLI, Chromium, and the shared sandbox directories.
  • Add deterministic local image tags and reuse in-process builds for matching base images.
  • Add process-level tracking for active sandbox containers.

Changed

  • Treat --docker-image as a Debian-based Node base image with npm, apt-get, and a node user for the generated sandbox layer.
  • Create each trial container from the prebuilt local image instead of repeating sandbox setup.
  • Keep agent-browser and its skill installation isolated to the walkthrough phase.
  • Remove active sandbox containers before exiting on SIGINT or SIGTERM.

Removed

  • Remove per-container image pulls and repeated npm, Copilot, and Chromium installation.

@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch 7 times, most recently from 8d2141b to 13dba90 Compare September 4, 2026 02:17
@joshblack
joshblack changed the base branch from feat/add-benchmark-support to feat/split-trial-output September 4, 2026 02:18
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch 2 times, most recently from 250fd17 to 3f8e748 Compare September 4, 2026 02:34
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 3f8e748 to c1f9f45 Compare September 4, 2026 02:44
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from c1f9f45 to 50b872b Compare September 4, 2026 02:48
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 50b872b to e2f6934 Compare September 4, 2026 02:49
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from e2f6934 to c5a7117 Compare September 4, 2026 03:32
@joshblack
joshblack marked this pull request as ready for review September 4, 2026 03:36
Copilot AI lite review requested due to automatic review settings September 4, 2026 03:36
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from c5a7117 to cdd5913 Compare September 4, 2026 03:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It changes core sandbox container/image lifecycle behavior (Docker build + signal cleanup), which is operationally sensitive and is best validated with human review and real-run testing.

Review tier: Lite
Findings: None

What changed in this PR

This PR improves evaluation runtime performance by prebuilding and reusing a local “sandbox layer” Docker image (via Dockerode) that contains the common sandbox dependencies (npm, Copilot CLI, Chromium, and shared directories). It also adds process-level tracking and cleanup of active sandbox containers on termination signals.

Changes:

  • Build a deterministic, reusable local sandbox image layered on top of --docker-image (now treated as the base image) and reuse the build within the process.
  • Remove per-container bootstrapping (package installs / directory setup) and create trial containers from the prebuilt local image.
  • Track active sandbox containers and force-remove them on SIGINT/SIGTERM, plus update CLI/workflow docs and add a changeset.
File Description
script/​run-benchmark.sh Passes --docker-image (base image) into benchmark runs, with a default.
packages/​agent-eval/​src/​trial.ts Removes per-walkthrough Chromium installation from trial execution.
packages/​agent-eval/​src/​sandbox/​system.ts Adds Dockerfile-based local image build, deterministic tagging, build reuse, and signal-driven container cleanup.
packages/​agent-eval/​src/​sandbox/​system.test.ts Adds coverage for image build and termination cleanup behavior; updates lifecycle mocks.
packages/​agent-eval/​src/​cli.ts Clarifies --docker-image semantics as a base layer rather than the final runtime image.
.github/​workflows/​experiment.yml Updates workflow input documentation to match new docker-image semantics.
.github/​workflows/​benchmark.yml Updates workflow input documentation to match new docker-image semantics.
.changeset/​puny-houses-arrive.md Publishes a patch note about cleaning up active containers on termination.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI review requested due to automatic review settings September 4, 2026 03:48
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from cdd5913 to b66e9ae Compare September 4, 2026 03:48
Copilot AI review requested due to automatic review settings September 4, 2026 04:08
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 6f8be9f to 27d8583 Compare September 4, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The updated --docker-image documentation in the CLI and workflows understates the requirement that the base image must include Node/npm, which can lead to misconfiguration and failed sandbox builds.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 3 Low severity

New issues introduced by this change (3)
Severity Finding
Low severity .github/​workflows/​benchmark.yml — This workflow input description drops the requirement that the base image includes Node/npm, but…
Low severity .github/​workflows/​experiment.yml — This workflow input description should continue to require a Debian-based Node image (with npm)…
Low severity packages/​agent-eval/​src/​cli.ts — The --docker-image flag is now described as only requiring a Debian base with apt-get and a…
Issues resolved since last review (3)
Severity Finding
Low severity .changeset/​puny-houses-arrive.md — This changeset only mentions SIGINT/SIGTERM container cleanup, but the PR also changes how… View resolved comment
Medium severity packages/​agent-eval/​src/​sandbox/​system.ts — removeContainer treats any container.remove failure as fatal. With AutoRemove enabled, a container… View resolved comment
Medium severity packages/​agent-eval/​src/​sandbox/​system.ts — getDockerImageName only hashes the base image string + npm/copilot versions. If the Dockerfile… View resolved comment
Suppressed comments (1)

packages/agent-eval/src/cli.ts:92

  • The usage text for --docker-image no longer mentions that the base must include Node/npm. Since the sandbox image build runs npm inside the base stage, passing a non-Node Debian image will fail; the help line should reflect the Node requirement to avoid misconfiguration.
  -b, --benchmark <file>     The file name of the benchmark to run
      --benchmarks <dir>     The directory containing local benchmark files (default: ./benchmarks)
  -c, --concurrency <num>    The number of treatments to run in parallel
      --docker-image <image> The Docker base image to layer the treatment environment on (default: node:26.5.0-slim)
  -e, --experiment <file>    The file name of the experiment to run

Comment thread .github/workflows/benchmark.yml
Comment thread .github/workflows/experiment.yml
Comment thread packages/agent-eval/src/cli.ts
Copilot AI review requested due to automatic review settings September 4, 2026 04:28
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 27d8583 to 215fcd2 Compare September 4, 2026 04:28
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 215fcd2 to 040ba6f Compare September 4, 2026 04:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The SIGINT/SIGTERM cleanup relies on process.once, so a second signal during cleanup can terminate the process without handlers and leave active sandbox containers behind.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity packages/​agent-eval/​src/​sandbox/​system.tstrackContainer registers SIGINT/SIGTERM handlers with process.once, which removes the listener…
Issues resolved since last review (3)
Severity Finding
Low severity packages/​agent-eval/​src/​cli.ts — The --docker-image flag is now described as only requiring a Debian base with apt-get and a… View resolved comment
Low severity .github/​workflows/​experiment.yml — This workflow input description should continue to require a Debian-based Node image (with npm)… View resolved comment
Low severity .github/​workflows/​benchmark.yml — This workflow input description drops the requirement that the base image includes Node/npm, but… View resolved comment

Comment on lines +479 to +480
process.once('SIGINT', terminationHandlers.SIGINT)
process.once('SIGTERM', terminationHandlers.SIGTERM)
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 040ba6f to cf55880 Compare September 4, 2026 04:34
Copilot AI review requested due to automatic review settings September 4, 2026 04:38
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from cf55880 to 6fda76f Compare September 4, 2026 04:38
Base automatically changed from feat/split-trial-output to main September 4, 2026 04:41
joshblack and others added 6 commits September 3, 2026 23:41
Build a reusable sandbox image through Dockerode and layer it on the configured base image before running trials.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6d12a97a-b8d3-45ad-a09a-3c76d1411705
Move Chromium installation into the reusable base layer while keeping agent-browser and its skill isolated to the walkthrough phase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6d12a97a-b8d3-45ad-a09a-3c76d1411705
Track active sandbox containers and remove them before exiting on SIGINT or SIGTERM while keeping normal disposal idempotent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6d12a97a-b8d3-45ad-a09a-3c76d1411705
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6d12a97a-b8d3-45ad-a09a-3c76d1411705
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joshblack
joshblack force-pushed the feat/prebuild-sandbox-image branch from 6fda76f to e70a9cb Compare September 4, 2026 04:41
@joshblack
joshblack merged commit 0e49dbf into main Sep 4, 2026
8 checks passed
@joshblack
joshblack deleted the feat/prebuild-sandbox-image branch September 4, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Trial file read/write currently performs unbounded concurrent filesystem operations, which can fail or become unstable for large runs due to open-file limits and I/O contention.

Review tier: Lite
Findings: 1 Medium severity

Pre-existing issues (1)
Severity Finding
Medium severity packages/​agent-eval/​src/​sandbox/​system.tstrackContainer registers SIGINT/SIGTERM handlers with process.once, which removes the listener… View comment
Suppressed comments (2)

packages/agent-eval/src/trial.ts:187

  • writeTrialFiles writes all trial JSON files using an unbounded Promise.all over the full trials map. For larger runs this can open many files concurrently and hit OS FD limits (EMFILE) or create significant I/O contention. Consider batching/limiting concurrency when writing trial files.

This issue also appears on line 200 of the same file.

  const entries = await Promise.all(
    [...trials].map(async ([trialId, trial]) => {
      if (trial.id !== trialId) {
        throw new Error(`Trial map key "${trialId}" does not match trial id "${trial.id}"`)
      }

      const artifactDirectory = path.isAbsolute(trial.artifacts.directory)
        ? trial.artifacts.directory
        : path.resolve(outputDirectory, trial.artifacts.directory)
      const trialFilePath = await preparePathWithinDirectory(
        host,
        outputDirectory,
        path.relative(outputDirectory, path.join(artifactDirectory, `${trialId}.json`)),
        `Trial file for "${trialId}"`,
      )
      await host.fs.writeFile(trialFilePath, JSON.stringify(trial), 'utf-8')
      const reference = path.relative(outputDirectory, trialFilePath).split(path.sep).join(path.posix.sep)
      return [trialId, reference] as const
    }),
  )

packages/agent-eval/src/trial.ts:222

  • readTrialFiles reads all referenced trial files using an unbounded Promise.all across Object.entries(references). For runs with many trials this can trigger EMFILE/open-file limits or overwhelm the FS. Consider batching/limiting concurrency when loading the trial files.
  const entries = await Promise.all(
    Object.entries(references).map(async ([trialId, reference]) => {
      const trialFilePath = await resolveExistingPathWithinDirectory(
        host,
        outputDirectory,
        reference,
        `Trial reference for "${trialId}"`,
      )
      const contents = await host.fs.readFile(trialFilePath, 'utf-8')
      const trial = parse(JSON.parse(contents))
      if (
        typeof trial !== 'object' ||
        trial === null ||
        !('id' in trial) ||
        typeof trial.id !== 'string' ||
        trial.id !== trialId
      ) {
        throw new Error(`Trial file "${reference}" does not contain trial id "${trialId}"`)
      }

      return [trialId, trial] as const
    }),
  )

Copilot AI review requested due to automatic review settings September 4, 2026 04:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new sandbox image build path no longer ensures the base image tag is refreshed, which can cause local runs to drift onto stale base layers unless the build explicitly pulls updated bases.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 2 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity packages/​agent-eval/​src/​sandbox/​system.ts — The sandbox image build no longer forces a refresh of the configured base image. Previously, the…
Pre-existing issues (1)
Severity Finding
Medium severity packages/​agent-eval/​src/​sandbox/​system.tstrackContainer registers SIGINT/SIGTERM handlers with process.once, which removes the listener… View comment

Comment on lines +412 to +415
dockerfile: 'Dockerfile',
t: dockerImage,
target: 'sandbox',
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants