Repository navigation
Conversation
ananos
force-pushed
the
ci/e2e-report
branch
3 times, most recently
from
September 26, 2026 01:27
8ac2da4 to
a4498b7
Compare
ananos
force-pushed
the
ci/e2e-report
branch
8 times, most recently
from
September 30, 2026 09:44
a1809e1 to
1d2a1be
Compare
6 tasks done
ananos
force-pushed
the
ci/e2e-report
branch
4 times, most recently
from
September 30, 2026 11:32
9b22e46 to
fb7567b
Compare
This was referenced Sep 30, 2026
ananos
force-pushed
the
ci/e2e-report
branch
6 times, most recently
from
October 5, 2026 00:38
f310890 to
ac4817a
Compare
ananos
force-pushed
the
ci/e2e-report
branch
2 times, most recently
from
October 6, 2026 18:13
c3e1124 to
fccb487
Compare
1 of 6 tasks
ananos
added a commit
that referenced
this pull request
Oct 6, 2026
v0.1.0-rc15 carries hull-assets 0.1.9: the same 6.18.55 guest kernel as rc14, with Landlock built in. It keeps rc14's signed record of the kernel and initrd, and its urunc. The e2e runs of #360 installed rc15 through install.sh on a hosted Ubuntu runner, and every check passed. The brig version does not change with the bump, since install_runtime replaces the brig the bundle carries. Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The v0.3.0 release gate was run by hand. Its results were written up as one page, rendered by a small template from a results.json. A scheduled job needs the same page, and a summary it can show on the run itself. script/e2e/render-report.py renders a results.json in schema 1 as a whole HTML document, as a fragment for a page that wraps its own, or as Markdown for $GITHUB_STEP_SUMMARY. It refuses a status the page has no chip for. script/e2e/results.py is how a job writes that file: each gate, check and timing sample is one JSON line, and build turns the lines into the document, with an earlier run as the baseline. The template gets a few fixes too: - high-severity findings sorted after the low ones; - an open item lost its links when it had no detail; - a page with no baseline showed an empty legend entry. The full document now puts the title and the styles in <head>. script/e2e/test-render-report.sh renders the v0.3.0 results and a canary's records, and checks the refusals. CI runs it in the shell job, next to shellcheck. Refs: #358 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
CI drives brig against a stub runtime only. Every v0.3.0 release blocker needed a real one: the runtime's own environment, many real execs, a second user on the host. They were all on main before the release candidate caught them. script/e2e/canary-linux.sh runs on a fresh ubuntu-24.04 runner, which exposes nested KVM and has passwordless sudo. It installs brig with this commit's install.sh as a rootless user, and swaps in brig and brigd built from the commit. Then it runs one gate per past blocker: - a forced image pull with DOCKER_CONFIG unset; - 100 short brig sh commands and 100 raw nerdctl execs, counting empty or wrong output; - claude-code boots, counting refusals that name .claude; - a no-sudo user's install, which must stop before the bundle download and print the commands for root; - --cpus, which is expected to fail on bundle rc9 and is enforced from rc10 on. The checks cover version and doctor, every shipped profile, rm and --home, #344, port publishing, the #342 postures, #341, the #332 symlink refusal with a canary checksum, and leftovers after every block. LEVEL=nightly boots more often, and adds a parallel block and stop/start churn. .github/workflows/e2e.yml runs it nightly, on demand, and on a pull request that touches script/e2e/, the workflow or install.sh. The job always uploads report.html, results.json and the logs as e2e-report, and writes the summary. A no-go verdict fails the job. Refs: #358 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The Linux canary covers the rootless install. Three v0.3.0 blockers were on macOS, though: every boot failed on the guest HOME, a quarantined channel build hung over SSH, and the Homebrew tap would not read. None of them shows on a hosted macOS runner, which cannot nest virtualization and has SIP in a state we do not control. The macos job runs script/e2e/canary-macos.sh on the self-hosted SIP-enabled Macs. It installs hull from the newest hull@main channel release, checked against its checksums.txt, and brig built from the commit, and boots real microVMs through them. On schedule and dispatch it also puts the notarized channel build of the commit through Gatekeeper, quarantined the way Homebrew leaves it. The checks mirror Linux, plus the three hvi postures and vz's refusal of isolated. A published port that takes about 30 s to answer after a restart is a warning. These Macs are shared and persistent, so the job moves HOME to a scratch directory for its whole run. hull's store, ~/.brig, ~/.config/brig and the Go caches live there. script/e2e/cleanup-macos.sh stops every process that names that directory, detaches the store and removes it, in an always() step. Pull requests from forks never reach these Macs. The tap job taps brig-sh/brig and runs brew readall on it, on a hosted macos-15 runner. readall prints nothing when every cask reads, and the check records that as a pass. Each host uploads its own results. The report job merges them with script/e2e/merge-results.py into one results.json and one page, with a column and a tab per host, and a no-go when any gate fails on any host. A host whose job uploaded nothing shows as a failure. The report job also takes over the timing baseline, the last scheduled run that passed. Refs: #358 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
A new runtime bundle reaches users when install.sh's pin moves. The e2e job only tested the pin, so a bundle met real hosts only after the pin already named it. The workflow now takes a runtime_version input, a brig-standalone-linux tag. A plan job checks its form and that the release exists. It then adds a second Linux leg that exports BRIG_RUNTIME_VERSION, which install.sh already reads in place of its pin. Each leg is a host of its own in the report. The page names the bundle each leg installed, read from its pins.env, and whether it came from the pin or the override. A pull request and a scheduled run test the pin alone, and a runtime_version equal to the pin adds no leg, since the pinned leg installs it. The --cpus gate still decides from pins.env, so from rc10 on it is a real check. It now also records the cloud-hypervisor boot= value next to the guest's nproc. Each leg also reads the record a bundle from rc12 on keeps in share/guest. The kernel and initrd have to match SHA256SUMS, and the release's checksums.txt has to list it. The bundle's own cosign has to verify the signature against the identity install.sh pins, and to refuse brig's release identity. An older bundle skips both checks. Refs: #358 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The canaries checked the release blockers up to v0.3.0. The fixes since then were covered by unit tests and script/smoke.sh only, against a stub runtime. Both canaries now check that: - only the variables brig is told to forward reach a claude-code guest, and a denylisted one does not (#236) - agent rm refuses while a sandbox of the profile exists (#367) - a policy the runtime cannot enforce is refused before boot (#237) - a credsStore helper that never answers is named in the refusal and killed with cosign (#365). The Mac skips this without cosign Linux also checks the note brig prints when it drives docker (#30). The Mac also checks that: - a gateway probe that fails refuses a policy boot (#171) - brig info names the posture a sandbox kept after its policy was detached (#368) - rm --all stops the shared gateway, and a boot replaces a gateway that brig 0.2.0 started (#366) - vz and qemu refuse --network isolated and point at separate hosts, and two shared hvi sandboxes reach each other, as docs/security.md says (#364) The boot-asset checks (#234, #403) run when the commit under test has the change they check, and are skipped otherwise. On Linux they change the installed runtime bundle's files one at a time: - intact files boot with the boot assets verified, under warn and under require - a changed kernel, or a record the release does not list, refuses in both modes, and the files put back boot again - without the signature file, warn says it cannot check and boots, and require refuses - a cosign held by a proxy that never answers is cut off, and warn says it cannot check and boots The Mac runs them against a scratch HULL_BOOT_ASSETS directory: - a first fetch under require records the verified digest, not the tag - a changed kernel refuses under warn, and is not fetched over - a named copy that differs refuses under require, before hull runs - an older published bundle is fetched again by digest, and boots Refs: #358 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The two link-refusal fixes that just landed change behaviour the stub runtime in smoke.sh cannot see: a hostmount source resolved by a real container runtime, and brigd's lock file on a real host filesystem. Guard both on the real hosts the e2e canaries drive. Linux gains two checks. One boots claude-code, stops it so the runtime releases the .claude hostmount sources, replaces the sessions source with a symlink out of the workspace, and asserts the next boot is refused and the directory the link pointed at is untouched. The other runs the built brigd against a scratch --socket directory with a symlink and then a hard link planted at the lock path, and asserts each is refused before the socket binds and the target file is not truncated. macOS gains the brigd lock check, which is the platform it bites on: /tmp is sticky but has no protected-symlinks equivalent. The hostmount check stays Linux-only, since hull mounts hostmounts in-guest and hands the runtime no source path. Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
784fd4e makes nerdctl enforce an egress policy instead of refusing the run. The Linux canary still checked for the refusal, so it fails once rebased onto main. b65ef46 refuses an unquoted mode in a files: binding, and only unit tests covered it. The Linux policy check now runs the cases of docs/manual-tests/egress-policy-linux.md on claude-code sandboxes, since a policy attaches to an agent profile only: - under default: deny, the allowed name reaches, a denied name does not resolve, and an address no rule allows is refused. A sandbox with no policy reaches all three, so the refusals come from the policy; - under default: allow, the denied name is refused and the rest reaches; - a resolver killed with SIGKILL fails closed: the allowed address is refused once its heartbeat lapses; - the next brig sh boots the sandbox again under a new resolver; - detaching the policy boots it with no table and no resolver records; - rm leaves no egress table, resolver or record. Setup installs nftables when nft is missing, since a policy run refuses to boot without it. Both canaries also check that an unquoted files: mode is refused with a message that says to quote it, that the quoted mode parses, and that an override of a built-in with an unquoted mode is ignored and the built-in runs. Refs: #358 Refs: #266 Refs: #453 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The Linux job tested the bundle install.sh pins, and a newer release only when someone dispatched the run with runtime_version. The plan now adds a leg for the release marked Latest on every event, unless install.sh already pins it. A runtime_version that another leg installs adds none. Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The report named the hull build and the runtime bundle, but not what booted the guests. macOS now records the hvi commit the hull build pins, with hvi-vmm main beside it when they differ, and the boot assets brig fetched into hull's store. It also checks that hull is the build of hull's main, and skips while the channel has not published it yet. Linux records the hull-assets version and the VMMs from pins.env. Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
Since #462 claude-code keeps its credential directory on a tmpfs at /brig/claude, off the home share. The Linux claude gate still read /root/.claude, and a refusal naming /brig/claude did not count as one. A new block on both hosts boots claude-code and checks that: - CLAUDE_CONFIG_DIR is /brig/claude, on a tmpfs, and a file written there reaches no host directory; - brig rewrites a hostmounted .claude.json in place and the guest sees the key; - a host rename of the home's .claude leaves the tmpfs in place. Refs: #462 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The canaries ran with DO_NOT_TRACK=1, so nothing exercised the events brig sends since #459. A new block on both hosts records a yes, points HULL_TELEMETRY_ENDPOINT at a local capture server and checks that: - brig ls, sh and rm send one ok command event each, with product brig and the install id brig telemetry status shows; - hull adds the sandbox's start and end only when brig counts it: a release or rcN-main build from rc31 on, as hullVersionAtLeast reads it; - DO_NOT_TRACK=1 sends nothing, and nothing stays queued. The block records a no at the end. No event leaves the runner. Refs: #459 Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CI drives brig against a stub runtime only. Every v0.3.0 release blocker needed a real one, so each of them was found late, by hand, before the release candidate. This is the first step of #358: an e2e workflow that runs real-runtime checks on three kinds of host, nightly and on demand, and renders the results as one HTML report.
ubuntu-24.04runner, which exposes nested KVM.script/e2e/canary-linux.shinstalls brig with this commit'sinstall.shas a rootless user, swaps in brig and brigd built from the commit, and runs one gate per past release blocker plus a set of checks.mac-dev-01-brig-1and-2(labelsip-enabled).script/e2e/canary-macos.shinstalls hull from the newest hull@main channel release and brig from the commit, and boots real microVMs through them. On schedule and dispatch it also puts the notarized channel build of the commit through Gatekeeper, quarantined the way Homebrew leaves it.macos-15runner runsbrew tapandbrew readall brig-sh/brig, with no VM.Each host uploads its own
results-<host>.json. A report job merges them withscript/e2e/merge-results.pyinto oneresults.json, rendersreport.htmland the job summary, and uploads both ase2e-report. The verdict is go when no gate fails on any host. A host whose job uploaded nothing shows as a failure, never as green.The Mac runners are shared and persistent, so the macOS job leaves them as it found them. It points
HOMEat a scratch directory under/private/tmpfor the whole job. hull's store,~/.brig,~/.config/brigand the Go caches all live there, and the runner account's own~/.hulland~/.brigare never touched. Analways()step (script/e2e/cleanup-macos.sh) stops every process that names that directory, including brig'shull network-gateway, detaches the store's sparse image and removes the directory. A concurrency group keeps one macOS e2e job on those Macs at a time, and pull requests from forks never reach them.A runtime bundle can be tested before
install.shpins it.workflow_dispatchtakesruntime_version, a brig-standalone-linux tag. A plan job checks its form and that the release exists, and then adds a second Linux leg that exportsBRIG_RUNTIME_VERSION, whichinstall.shalready reads. Each leg is its own host in the report, and the page names the bundle each one installed, read from itspins.env. A pull request and a scheduled run test the pin alone.Not in this PR, and next: a failing scheduled run opening or updating one tracking issue.
Related issues
Refs #358
Changes
script/e2e/canary-linux.sh, the Linux canary.LEVEL=canaryis short.LEVEL=nightlyboots more often, and adds a parallel block and stop/start churn. The gates:DOCKER_CONFIGunset (brig from main cannot boot or exec on macOS: the guest HOME reaches hull's own environment #337);brig shcommands and 100 rawnerdctl exec, counting empty or wrong output;.claude;--cpus, read from the bundle'spins.env: expected to fail on rc9 and older, enforced from rc10.brig sh <ref> -c, on a pty from script(1). A check about the shared network names--network shared, since the shipped profiles start a new sandbox isolated.script/e2e/canary-macos.sh, for the SIP-enabled Macs, written for the bash 3.2 macOS ships. The Gatekeeper gate, and checks for version and doctor, every shipped profile,rmand--home, fix(wrap): Delete the guest home a failed first boot created #344, feat(cli)!: Remove brig shell #341, the Keep the network posture a sandbox was started with #342 postures on hvi (shared, offline and isolated, egress checked with curl from claude-code), vz's refusal of--network isolated, port publishing and the fix(wrap): refuse a symlink on the project path #332 refusal. A published port that takes about 30 s to answer is a warning: a guest that gets an address an earlier guest had comes with a new MAC.rmand--home, fix(wrap): Delete the guest home a failed first boot created #344, port publishing, the Keep the network posture a sandbox was started with #342 postures, feat(cli)!: Remove brig shell #341, the fix(wrap): refuse a symlink on the project path #332 symlink refusal with a canary checksum, and leftovers after every block.SHA256SUMS, the release'schecksums.txtlists it, and the bundle's cosign verifies the signature againstinstall.sh's identity and refuses brig's own. It then changes the installed files one at a time: a changed kernel and a record the release does not list refuse under warn and require, a missing signature is "cannot check", and so is a cosign held by a proxy that never answers.HULL_BOOT_ASSETSdirectory: a first fetch pinned to the verified digest, a changed kernel refused under warn and not fetched over, a named copy refused under require, and an older bundle fetched again by digest.dc36d62:docs/manual-tests/egress-policy-linux.mdon claude-code sandboxes, since a policy attaches to an agent profile only.default: denyanddefault: alloware each checked against a sandbox with no policy, so a refusal is shown to come from the policy. A resolver killed with SIGKILL must fail closed, the nextbrig shmust boot the sandbox under a new resolver, a detach must leave no table, andrmmust leave no table, resolver or record. This replaces the Linux half of the Refuse a run when the runtime cannot enforce a planned property #237 refusal check, which 784fd4e made obsolete. The Mac keeps it. Setup installs nftables whennftis missing.files:mode is refused with a message that says to quote it, the quoted mode parses, and an override of a built-in with an unquoted mode is ignored while the built-in runs (Refuse an unquotedmode:in a files binding instead of applying another mode #453).a82606a, in both canaries:CLAUDE_CONFIG_DIRis/brig/claudeon a tmpfs, and a file the guest writes there reaches no host directory. A hostmvof the home's.claude, asgit clean -fdxmakes, leaves the tmpfs in place, and a second file stays off the host too. brig rewrites a hostmounted.claude.jsonin place (e1df7bd), so the guest's bind sees the trust key. The Linux claude gate now reads/brig/claude, and a refusal that names it counts as one.HULL_TELEMETRY_ENDPOINTat a local capture server, and expects oneokcommand event each forls,shandrm, with productbrigand the install idbrig telemetry statusshows. hull adds the sandbox'sstartandendon macOS only when brig counts its events: a release or anrcN-mainchannel build from rc31 on.DO_NOT_TRACK=1sends nothing, nothing stays queued, and the block records a no at the end. No event leaves the runner.bundle.json; on Linux the hull-assets version and the VMMs frompins.env. macOS also checks that hull is the build of hull's main, and skips while the channel has not published it yet.script/e2e/tap-check.sh:brew tap brig-sh/brigandbrew readall brig-sh/brig, each a pass or a fail..github/workflows/e2e.yml: nightly withLEVEL=nightly,workflow_dispatchwith a level and a runtime version, andpull_requestonscript/e2e/**, the workflow andinstall.sh. Jobsplan,linux(a matrix of bundle legs: install.sh's pin, the newest bundle release when install.sh does not pin it yet, and a dispatched runtime version),macos,tapandreport. The timing baseline is the report of the last scheduled run that passed.script/e2e/render-report.pyandreport-template.html: the renderer of the v0.3.0 release-gate page, with a new--summarymode for$GITHUB_STEP_SUMMARY. Template fixes: high-severity findings sorted after the low ones, an open item lost its links without a detail, and a page with no baseline showed an empty legend entry. The full document now puts the title and the styles in<head>, and the footer links to the workflow run.script/e2e/results.pyandmerge-results.py: how the scripts writeresults.json, one JSON line per result, and how the report job joins the hosts.script/e2e/test-render-report.sh: builds, merges and renders the fixtures, and checks that a bad status, a failing or missing gate and a missing host are caught. CI's shell job runs it, with shellcheck overscript/e2e/..github/actionlint.yamlnames thesip-enabledrunner label.CONTRIBUTING.mddescribes the new CI step, the e2e workflow and the runtime override.Testing on this PR
e2e 37521497079 at
f9d61f9, rebased on mainf685ee9(v0.4.0): all six jobs pass (plan,linux (linux),linux (linux-rc16),macos,tapandreport), the verdict is go, and no check fails. CI and DCO are green on the same head.v0.1.0-rc15(hull-assets 0.1.9, uruncf6d54d6): 71 checks pass.v0.1.0-rc16, the leg the plan added on its own: 71 checks pass. rc16 carries brig v0.4.0 and the same runtime as rc15.claude-desktop bootsis skipped (no console session on a runner). hull came from the channel build of159772e,v0.1.0-rc31plus its changelog commit, with hvic53a857and boot assets 0.1.10 (kernel 6.18.55, urunc6c062f3).brew tapandbrew readall brig-sh/brigpass.The three
/brig/claudechecks and the telemetry check pass on all three hosts. The channel hull reports the Go pseudo-versionv0.1.0-rc31.0.<date>-159772e, which brig'shullVersionAtLeastreads as a source build, so brig suppresses its sandbox events. The check follows that rule.CodeQL's
Analyze (python)failed on this head while uploading its results, after the analysis itself finished, and recorded no analysis. The Python files are the same as onfccb487, where it passed with no alerts.The previous run, e2e 37509684368 at
fccb487on mainc405276, passed the same way with rc14 as the pin and rc15 as the newest leg. The run before it, e2e 37507499577 atc3e1124, failed the telemetry block on every host (no hull on Linux, and the pseudo-version on macOS); both were fixed infccb487. Its rc15 leg also saw9.9.9.9:443closed from a sandbox with no policy, twice, while an allow policy reached it. The same checks have passed on rc15 since.The new checks also ran locally on this Mac (macOS 26.3.1, SIP off) against the brig they check and against brig from before each change:
8e9d2ff(before #462): 3 fail, noCLAUDE_CONFIG_DIR, no/brig/claude.claude.jsonrewritten in placeb769d28(before e1df7bd): fail, inode replaceda82606a(before #459): fail, hull'sexec,run,stopinstead of brig'sls,sh,rm9f1d349(rc30.x, no sandbox events) and with hullv0.1.0-rc31(start,end)A probe the guest writes on the home share is found by the same leak search, so the search is not blind.
The egress checks pass on the targets they name. For instance:
Before the push, the Linux canary ran on a fresh Ubuntu 24.04 VM with nested KVM. Its exec gate once got
\^@ok-76frombrig sh, a NUL byte ahead of the right output. The same loop on the VM gives it ondc36d62too, so it predates this branch. It is tracked in #460.The macOS job leaves nothing behind:
The
runtime_versionleg was last run in e2e 36208511977 with rc10, and its--cpusgate passed on that bundle.Locally:
script/e2e/test-render-report.shand shellcheck 0.9.0, the version CI installs, pass on the final tree.Checklist
make allpasses (vet, test, build)script/smoke.shpassesI have rungo test ./... -race, if the change touches concurrency, subprocesses or the daemonI have exercised the change against a real runtime (brig run <agent>), if it touches the run, exec or credential pathCredentials and the sandbox boundary
Neither promise changes, since no Go code changes. The canary adds a negative test of the first one on a real runtime, on Linux and on macOS. A sandbox with the parent project read-write plants links to a directory outside it. Three runs through those links must be refused, the real directory next to them must still boot, and a checksum over the outside directory must match before and after.