From 19a568b9ed7129170497d2dab121e70c0365d646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20=F0=9F=94=B6=20Tarbert?= <66887028+NathanTarbert@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:52:19 -0400 Subject: [PATCH 1/3] fix: typecheck the two deployables, and hold every lockfile to what is committed (#112) Root typecheck is bun run --filter '*' typecheck, and workspaces is app, server and worker, so agent-computer and supervisor ship a typecheck script nothing ever runs. agent-computer holds the only spawn in the deployment and supervisor is the only thing holding a Docker socket. Both are clean today, so this wires up a check rather than clearing a backlog. A deployables matrix job runs each package's own script after its own frozen install, and it is in verify's needs: branch protection requires verify alone, so a job outside it blocks nothing. A third deployable is one matrix entry and no change to verify. Neither package joins workspaces, because Dockerfile and tests/workspace.test.ts both depend on them staying separate. Six of the seven committed lockfiles were installed without --frozen-lockfile. Adding the flag alone would not have fixed it: bun install --frozen-lockfile exits 0 when no lockfile is present at all, and every one of these Dockerfiles copied only package.json, so the flag would have produced five checks that can never fail. Each now copies its bun.lock as well, which is what makes the flag mean anything. Verified by injection: a bad type in each package fails with TS2322, and a dependency the lockfile cannot satisfy fails with 'lockfile had changes, but lockfile is frozen'. The examples lockfiles are installed by nothing, so there is no site to add a flag to, and agent-bot and agent-langgraph have no typecheck script to run. --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++++++++++++- Dockerfile | 7 ++++++- agent-bot/Dockerfile | 6 ++++-- agent-computer/Dockerfile | 6 ++++-- agent-langgraph/Dockerfile | 6 ++++-- supervisor/Dockerfile | 7 +++++-- 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efc75863..ae94cdca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,39 @@ jobs: - run: bun run lint - run: bun run typecheck + # The two packages that are deployables in their own right rather than root workspaces. Root + # `typecheck` is `bun run --filter '*' typecheck`, and `--filter '*'` enumerates `workspaces`, which + # is app, server and worker. So both of these ship a `typecheck` script that nothing has ever run: + # `agent-computer` holds the only `spawn` in the deployment and `supervisor` is the only thing + # holding a Docker socket, which is a poor pair to leave untyped. Both are clean today, so this + # changes nothing about main and only stops the next change being the first one a type checker sees. + # + # Not by adding them to `workspaces`: each is built from its own lockfile and the Dockerfile depends + # on that, so they are installed separately here for the same reason they are installed separately + # there. A matrix rather than two jobs, so each package reports its own result and a third one is a + # line in the list below and nothing in `verify`, which sees the whole matrix as one entry. + deployables: + name: types (${{ matrix.package }}) + runs-on: ubuntu-latest + strategy: + # One red package must not hide whether the other is red too. + fail-fast: false + matrix: + package: [agent-computer, supervisor] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 + # No root install: each package carries its own typescript, and its tsconfig reaches + # tsconfig.base.json through the checkout rather than through node_modules. + - run: bun install --frozen-lockfile + working-directory: ${{ matrix.package }} + - run: bun run typecheck + working-directory: ${{ matrix.package }} + test: name: tests runs-on: ubuntu-latest @@ -220,7 +253,7 @@ jobs: name: verify runs-on: ubuntu-latest if: always() - needs: [static, test, build, migrations, image] + needs: [static, deployables, test, build, migrations, image] steps: - name: Require every check env: diff --git a/Dockerfile b/Dockerfile index 473ba1a6..105ff461 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,13 @@ COPY server/package.json server/package.json COPY worker/package.json worker/package.json RUN bun install --frozen-lockfile +# The lockfile travels with the manifest, because `--frozen-lockfile` with no lockfile in the context +# is not an error: bun resolves afresh, succeeds, and the flag has decorated nothing. With both files +# here, the tree in the image is the tree this repository resolved and committed. Bun is already +# pinned twenty-odd lines above for the same reason; this is the install below it. COPY agent-computer/package.json agent-computer/package.json -RUN cd agent-computer && bun install +COPY agent-computer/bun.lock agent-computer/bun.lock +RUN cd agent-computer && bun install --frozen-lockfile # A second tree with the build-time dependencies left out, for the runtime stage to take. Vite, # biome and the test tooling are a gigabyte that nothing in a running container imports. diff --git a/agent-bot/Dockerfile b/agent-bot/Dockerfile index 6251f01a..249f57d9 100644 --- a/agent-bot/Dockerfile +++ b/agent-bot/Dockerfile @@ -5,8 +5,10 @@ FROM oven/bun:1.3-alpine WORKDIR /app RUN chown bun:bun /app USER bun -COPY --chown=bun:bun agent-bot/package.json ./agent-bot/ -RUN cd agent-bot && bun install +# The lockfile as well as the manifest: `--frozen-lockfile` with no lockfile present resolves afresh +# and exits 0, so the file has to be here for the flag to mean anything. +COPY --chown=bun:bun agent-bot/package.json agent-bot/bun.lock ./agent-bot/ +RUN cd agent-bot && bun install --frozen-lockfile # Preserve the repo layout so `../../shared/bot-prompt` resolves the same in the image and locally. COPY --chown=bun:bun shared ./shared diff --git a/agent-computer/Dockerfile b/agent-computer/Dockerfile index 594db89e..fd15ec09 100644 --- a/agent-computer/Dockerfile +++ b/agent-computer/Dockerfile @@ -11,8 +11,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends unzip \ ENV PATH="/root/.bun/bin:${PATH}" WORKDIR /app -COPY agent-computer/package.json ./ -RUN bun install +# The lockfile as well as the manifest: `--frozen-lockfile` with no lockfile present resolves afresh +# and exits 0, so the file has to be here for the flag to mean anything. +COPY agent-computer/package.json agent-computer/bun.lock ./ +RUN bun install --frozen-lockfile COPY agent-computer/src ./src diff --git a/agent-langgraph/Dockerfile b/agent-langgraph/Dockerfile index a8223fa8..614d766e 100644 --- a/agent-langgraph/Dockerfile +++ b/agent-langgraph/Dockerfile @@ -5,8 +5,10 @@ FROM oven/bun:1.3-alpine WORKDIR /app -COPY agent-langgraph/package.json ./agent-langgraph/ -RUN cd agent-langgraph && bun install +# The lockfile as well as the manifest: `--frozen-lockfile` with no lockfile present resolves afresh +# and exits 0, so the file has to be here for the flag to mean anything. +COPY agent-langgraph/package.json agent-langgraph/bun.lock ./agent-langgraph/ +RUN cd agent-langgraph && bun install --frozen-lockfile # The repo's own layout, kept exactly, so `../../shared/bot-prompt` resolves the same here as it # does on a laptop. Flattening src to /app/src would change the depth and break only on deploy. diff --git a/supervisor/Dockerfile b/supervisor/Dockerfile index e45689f0..5a682967 100644 --- a/supervisor/Dockerfile +++ b/supervisor/Dockerfile @@ -10,8 +10,11 @@ FROM ghcr.io/spiffe/spire-server:1.15.1 AS spire FROM oven/bun:1.3.14-alpine WORKDIR /app -COPY supervisor/package.json ./ -RUN bun install +# The lockfile as well as the manifest: `--frozen-lockfile` with no lockfile present resolves afresh +# and exits 0. This is the process whose compromise costs the host, so what it runs is the tree that +# was reviewed, not whatever the registry had this morning. +COPY supervisor/package.json supervisor/bun.lock ./ +RUN bun install --frozen-lockfile COPY --from=spire /opt/spire/bin/spire-server /usr/local/bin/spire-server From 6d2b37686389b839b446e95bbfcddeb392937095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20=F0=9F=94=B6=20Tarbert?= <66887028+NathanTarbert@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:00:12 -0400 Subject: [PATCH 2/3] fix: install at the root before typechecking a deployable (#112) The new job failed on a fresh checkout with TS2688, cannot find type definition file for 'bun'. Both packages set types: ["bun"] in their tsconfig and @types/bun is a root devDependency rather than one of theirs, so tsc finds it by walking up to the root node_modules. A working copy and the image both have that already, which is why the scripts pass locally: Dockerfile installs at the root before it installs agent-computer. Reproduced by moving the root node_modules away, which fails the same way, and by restoring it, which passes. Then checked in the job's real order against a clean tree with every node_modules removed: root install, package install, typecheck, both packages green. --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae94cdca..c8671f95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,8 +62,13 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.14 - # No root install: each package carries its own typescript, and its tsconfig reaches - # tsconfig.base.json through the checkout rather than through node_modules. + # The root install comes first, and it is not redundant. Both packages set + # `types: ["bun"]` in their tsconfig, and `@types/bun` is a root devDependency rather than + # one of theirs, so tsc resolves it by walking up to the root node_modules. Without this step + # the typecheck fails with TS2688 on a fresh checkout, which is what a first attempt at this + # job did. It is also what the image already does: Dockerfile installs at the root before + # installing agent-computer, and a developer's working copy has both for the same reason. + - run: bun install --frozen-lockfile - run: bun install --frozen-lockfile working-directory: ${{ matrix.package }} - run: bun run typecheck From e27446a8651019693a62e7db23d1b96c4341b698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20=F0=9F=94=B6=20Tarbert?= <66887028+NathanTarbert@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:17:02 -0400 Subject: [PATCH 3/3] docs: state why the deployables job installs at the root The comment described how the requirement was found rather than what it is. State the requirement: @types/bun is a root devDependency and both packages declare types: ["bun"], so tsc resolves it from the root node_modules. --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8671f95..c1ce54b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,12 +62,11 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.14 - # The root install comes first, and it is not redundant. Both packages set - # `types: ["bun"]` in their tsconfig, and `@types/bun` is a root devDependency rather than - # one of theirs, so tsc resolves it by walking up to the root node_modules. Without this step - # the typecheck fails with TS2688 on a fresh checkout, which is what a first attempt at this - # job did. It is also what the image already does: Dockerfile installs at the root before - # installing agent-computer, and a developer's working copy has both for the same reason. + # Two installs, and the root one is load-bearing. Both packages set `types: ["bun"]` in + # their tsconfig while `@types/bun` is a root devDependency, so tsc resolves it by walking up + # to the root node_modules. Without it the typecheck fails with TS2688 on a fresh checkout. + # This is the order the image uses for the same reason: Dockerfile installs at the root + # before installing agent-computer. - run: bun install --frozen-lockfile - run: bun install --frozen-lockfile working-directory: ${{ matrix.package }}