mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #66] [MERGED] feat(groom): bwrap sandbox harness + key-broker with deterministic CI proofs (BE-4302) #78
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#78
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/66
Author: @mattmillerai
Created: 7/24/2026
Status: ✅ Merged
Merged: 8/4/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-4302-groom-sandbox📝 Commits (6)
469df38feat(groom): bwrap sandbox harness + key-broker with deterministic CI proofs (BE-4302)b59cd67fix(groom): make sandbox-tests cleanup SC2015-clean (explicit if over A && B || C)8ca79f3fix(groom): read /proc/self/environ via cat, not < redirect (empty in jail)3ed4386fix(groom): harden sandbox path validation + broker stream resilience (BE-4302)7942e58fix(groom): close residual sandbox+broker review gaps (BE-4302)42340b3feat(groom): isolated netns sandbox — drop --share-net, broker over a bind-mounted unix socket (BE-4421) (#73)📊 Changes
7 files changed (+916 additions, -0 deletions)
View changed files
📝
.github/groom/README.md(+95 -0)➕
.github/groom/agent-sandbox.sh(+240 -0)➕
.github/groom/broker.mjs(+165 -0)➕
.github/groom/jail-shim.mjs(+44 -0)➕
.github/groom/tests/fake-upstream.mjs(+48 -0)➕
.github/groom/tests/sandbox-tests.sh(+301 -0)📝
.github/workflows/test-groom-scripts.yml(+23 -0)📄 Description
ELI-5
The groom auto-builder lets an AI agent write code changes. We don't fully trust that agent, so before phase 2 wires it up, this PR builds the cage it will run in and a safe mail slot for its one allowed outside call:
agent-sandbox.sh): the agent gets a shell where it can only see a read-only copy of the system, a scratch tmpfs, its own repo checkout, and one folder it's allowed to write to. It cannot see the runner's other files, secrets, or even the other running processes. If the cage can't be built, we refuse to run the agent at all — we never quietly let it out.broker.mjs): the agent has no API key. It talks to a tiny proxy on localhost that holds the real key, swaps it in, and forwards only to Anthropic. The key lives on the host; the agent can spend it but never read it.Plus a CI job that proves all of this with plain
bashcommands — noclaude, no API key, no spend. Nothing about groom's actual agent steps changes here, so it can merge on its own.What's in this PR
.github/groom/agent-sandbox.shbwrapwrapper — jails an arbitrary command; loud fail-closed preflight.github/groom/broker.mjs.github/groom/tests/sandbox-tests.sh.github/groom/tests/fake-upstream.mjsapi.anthropic.comin the broker test.github/workflows/test-groom-scripts.ymlsandbox-testsjob (ubuntu-latest).github/groom/README.mdThe wrapper matches the ticket's interface and bwrap invocation exactly (
--unshare-all --share-net --die-with-parent --new-session --clearenv, the base mounts,HOME/PATH/TERM+ each--env, the clone at its real path ro or rw-git-ro, ro-files, the rw out-dir,--chdir <clone>). The broker holds the key host-side, deletes inboundx-api-key/authorization, forwards only/v1/*, answers/healthzlocally, 404s everything else, streams responses unbuffered, and logs method + path + status only.Acceptance criteria
sandbox-testsjob onubuntu-latest— asserts env scrub (FOO/HOME/PATH present, a hostHOSTSECRETcanary absent), FS confinement (host$HOME/$RUNNER_TEMP/$GITHUB_WORKSPACEcanaries unreadable; writes fail except the out-dir; host/tmp/canaryshadowed by tmpfs), both clone modes (rowrite fails;rw-git-roworktree write succeeds and is visible on the host while.git/configwrite fails), pid isolation (a hostsleep's pid absent from/proc), and the full broker contract (real key forwarded +dummystripped,/healthz200, non-/v1404, SSE streams intact).shellcheckclean onagent-sandbox.sh(verified locally; also gated in the new job, alongsidesandbox-tests.sh).groom.ymlagent steps —groom.ymlis untouched.Judgment calls (please read)
1. The
sandbox-testsjob lives intest-groom-scripts.yml, notci-groom.yml. The ticket said extendci-groom.yml, butci-groom.ymlis the scheduled groom caller that deliberately never fires on a PR (its header + AGENTS.md: "runs weekly on a schedule … never on-PR") and spends API. A deterministic, no-spend job placed there could never be seen green on this PR — which is the acceptance criterion.test-groom-scripts.ymlis the repo's PR-gating, path-filtered (.github/groom/**) home for groom's deterministic script tests, so the job runs on this very change. The job is namedsandbox-testsexactly as specified. This felt clearly right, but flagging it since it deviates from the literal wording.2. Verification status — proven both locally and in CI. I can't run unprivileged-userns
bwrapon the dev host (macOS), so I validated the full driver end-to-end in a privilegedubuntu:24.04container (all five property groups + the broker proxy pass, shellcheck clean) and thesandbox-testsCI job is now green onubuntu-latest— everyPASS:line plusALL SANDBOX TESTS PASSED. The broker layer was additionally smoke-tested standalone on the host (key injection +dummystrip,/healthz200, non-/v1404, SSE pass-through).3. Fail-closed refusal is intentional, not a capability dead-end. The preflight ends in
::error::bwrap sandbox unavailable … refusing to run the agent unsandboxed+ non-zero exit. That refusal fires only after the preflight has exhausted every path to establish the sandbox (install bubblewrap, install the AppArmor unconfined profile mirroring the runner image's podman workaround, dropkernel.apparmor_restrict_unprivileged_userns, and retest). The ticket references the runner image's own userns support, and thesandbox-testsjob empirically confirms the sandbox comes up onubuntu-latest. This is a deliberate security-control fail-closed, not a denial of a reachable capability.4.
NODE_TLS_REJECT_UNAUTHORIZED=0is test-only. It's set on the test broker process's env so it accepts the fake upstream's self-signed cert —broker.mjsitself has no TLS bypass; production talks real TLS toapi.anthropic.com. Thesk-ant-TESTFAKE-…key passed via--envin the broker test is a throwaway literal used to prove forwarding; the real-key-never-in-jail property is a production concern the test doesn't (and shouldn't) touch.5. Preflight matches the ticket verbatim (
sudo apt-get install -y bubblewrapwith noapt-get update). bubblewrap is present on the ubuntu-latest image and the fast-path skips the install when the sandbox already works, so this is a no-op in practice; kept literal to match the spec.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.