Skip to content

Audit fixes, wave 2: sandboxed validation, batched Git snapshots, UI consistency pass - #14

Merged
christopherjnelson merged 5 commits into
mainfrom
claude/optimistic-shannon-4dh7gs
Sep 28, 2026
Merged

christopherjnelson merged 5 commits into
mainfrom
claude/optimistic-shannon-4dh7gs

Conversation

@christopherjnelson

@christopherjnelson christopherjnelson commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Second batch of audit fixes.

Validation commands run inside bubblewrap (2af8f82, test fixes 259c623, 107da21)

The Worker ran confined, but Foreman then ran validation commands (pnpm install, tests) directly on the host against Worker-authored files. With the default scope, which includes package.json, a Worker could add a preinstall script or a test and run code as the operator before any human review.

What is sandboxed. Every validation command now runs under bwrap by default:

  • The host root is read-only, and all capabilities are dropped with --cap-drop ALL, so root can't unmount the masks.
  • These are hidden behind tmpfs: /home, /root, /tmp, /run, /mnt, /media, /srv, the Foreman data dir and the source checkout.
  • Toolchains that live under a hidden root, such as nvm or ~/.local, are re-exposed read-only. A re-expose can never reveal a hidden root, the repo or the data dir.
  • A persistent, owner-only cache keeps installs fast.

Failure and opt-out.

  • If bubblewrap is missing or unusable, validation fails closed with a clear message.
  • FOREMAN_VALIDATION_SANDBOX=none is an explicit, loudly logged opt-out for hosts without bubblewrap.
  • FOREMAN_VALIDATION_SANDBOX_RO_PATHS covers unusual toolchain layouts.

Reporting. Observations record the sandbox mode, and /api/status reports whether the sandbox is available.

Default scope. CI configuration (.github/ and similar) is no longer in the suggested Worker scope, because it runs with repository secrets once pushed.

Known residual. The network namespace is still shared, because installs need it. This is documented and will be followed up separately.

Test fixes. The follow-up commits fix two host-layout assumptions in the new sandbox tests that CI's runner exposed: systemd-resolved's resolv.conf under /run, and a home directory under the masked /home.

Batched Git snapshots and promotion (89e33ca)

  • Snapshots. snapshotGitCommit used to run git cat-file twice per file. It now runs ls-tree -l plus one cat-file --batch process, with size limits enforced before any content is read.
  • Promotion. It reuses base blob IDs for unchanged bytes and hashes only changed content.
  • Measured on a 5,050-file repo: snapshot went from 52.7 s to 1.2 s, and promotion from 223 s to 4.3 s. Result commit SHAs are identical, which a golden test pins.

UI consistency and layout pass (8c93753)

  • One badge system. A single Badge component and one status-to-tone mapping replace about ten ad-hoc pill, badge and dot variants. Every badge has a marker plus text, and contrast is at least 8:1.
  • Stylesheet. Rewritten around tokens, 86 KB → 64 KB. Text is now at least 11px, and faint text has at least 5.2:1 contrast.
  • Review panel. When a decision is pending it moves to the top, so Approve and Reject are visible without scrolling from 1024 to 1920 wide.
  • Project tree. It fits 7 tasks at 1440×900, and the usage dock collapses to one line.
  • 1024 layout. Header and inspector overlap and clipping are fixed.
  • Checks. Smoke-tested against the real backend in Chromium: no page, console or HTTP errors, and live events connect through the new origin guard.

Verification

  • pnpm typecheck and pnpm build: pass.
  • pnpm test: 448/448.
  • pnpm test:bridge: 54/54.
  • Sandbox tests were verified as root and as an unprivileged uid with a home under /home, which mirrors the CI runner.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP

The Worker ran confined, but Foreman then executed validation commands
(pnpm install, tests) directly on the host against Worker-authored
files. With the default scope (every top-level path, including
package.json) a Worker could add a preinstall script or a test and run
code as the operator, reading ~/.ssh, gh credentials and other repos,
before any human review.

Every validation command now runs under bwrap by default and fails
closed if bubblewrap is unusable. The host root is read-only; home
directories, /tmp, /run, the Foreman data dir and the source checkout
are hidden behind tmpfs; capabilities are dropped; toolchains under a
hidden root are re-exposed read-only; a persistent owner-only cache
keeps installs fast. Network access is still shared (documented).

FOREMAN_VALIDATION_SANDBOX=none is an explicit, loudly logged opt-out
for hosts without bubblewrap. Observations record the sandbox mode, and
/api/status reports whether the sandbox is available.

CI configuration (.github/ and similar) is no longer in the suggested
Worker scope, since it runs with repository secrets once pushed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
snapshotGitCommit spawned `git cat-file` twice per file (~7 ms/file,
about 53 s for a 5,000-file repo) and ran several times per run;
promotion also ran `hash-object` once for every file in the repo and
wrote the whole tree into a worktree it never read.

Snapshots now use ls-tree -l (enforcing size limits before any content
is read) plus a single `cat-file --batch` process with a streaming,
strictly validated parser. Promotion reuses the base commit's blob ids
for unchanged bytes and hashes only changed contents with one
`hash-object --stdin-paths --no-filters`; the manifest is still fully
validated and the result tree still independently re-verified.

On a 5,050-file repo: snapshot 52.7 s -> 1.2 s, promotion 223 s ->
4.3 s, with identical result commit SHAs before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
On hosts where /etc/resolv.conf links into /run/systemd/resolve (such as
the Ubuntu CI runners), the sandbox deliberately re-exposes that one file,
so /run is not empty. Assert instead that the only thing visible under
/run and /var/run is the host's resolv.conf target and its parent
directories, cover the systemd-resolved layout explicitly, and tighten
the pure argv test to exactly one re-exposed bind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
On CI the home directory is /home/runner. The sandbox masks /home as a
whole, so when nothing is re-exposed /home/runner does not exist inside
it and `find` failed. A root that does not exist in the sandbox has
nothing visible under it; errors from bwrap or find still fail, and the
positive case still fails on an empty result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
Replace about ten ad-hoc pill, badge, dot and chip variants with one
Badge component: one size, a marker plus text for every tone, six tones
with at least 8:1 contrast, and a single status-to-tone mapping so the
same status never shows in two colours.

Rewrite the stylesheet around tokens (type, spacing, radii, controls,
focus ring): 86 KB to 64 KB, text at least 11px, faint text at least
5.2:1. When a decision is pending, the review panel moves to the top so
Approve and Reject are visible without scrolling at 1024 to 1920 wide.
The project tree keeps status and run count on one row and the usage
dock collapses to one line, so 7 tasks fit at 1440x900. The header and
inspector no longer overlap or clip at 1024.

awaiting_approval now shows the warning tone when validation has not
passed or the Reviewer did not recommend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXCvrGWmmRNDLmy7nYS2QP
@christopherjnelson christopherjnelson changed the title Audit fixes, wave 2: sandboxed validation and batched Git snapshots Audit fixes, wave 2: sandboxed validation, batched Git snapshots, UI consistency pass Sep 28, 2026
@christopherjnelson
christopherjnelson merged commit 00e2eec into main Sep 28, 2026
1 check passed
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