Skip to content

fix(egress): make the high-volume flag's documented contract true - #96

Merged
Pyronewbic merged 9 commits into
mainfrom
fix/egress-high-volume-surface
Jul 21, 2026
Merged

fix(egress): make the high-volume flag's documented contract true#96
Pyronewbic merged 9 commits into
mainfrom
fix/egress-high-volume-surface

Conversation

@Pyronewbic

Copy link
Copy Markdown
Owner

docs/configuration.md documented sluice egress --json as emitting
"high_volume":true on a host row. It never did — the only emitter was the
persisted receipt. A CI gate written against the documented contract parsed for a
key that was never there and silently never fired. This makes the documented
behaviour true rather than weakening the doc, then corrects what the docs say
about the measure and the surfaces.

Code

  • high_volume and the GB/TB ladder now reach sluice egress (human and
    --json). The ladder previously shipped in the receipt renderer only, so a
    5 GiB transfer printed as 5222.4 MB on one surface and 5.10 GB on the other.
  • SLUICE_EGRESS_FLAG_BYTES=0 disables the flag. It is all-digits, so it
    passed the numeric guard and reached the comparison as a real 0; every reached
    host then satisfied bytes >= 0, including 0-byte rows. The value you would pick
    to turn the flag off turned it on for everything.
  • The threshold is length-bounded. Past 2^63 the shell comparison emitted
    [: integer expected on stderr once per reached host, breaking a 2>&1 | jq
    consumer; between 2^53 and 2^63 the shell compared exactly while awk compared as
    an IEEE double, so the two renderers disagreed about the same host.

The unit ladder was already duplicated between _human_bytes and the receipt awk.
Rather than add a third copy, it is hoisted to one _AWK_HUMAN fragment prepended
as awk program text — one definition, still zero forks per row (these format
inside a single awk pass, so calling _human_bytes per row would fork an awk per
host). grep 1099511627776 src/ returns one line where it returned three.

Docs

The measure was documented as "tx bytes"; the accumulator is tx + rx, so a large
download trips it too. Also registers high_volume in the operations.md schema
table that declares itself the single registry, documents SLUICE_ALLOW_HOME (the
only escape from a hard refusal, previously in no knob reference), corrects the
SLUICE_MASK symlink limit — which described the behaviour backwards — and records
that forbid-laundering refuses every shipped agent preset.

Verification

Adversarial pass over the branch diff found 10 confirmed defects, 9 new,
all fixed here. Three were false claims in my own comments and docs:

  • The %d in the shared ladder was documented as coercing a non-numeric count to
    0. It does not — a non-numeric value string-compares past every rung and renders
    0.00 TB, a garbage count reading as petabytes on an audit surface. Fixed with
    b+=0 so the claim is true.
  • The errexit rationale on the =0 guard was wrong: && lists are already exempt.
  • docs/hardening.md asserted a symlink target outside the mount "already dangles
    in the box". In a linked git worktree the git common dir is mounted, so a
    masked-pattern link into it stays readable while doctor reports three green
    checks. The code behaviour is pre-existing; only the false justification was new.

One new test could not fail — it refuted a tag without asserting the row rendered,
so it passed when cmd_egress early-returned, and passed unchanged on main. Now
anchored.

414 unit tests, 0 failures. make test-awk clean under one-true-awk, mawk and
gawk; real render eyeballed under all three, byte-identical. Linux VM leg not run —
relying on CI's Linux, Docker.

🤖 Generated with Claude Code

https://claude.ai/code/session_017TqRkhSka9ArLr4svJv858

`_egress_flag_bytes` guards only against non-numeric values, so `=0` is
all-digits and reaches the comparison as a real 0. The human receipt renderer
always guarded this (`thr+0>0` in the awk), but the JSON path did not: with a 0
threshold every reached host satisfied `bytes >= thr` and was written
`"high_volume":true` - including a 0-byte row. The one value a user would pick
to turn the flag OFF turned it on for everything, and the persisted record is
the surface `egress --export` and `ls --json` re-serve.

Guards the threshold before the comparison so both renderers agree, in an `if`
rather than a longer && chain so it cannot trip errexit.

Covers `=0` on a normal and a zero-byte row, plus the malformed-value fallback
that was never asserted. Verified as a real tripwire: the =0 case fails against
the unfixed slice, not just passes against the fixed one.
…ess`

docs/configuration.md documented `sluice egress --json` as emitting
`"high_volume":true` on a host row. It never did - the only emitter was
_persist_receipt, so the field rode only on the at-exit receipt and the records
`egress --export` and `ls --json` re-serve. A CI gate written against the
documented contract parsed for a key that was never there and silently never
fired. This makes the documented behaviour true rather than weakening the doc.

Also extends cmd_egress's byte render through GB/TB. The ladder shipped in the
receipt renderer only, so `sluice egress` still printed a 5 GiB transfer as
"5222.4 MB" while the receipt beside it said "5.10 GB" - the same volume, two
answers, on the two surfaces a reader compares first.

The ladder was already duplicated between _human_bytes and the receipt awk;
adding a third copy here would have made the unit boundaries a set of magic
numbers in three places that must agree or the surfaces disagree. Hoisted it to
one _AWK_HUMAN fragment instead, prepended as awk program text at each site.
That keeps it fork-free - these rows format inside a single awk pass, so calling
_human_bytes per row would fork an awk per host, the opposite direction from
f17b632 - while leaving exactly one definition. The B branch unifies on
sprintf("%d B"), which coerces a non-numeric count to 0 rather than echoing it
back into the render; the two inline copies previously concatenated it raw.

The flag compares tx+rx (field 4), so a large inbound transfer trips it too, and
the boundary is inclusive - both now pinned by tests. Note cmd_egress reads the
whole-boot window while the receipt is run-scoped, so the same threshold flags at
least as often here; that is a decision, not an accident.

Verified under one-true-awk, mawk and gawk (make test-awk, 0 failures) and by
eyeballing the real render under each - byte-identical output on all three.
Three fixes to what SLUICE_EGRESS_FLAG_BYTES actually does, now that
`sluice egress` carries the field:

- "a single reached host's tx bytes" understated it. The flag compares field 4
  of egress_rows, and that accumulator is `tx + rx` - a large inbound transfer
  trips it exactly as an upload does. Reading the old wording, someone tuning
  the threshold against an upload budget would set it too high.
- Names every surface that carries the tag rather than the one that did not.
- States the `0`-disables and non-numeric-falls-back contract, which was never
  written down. Sibling bullets are explicit about malformed handling; this one
  was silent while `=0` was the value most likely to be reached for.

Also registers `high_volume` in the operations.md schema table, which declares
itself the single registry for JSON contract fields - it was emitted on every
persisted record but absent from the registry, so anything generated from that
table missed it. Links the knob rather than restating the default a third time.
Two guards a user could not learn from the docs:

- SLUICE_ALLOW_HOME appeared only in THREAT_MODEL.md, which states the guarantee
  but is not where anyone looks up a knob. It is the sole escape from a hard
  refusal, and it is settable in sluice.config.sh (the config is sourced before
  the guard dispatches), so it belongs in Wiring rather than under
  "Environment-only knobs".
- hardening.md described the mask's symlink handling backwards: "symlink matches
  are skipped (a mount over a link would shadow its target)". That was true
  before the mask began resolving a matched link physically and shadowing its
  in-project target. A reader following the old text would believe a
  symlinked .env was exposed and go build a second mechanism for it.

The git-history limit stays owned by THREAT_MODEL.md, which hardening.md already
links - restating it here is how this paragraph drifted in the first place.
…ount

forbid-laundering is documented as a ceiling like any other, but it has a
consequence nothing recorded: every shipped agent preset allowlists a model or
stream API that laundering_host classifies, so a policy carrying the directive
refuses all ten. An operator would deploy it org-wide and discover that
`sluice agent` no longer runs anywhere. Notes too that SLUICE_LAUNDERING_OK
silences the session warning but does not override a policy refusal - it is
checked on the warn path, never in apply_policy - and that the directive takes
no argument, so the remedy is selective deployment rather than a relaxation.

This is a standing property, not fallout from the recent classification work:
most presets already refused before it; that work took coverage to all ten.

agents.md gains the matching two sentences on the session-start note, linking
the knob and the gate rather than restating either.

The preset count was stale in ROADMAP.md (nine, ten ship) and in the agent-demo
alt text. The count is a straight fix in ROADMAP; in the alt text it is not -
the GIF was recorded before the tenth preset and genuinely renders nine rows, so
"ten" would make the alt lie about the image beside it. Both alts drop the count
instead, which is honest against either recording; docs/agents.md keeps the full
description and the README carries a shorter teaser, so there is one owner to
re-sync when the GIF is re-recorded.
The repo carries no in-tree release history, so the only way to find what
changed in a version was to already know the Releases page exists. A pointer
rather than a CHANGELOG.md: the GitHub releases already carry Keep-a-Changelog
bodies and nothing generates them from a file, so a committed copy would be a
second thing to hand-maintain and a second thing to drift.
Findings from an adversarial pass over this branch. Each was reproduced against
the real code before being taken as real.

- SLUICE_EGRESS_FLAG_BYTES accepted any all-digit string. At >= 2^63 the shell
  comparison emits "[: integer expected" on stderr once per reached host, which
  breaks a `2>&1 | jq` consumer; between 2^53 and 2^63 the shell compares exactly
  while awk compares as an IEEE double, so the JSON and human renders disagree
  about the same host. Bounded to 15 digits at the single owner, which fixes both
  renderers and both call sites at once. ~1 PB is far past any real threshold.

- The shared awk ladder's `%d` was documented as coercing a non-numeric byte
  count to 0. It does not: a non-numeric value string-compares past every rung
  and falls out the bottom as "0.00 TB" - a garbage count rendering as petabytes
  on an audit surface. Added `b+=0` so the claim is true, since the point of one
  shared ladder is that the next call site can rely on it. Every current caller
  gates on *[!0-9]* first, so this is defence for the one that forgets.

- The errexit rationale on the =0 guard was wrong: `&&` lists are already exempt,
  so the old chain could never have tripped it. Dropped the claim; the `if` and
  the `-gt 0` term stay, the latter being the actual fix.

- docs/hardening.md's new symlink sentence asserted a safety property that does
  not hold: a target outside the mount was said to "already dangle in the box".
  In a linked git worktree the git common dir IS mounted, so a masked-pattern
  symlink pointing into it resolves and stays readable - while doctor reports
  masked:[], unmasked_secrets:[] and broken_symlinks:[]. mask_matches scopes to
  PROJECT_DIR while symlinks_outside_scope scopes to PROJECT_DIR plus the common
  dir, and that divergence is the mechanism. The code behaviour is pre-existing;
  only the false justification was new. Names the exception instead.

Also anchors a test that could not fail: the under-threshold human-render case
refuted a tag without asserting the row rendered, so it passed when cmd_egress
early-returned "nothing yet" - and passed unchanged on main. And unsets
SLUICE_EGRESS_FLAG_BYTES in the receipt suite's setup, so an exported value in
the environment cannot rewrite what the default-threshold assertions mean.
demo-beats designs the VHS tape beats for what has actually shipped: it
inventories user-visible capabilities since a tag, scores each on three
independent lenses (security story / recordability / coverage gap), designs a
beat for the survivors, then has a skeptic try to make each recording pass for
the wrong reason. Defaults to main, because a beat designed against an unmerged
branch can be designed against code that changes in review; args.ref and
args.include_uncommitted opt into unmerged work when that is what you want.

The skeptic stage is the point. A laundering tape was once rejected because
resolve_engine runs ~725 lines before warn_laundering, so with no engine on PATH
the command dies early - and since die also exits 1, a tape asserting
`|| echo "refused (exit $?)"` still prints its success line and the recording
proves nothing. Every beat now carries a cannot_pass_vacuously field, and the
shortlist step must argue explicitly about any capability the lenses disagree on
(one was previously dropped with no argument made against it).

attack-changes parsed args unconditionally as JSON, so the invocation its own
skill documents (`args: "main"`) threw before a single agent started - it had
never run via that path. preflight already had the tolerant form; both now
accept a bare ref, an object, or an object as a JSON string.
…anges is due

triage-tests was the only workflow here with no adversarial stage: it root-caused
each failure cluster and returned the diagnosis unchecked. A root cause is a
claim until someone re-derives it, and acting on a plausible-but-wrong one means
editing the wrong file - or rewriting a test to agree with broken code, which
CLAUDE.md names as how a broken `ls` shipped with a green suite. Adds a skeptic
per diagnosis that must establish the CAUSAL link rather than a correlation,
rule out an environmental cause (Docker down, a stale bin/sluice), and decide
explicitly whether the TEST or the CODE is wrong - surfaced as test_is_the_bug so
it can never be quietly resolved by making the test pass.

preflight now reports when the diff touches a security path and the attack pass
is warranted, rather than invoking it. Composing them was the alternative and is
worse: attack-changes runs ~20 agents for ~20 minutes, an order of magnitude past
this gate, and preflight is run on every branch while CLAUDE.md warrants the
attack pass only on a security path. Auto-escalation would bury that spend in a
routine check. The gate agent classifies from what the changed lines DO, not the
filename, so a docs-only touch of an egress file does not trip it.
@Pyronewbic
Pyronewbic enabled auto-merge (rebase) July 21, 2026 06:23
@Pyronewbic
Pyronewbic merged commit 9a68da4 into main Jul 21, 2026
9 checks passed
@Pyronewbic
Pyronewbic deleted the fix/egress-high-volume-surface branch July 21, 2026 13:27
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.

1 participant