Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
010bd2b
feat(gate): observe what a run does to the disk, and say so as it hap…
ebursztein Aug 3, 2026
c43aa75
test(gate): inventory every Rust test target
ebursztein Aug 3, 2026
64fb793
test(rust): eliminate ignored correctness evidence
ebursztein Aug 3, 2026
a9e5ae1
fix(gate): preserve provenance in linked worktrees
ebursztein Aug 3, 2026
897c7c8
test(rust): enforce Clippy in Linux parity
ebursztein Aug 3, 2026
acbe31f
test(release): respect fail-closed staging policy
ebursztein Aug 3, 2026
056dafd
fix(gate): build the plan from source, not from what the last run lef…
ebursztein Aug 3, 2026
1405ecf
test(gate): pin the release plan as identical, not merely buildable
ebursztein Aug 3, 2026
7667c5e
fix(gate): stop failure evidence from being silently incomplete
ebursztein Aug 4, 2026
b403b3c
feat(gate): make the container boundary a decision instead of an omis…
ebursztein Aug 4, 2026
630bb90
feat(gate): give the Linux parity lane its own bytes, and seal it
ebursztein Aug 4, 2026
7805bd8
chore(gate): remove what the sealed lane stopped needing
ebursztein Aug 5, 2026
dae4de0
fix(gate): stop the observer inventing faults it cannot locate
ebursztein Aug 5, 2026
3aaafce
test(gate): stop the source-state probe racing the suite around it
ebursztein Aug 5, 2026
f673a6a
fix(frontend): bound fast-uri past the advisory it was meant to exclude
ebursztein Aug 5, 2026
19c93e3
fix(gate): write the recipe the sealed lane tells you to run
ebursztein Aug 5, 2026
ab834f3
test(gate): give the spawned child a module it can import
ebursztein Aug 5, 2026
05502a0
test(gate): answer the one probe the recorded plan reads
ebursztein Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Ignore massive host build artifacts
target/
node_modules/
.venv/
.pytest_cache/
.ruff_cache/
.astro/
coverage/
dist/
site/node_modules/
frontend/node_modules/
frontend/dist/
*.db
*.sqlite
*.log
.DS_Store
# Build context for the thin lane image.
#
# Patterns are `**`-prefixed deliberately. A bare `target` matches only the
# root, and this repository contains agent worktrees under `.claude/worktrees/`
# that carry their own multi-gigabyte `target/` trees -- the first build with a
# root-only pattern swept one in and failed with `no space left on device`
# after nineteen minutes.
**/target
**/node_modules
**/.venv
**/dist
**/assets
**/packages
.claude
.git
**/.pnpm-store
**/test-artifacts
176 changes: 176 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `just _warm-linux-rust-base` builds the Linux parity base image, with
network, before a sealed run needs it. The lane deliberately refuses to build
it mid-run -- its tag is keyed by `Cargo.lock`, `rust-toolchain.toml` and
`frontend/pnpm-lock.yaml`, so a dependency bump re-keys it, and resolving
that inside the run would turn a `--network none` lane into a multi-gigabyte
network build at minute four -- and its refusal named `just warm`, which did
not exist. Bumping `fast-uri` for a security advisory re-keyed the image and
found it: the release stopped correctly and handed back a command that fails.

A contract now checks that every recipe the gate names in an operator-facing
message is a recipe the justfile defines. `hostimage.py` already carried a
note about the last time this happened -- `just _build-host-image`, dispatched
by two lanes, never written -- so it is a class, not an incident.

### Fixed

- `capsem-gate doctor` no longer reads a justfile comment as a dispatch. It
scanned every line containing `capsem-gate `, so prose naming a subcommand
was parsed as a call to ``linux-rust` `` -- trailing backtick included -- and
reported as unknown. Three doctor checks went red on a comment.

- `test_a_live_run_is_never_rotated_away_by_another` no longer answers
differently depending on how pytest was started. A spawned child re-imports
its target's module with nothing but a copy of the parent's `sys.path`, and
`--import-mode=importlib` names test modules `tests.<basename>` without ever
putting the repository root on that path -- so the name resolved under
`python -m pytest`, which contributes the working directory and is how the
gate invokes every suite, and not under the `pytest` console script. Run the
file on its own and the child died on `ModuleNotFoundError: No module named
'tests'` while the parent sat out a sixty-second queue timeout. The worker
moved to `tests/helpers/`, which the root conftest puts on `sys.path` before
collection under every invocation and in every xdist worker. Spawn is
unchanged: cross-process rotation safety is the point, and it is the
stricter start method.

- The gate's ordering contracts run from a linked git worktree again.
`docker_git_metadata_mount` skips its probe entirely when `.git` is a
directory, so an ordinary checkout never asks; a worktree carries a `.git`
file instead, asks `git rev-parse --git-common-dir`, and got "" back from a
recording runner that answers nothing by default. An unresolvable common dir
is a build the gate rightly refuses, so the recorded plan died at
`package.<arch>.build` and every contract about a command issued at or after
that point failed for want of a git answer rather than for anything it was
about -- on worktrees only, which is why it stayed invisible. The recorder
now answers that one probe from the real repository, truthfully, because a
wrong path here becomes a `-v` mount those same contracts assert against.

### Security

- `fast-uri` is bounded past GHSA-7p8r-x3mc-p8w7 (host confusion via a
backslash authority introducer). The frontend's override read `>=3.1.2`,
which resolved to 4.1.1 -- inside the advisory's `>=4.0.0 <4.1.2` -- so the
bound admitted the very version it was there to exclude. Now `>=4.1.2`. It
arrives through `@astrojs/check`, so it is dev-only tooling, but the audit is
a release gate and blocked one.

### Fixed

- The filesystem observer reported 42 phantom source mutations on every release
run, and each named a file nothing had touched. `shutil.rmtree` deletes
through a directory descriptor -- `os.unlink('profile.toml', dir_fd=5)` -- so
a bare entry name reached the observer, which resolved it against the current
working directory. That directory is the checkout root, so removing
`target/config/profiles/code/profile.toml`, an ordinary step, was reported as

[source-tree] profile.toml: unlink during the run

naming the tracked `config/profiles` file of the same basename. A guard that
cries wolf 42 times a run is a guard nobody reads, and this is the guard that
exists to catch the `config/profiles` race that killed a release run.

Fixed in three places, because one of them alone would have hidden the
others. Interception now resolves a subject against its `dir_fd` (and
resolves an integer descriptor subject, as in `os.truncate(fd, n)`), so the
fault names where the call acted. The judge refuses to classify any path that
is not absolute, so no caller's loose spelling can be misattributed again --
not merely the one that was found. And `dist`, `packages` and `assets` join
`target` as build output: all are gitignored roots the gate rewrites every
run, and with only `target` excluded, resyncing `assets/current` or clearing
a stale `.deb` read as mutating the tree being qualified.

When a path genuinely cannot be established the event is not judged at all: a
fault nobody can locate is not evidence, and inventing one is worse than
missing it.

### Security

- The local Tauri signing key and its password no longer reach anything that
Expand All @@ -24,6 +111,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Sealing the Linux parity lane left its old machinery behind, and the release
gate caught it. `_LinuxRustSuite` -- the action that assembled the read-only
source mount, the writable grafts and the four named volumes -- was still in
`hostimage.py` with nothing constructing it, along with nine `[hostimage]`
settings that existed only to repair what sharing a checkout with a
root-owned container broke: a writable `/tmp`, a hand-placed `HOME`, a
bound-out nextest directory, a writable graft for Tauri's generated ACLs, and
the volumes themselves. All are gone.

Restored in the same pass: the lane raised a named error on a host that is
neither Linux nor macOS, and sealing it dropped that guard, so a third
platform fell through to the Docker path and would have failed somewhere
inside a container instead of saying which host it will not run on.

The two contract tests that pinned the old mechanism are reimplemented rather
than deleted, and two of their claims are now stronger: `/src:ro` said the
container could not write the checkout, where the assertion is now that
nothing is mounted at all; and the lane's `--network none` is asserted, which
no earlier test could claim because it was not true.

- The Linux parity lane holds its own bytes. It bind-mounted the live checkout,
grafted two writable mounts back through it to retrieve coverage, inherited
four named volumes that survive between runs, and ran with outbound network
because nothing ever passed `--network`. It now builds its source into an
image, runs with `--network none`, and returns coverage through `docker cp`.
Dependencies live in a base image keyed by `Cargo.lock`,
`rust-toolchain.toml` and `frontend/pnpm-lock.yaml`; a lockfile change makes
a new tag and the gate refuses to start rather than rebuilding multiple
gigabytes at minute four.

Sealing it surfaced two fetches nobody had recorded. The lane built the
frontend with `pnpm install` mid-run whenever `frontend/dist` was absent, and
`ort` -- ONNX Runtime, under `magika` -- downloaded a binary from
`cdn.pyke.io` inside a build script on every cold build. Both now come from
the image: the frontend is built there, and ONNX Runtime is Microsoft's
official release with `ORT_STRATEGY=system` and `ORT_PREFER_DYNAMIC_LINK`.

- Containers now declare their network, and a mount of the working tree is
refused. Nothing in the gate passed `--network` at all, so every container
had outbound access by omission and several fetched dependencies mid-run --
the difference between proving a build reproduces and proving it reproduces
today. `Mount` refuses a source inside the checkout, because
`-v <repo_root>:/src` let a host step churning hardlinks and a container
reading the same inodes over virtiofs share a filesystem neither declared,
which killed a release run with an intermittent `Permission denied` on a
file that was `0644` before and after. The two privileged install
containers still need both and say so through `Mount.unmigrated`, which a
test enumerates so the count can only shrink.

- Every `Call` answers, in a form a machine can read, why it is not an ordinary
declared action and what it can affect. It carried a required kind before;
now it carries a closed kind, a reason its author wrote, and a declared
Expand Down Expand Up @@ -67,6 +203,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Failure-evidence bundles were quietly incomplete. `copy_small_file` returned
the same silence for three different outcomes -- the file was absent, over
the size cap, or unreadable -- and the IronBank globs matched nothing at all
on a tree where those builds never ran, so a bundle could not distinguish
"there was nothing to collect" from "the collector failed". Every bundle now
carries a `collected.json` naming each source attempted and what became of
it, globs included when they match nothing.

The first bundle written with that manifest reported `build.log` and
`docker-storage.jsonl` as over the cap -- meaning every previous bundle had
silently omitted the two files a post-mortem reaches for first, and did so
precisely on the long runs that needed them. Oversized files are now tailed
rather than dropped, because the end of a build log is where the failure is.

- A fresh clone now plans the same gate a warm tree does. The functional
module asked for its profile axis while the plan was being *built*, and that
read `target/config/profiles` -- build output -- so the same commit produced
one plan on a developer's machine and another on a clean checkout. A release
passed a 57-minute gate locally on leftovers, pushed, dispatched, and CI
failed with 94 tests all reporting `no materialized profiles found`. Neither
`source.record` nor `source.verify` could have caught it: they digest tracked
source, and this input was not tracked source. The axis comes from checked-in
`config/profiles/` now. The agreement it used to check inline -- materialized
against declared against source -- did not go away; it became a step, which
is where a question about build output can actually be asked.

- A gate now reports what it did to the filesystem, as it does it. `contends`
is a list an author typed and the overlap check compares two such lists to
each other -- nothing in that loop had ever looked at a disk, so a step that
did not mention what it touched satisfied every check by saying nothing, and
the writer was frequently a unit test three subprocesses down. Two sources
feed it now: the in-process primitives are proxied, so the caller and the
state *before* the call are both known exactly, and a `watchdog` observer
covers what subprocesses do. Faults land on stderr the minute they occur, in
a size-capped log beside the run that survives a `kill -9`, and in the run
log. It names hardlinks between checked-in source and build output, modes
that change and change back, source writable beyond its owner, artifacts that
end a run empty, identical bytes under two names, and two concurrent steps
touching one path neither declared.

- `release-profile` reached the step before publishing and refused, because
`tested-head` was empty: four contracts that run a release plan to read back
its argv had overwritten the running gate's record of the revision under
Expand Down
Loading
Loading