Repository navigation
Description
Epic
Transcribed from https://github.com/gominimal/inbox/issues/625.
Summary
A local Box Egress Proxy (BEP): the un-enrolled, no-Gatehouse form of the architecture's BEP (architecture.md D6, Gatehouse §6.10, networking design §7.1). It runs on the host OS outside the VM, beside gvproxy, and:
- terminates TLS only for a box's declared credentialed upstream hosts, under a host-generated, name-constrained interception CA that
min box specshows; - substitutes sealed GitHub values and referenced host-store secrets (set once with
min secret) into requests on the way out, so a box never holds a plaintext credential; - re-checks the box's declared
[network] egress(the single reachability authority) and audits every admit and refuse to a local append-only log.
git, gh, SDKs and MCP clients need no configuration because boxes talk to the real hostnames. Ships in gominimal/minimal as Rust crates; the hosted/enterprise form imports them into Gatehouse later behind a plugin surface. The open-source form is complete on its own: nothing is hidden or detoured to a proprietary piece.
This is the first slice of the secrets plane, not the plane: one upstream module (GitHub), one host store (macOS Keychain), one host. Anthropic and MCP upstream modules, 1Password and other stores, and the Gatehouse-hosted BEP are later epics that reuse these crates.
Problem
A developer working in a box today has three ways to give an agent a credential: paste it in, pass it through from the client environment, or run the Python reference broker. All three put a bearer in the box, where a prompt-injected agent can read it and use it from anywhere for its full lifetime. The architecture ruled on 2026-09-06 that brokered secrets enter a box only as sealed values redeemed at a Box Egress Proxy, and on 2026-09-17 that on a laptop the BEP runs node-local, on the host OS outside the VM. The Gatehouse-hosted path (#512, gominimal/gatehouse#1) lands with the identity plane's fifth phase. An un-enrolled laptop has no credential path at all until then. The local ordering decided on 2026-09-15 puts the un-enrolled laptop first.
Decisions (stated as decisions)
- 2026-09-06 (architecture D6, Gatehouse v1.10): sealed values plus a TLS-intercepting BEP replace in-box token delivery and the credential-free-endpoint shape. Dismissed shapes are recorded in Gatehouse §12.11 and are not re-proposed here.
- 2026-09-12 (Gatehouse v1.18): module host sets. The GitHub v1 set is
github.com,api.github.com,uploads.github.com,codeload.github.com;objects.githubusercontent.comstays ordinary egress; port discipline is the BEP's, sogithub.com:22is never proxied. - 2026-09-15: local-first ordering. The un-enrolled host ships before the enrolled one. NET (docs(specs): add the box networking spec (NET) #1380) was re-scoped the same way.
- 2026-09-15, local substitutions for the D6/§6.10 roles:
- Trust domain = the host. "Node binding" means a value is redeemable only by this host's proxy; the "tenant" is the single local operator. Per-box attribution is the box's own source address under minimald's switch anti-spoof (
own_ip);host_ipboxes are cohort-granular with the T28 residual recorded;noneboxes get no CA and no grants. - Three-layer CA. A root CA whose certificate is the trust anchor injected at session creation; a signing CA that rotates freely, its private key held in the host keychain (Secure Enclave where available, never a file on disk); throwaway leaf certificates per upstream hostname. Name-constrained to the declared credentialed upstream set.
- Sign-in and minting are local. With no Gatehouse configured,
minperforms the GitHub device-flow sign-in, holds the refresh, mints tokens with ≤ 8 h expiry narrowed to the session's repositories, seals them for this host's proxy, and delivers only the sealed value into the box environment. With Gatehouse configured,mindelegates sign-in and minting (out of scope; the seam is kept). - Two secret styles. (a) Sealed GitHub values from the sign-in above. (b) Referenced host-store secrets, macOS Keychain first, substituted by name into declared upstream requests under the client's
[secret-store-rules], with pluggable resolver backends. v1 = (a) plus Keychain (b). - Steering.
proxy_envworks on day one;dnssteering is the default once NET's box-zone resolver lands. A PAC recipe is provided for macOS browsers. UDP is dropped;quic443 = autoblocks QUIC iff the box holds credentialed upstreams. - Audit is a local append-only log.
- Trust domain = the host. "Node binding" means a value is redeemable only by this host's proxy; the "tenant" is the single local operator. Per-box attribution is the box's own source address under minimald's switch anti-spoof (
- 2026-09-17 (gominimal/arch#67, Gatehouse v1.19, networking design v0.7): the node-local BEP and store references (
[secrets] source = "store",[secret-store-rules]) are the architecture's shape for LocalVM. This epic implements that shape un-enrolled. One contradiction is filed against arch (see References): v1.19 says an un-enrolled node-local BEP carries "no broker grants, at-redemption store members only", while the 2026-09-15 decision has localminmint sealed GitHub values. This epic follows the 2026-09-15 decision.
Desired outcome — user stories and acceptance criteria
L1. Sign in once
AS A developer using Minimal on a laptop with no Gatehouse,
I WANT to sign in to GitHub once withmin,
SO THAT every box I start afterwards can reach my repositories without me handling a token.
Acceptance criteria:
- One
mincommand completes the GitHub device flow in the browser; a second box start needs no sign-in. minreports that a sign-in exists and when it expires, never a token.- The refresh material lives in the host keychain, never in a file under the project or the box.
L2. Clone, push and gh in a box with no token present
AS A developer working in a box,
I WANTgit clone,git pushandghto work against my repositories,
SO THAT I do real work without configuring any tool.
Acceptance criteria:
- With
[network.bep] steering = "proxy_env",git clone,git pushandgh apiagainst a private repository in the session's set succeed inside the box with no tool configuration beyond what the box already carries. - Every environment variable, file and volume in the box is free of the GitHub token; the only credential-shaped value present is a sealed value that decrypts nowhere but this host's proxy.
- The box's egress declaration must admit the module's full host set; a spec with a GitHub grant whose egress omits a member is refused at expansion with exit 3 naming the missing hosts.
L3. Keychain secret by reference
AS A developer with an API key in my macOS Keychain,
I WANT to reference it by identifier in a box spec,
SO THAT requests from the box to that key's upstream carry it without the key entering the box.
Acceptance criteria:
- A
[secrets]entry ofsource = "store", store = "keychain", id = <item>under a matching[secret-store-rules]rule causes requests from the box to the rule's registered upstream to carry the value in the rule's injection form. - The value is read from the Keychain per request under the Keychain's own access control and is never written to disk by the proxy.
- A rule naming a deep-module host (
api.github.com) or Minimal's own infrastructure is refused when the rule is read.
L9. Set a third-party secret once
AS A developer with a third-party API key, such as a Claude OAuth token or an MCP server credential,
I WANT to store it once withmin secret,
SO THAT box specs reference it by identifier and its value never appears inminoutput, a spec, or a box.
Acceptance criteria:
min secret set <id>reads the value from the terminal or standard input, never from a command-line argument, stores it in the host keychain under the identifier, and prints the identifier only.- The stored item is readable by the proxy without a per-request prompt and by no other application without one; the consent happens once, at
set. min secret listshows each identifier, its store, the upstream a[secret-store-rules]rule registers for it or that none does, and whether the item is readable; never a value.min secret rm <id>removes the item; the next request that would inject it is refused.- Setting an identifier again replaces the value; the next request carries the new value with no restart of the proxy or the box.
L4. Out-of-set refusal
AS A developer,
I WANT the proxy to refuse any use of a sealed or referenced secret outside its declared authority,
SO THAT a compromised workload cannot redirect my credential.
Acceptance criteria:
- A request carrying a sealed GitHub value to a host outside the GitHub host set, or to an admitted host whose
Hostheader names another authority, is refused before it leaves the host and is audited. - A CONNECT to any authority outside the configured modules' host sets is refused (
off_module). - A request to a credentialed host whose egress the box does not declare is refused even with a valid sealed value.
L5. Stolen-value refusal
AS A developer,
I WANT a sealed value copied out of a box to be worthless anywhere else,
SO THAT exfiltration of the box environment yields nothing usable.
Acceptance criteria:
- A sealed value copied to the host shell and presented to
api.github.comdirectly, or through the proxy from the host, is refused. - A sealed value copied into another box on the same host (
own_ip) is refused by box attribution. - On
host_ipboxes the residual is cohort-granular use within the value's own mint-time bound, audited, until expiry or revocation; the spec records this as the T28 residual.
L6. min box spec shows the CA and the upstream set
AS A developer reviewing what a box may reach,
I WANT the expanded box spec to show the interception CA and the derived credentialed upstream set,
SO THAT interception is declared, not discovered.
Acceptance criteria:
min box specrenders the root CA fingerprint, the derived upstream set per grant, the steering mode, and every store reference with its registered authorities.- A box with
steering = "off"ormode = "none"shows no CA and, with a grant present, a validation warning or error respectively.
L7. Audit readable
AS A developer,
I WANT to read what the proxy admitted and refused,
SO THAT I can see what my agents did with my credentials.
Acceptance criteria:
- A local append-only log records each admit and refuse with the box, the upstream authority, the member or store identifier, the mapped resource where the module extracts one (
module_unmappedotherwise), and the decision; never the credential, header value or body. minoffers one command to read the log filtered by box.
L8. Revocation
AS A developer,
I WANT to revoke a session's credentials,
SO THAT a value already delivered stops working.
Acceptance criteria:
- Stopping or destroying a session stops redemption of its sealed values within 60 seconds.
minsigning out stops redemption of every outstanding sealed GitHub value on this host within 60 seconds.- Deleting or disabling a Keychain item stops its substitution on the next request.
Requirements not covered by a user story
- Production grade. Rust, shipped in the Minimal release under the same signing and diagnostics discipline as the daemons; secret values never reach
Debug, logs or support bundles; streamed upstream responses are never buffered. - Pure redemption core. The redemption decision (host binding, box attribution, authority in host set, egress re-check, member scope, expiry) is a pure function over owned values, separate from I/O, so the spec can put property tests on it.
- Process separation. The proxy holds the signing-CA key handle and store access; gvproxy holds forwarding state; they share no writable state.
- Delegation seam. With a Gatehouse configured,
minhands sign-in, minting and sealing to it and the proxy consumes the same envelope. Nothing here forecloses that.
Scope
≤ 2 weeks of agent walk. First slice (the demo): proxy_env steering, the GitHub v1 host set, sealed value minted by local min sign-in, redemption with every check, local audit, min box spec showing CA and upstream set. Later slices: Keychain resolver with min secret set | list | rm, dns steering (after NET's resolver), signing-CA rotation, revocation feed, PAC recipe, host_ip cohort handling.
Out of scope, with destinations:
- Caching public artifacts through the proxy: separate story.
- Anthropic and MCP upstream modules, 1Password and other store backends: follow-on epics reusing the module and resolver seams.
- The Gatehouse-hosted BEP, the enrolled
gatewayhelper, the tenant store: docs(spike): gvproxy v0.8.9 switch-attachment protocol for DM2 #512, gominimal/gatehouse#1, Gatehouse §6.10/§8.6. min secretagainst the Gatehouse tenant store (deposits): Gatehouse §8.6 and gominimal/arch open item 17; here the noun binds the host keychain only.- The execution-facade lane: a separate design.
- Box-zone DNS and
dnssteering's resolver: NET (docs(specs): add the box networking spec (NET) #1380). - The
[network]table and itsegress.*fields: NET (docs(specs): add the box networking spec (NET) #1380, NET-060, NET-066) owns them; slice 1 here implements themodeandegress.allow_dns_hostsacceptance it needs and records it as covering NET-060. The full schema is inbox#570's.
Prior art
docs/spec-credential-lanebranch (2026-08-14,crates/credlane): superseded in shape (a lane endpoint in the box) by D6; its policy gate, resolver seam and review surface are candidates for reuse. The spec states reuse / adapt / drop per unit.- The shipped
:7654hostname proxy and the gvproxy/minvmd egress filter and own-ip switch: what this proxy sits beside. - GHI-021 to GHI-029 (gominimal/gatehouse#1): the egress-proxy redemption requirements for the enrolled case. The local spec is their home for the un-enrolled case and cross-references their IDs.
Coding-agent guidance
Coding agents working this epic use the skills in gominimal/minimal-skills (minimal-setup, minimal-networking, minimal-config) for driving min, and this repository's AGENTS.md for build and test. Nouns: box, box spec, box host, box provider, BEP, EGW. Never "sandbox" in user-facing text. Never a plaintext credential in a box, a spec file, a patch or a bind mount.
References
- gominimal/arch:
architecture.mdD6, Deployment Styles → LocalVM, Box Spec[network],[network.bep],[secrets],[secret-store-rules], AT7, AT25; Gatehouse §6.10, §8.6, §12.11, T28, T31, T32; networking design §5.4, §7.1 (degraded-mode profile), §11, §12 item 14 - Contradiction filed: gominimal/arch#69 (un-enrolled node-local BEP and locally minted GitHub members)
- Command-tree notes filed: gominimal/arch#70 (
min box audit,min secret set | list | rmfor the host store) - docs(spike): gvproxy v0.8.9 switch-attachment protocol for DM2 #512 (GitHub identity epic), gominimal/gatehouse#1 (GHI spec), docs(specs): add the box networking spec (NET) #1380 (NET), chore: add free-disk-space steps to some github actions #586, build(deps): bump the github-actions group across 1 directory with 3 updates #587, chore(deps): fix pointer-deref advisory on crossbeam-epoch 0.9.19 #646
- Part of Unit 4: WireGuard mesh and remote browser access (UC7, UC2b) #494
Activity
norrietaylor commentedon Sep 18, 2026
Survey
Constraints
bepin it — T101min boxverbs later absorb the[session.network.bep]and[session.secrets]parsing and themin box specandmin box auditverbs this plan adds on today's create path; confirmed by handOpen questions
Slices
proxy_envsteering, the GitHub v1 host set (Gatehouse §6.10 Module host sets:github.com,api.github.com,uploads.github.com,codeload.github.com; pre-signed content hosts such asrelease-assets.githubusercontent.comstay ordinary egress) as the one module, a sealed reference to the device-flowmin auth loginsign-in, redemption with every check, the local audit log with its one-shotmin box audit <box>read, andmin box specshowing the CA and the upstream set. The demo box declaresgithub:user-token, the honest spelling of afullmember, so the slice also exercisesmin box spec'sfullmarker (BEP-038). Keychain references withmin secret,dnssteering, the browser sign-in flow,min box audit --followand--parent, audit segment rotation, signing-CA rotation, the PAC recipe andhost_ipcohort handling are later slices.min secret, reference it in a box spec, and the box's requests to its upstream carry it while the key never enters the box.min box auditreads every segment, andmin doctorreports an edited chain.sudo -n(T66 — Spawn gvproxy and the VMM child through sudo -n as their dedicated users #2566) and the EGW process (T38 — Run the egress gate and bep_host in an EGW process apart from the proxy #2285) go to gominimal/inbox#1184 with BEP-047; this slice keeps the design and the users it laid out.host_ipcohort sends no UDP 443, every store reference is checked at creation, and the CA deny set is the decided one. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-24 so the residuals dispatch as one slice.min auth login, and a GitHub grant box runsgh api userthrough the proxy with no extra step.min box auditshows the records the proxy wrote for a box and merges a parent's children with--parent, a relayed response carries no Alt-Svc header on the live path, a refused ambiguous request head shows inmin box audit, the logbep servewrites is the logmin box auditreads, the missing-proxy refusal, the sign-in refusal read as-o jsonand the proxy's credential refusals each name their cause and one next step, andbep --replace-keykeeps the old key when it cannot make a new one. The owner added these conditions from the close-out walk on 2026-10-31. Its tasks re-cover requirements whose origin tasks merged with the gap open; the owner accepted that double coverage on 2026-10-31 so the conditions dispatch as one slice.Spikes
SP1 — Do Claude Code and an MCP client send a sealed handle unmodified?
question: Does each intended client send the sealed handle as its bearer, unmodified, in the header the store rule names?
hypothesis: Claude Code with an OAuth token and one MCP client send
Authorization: Bearer <value>with no token-shape checkassumption: if false, the injection task adds a prefix-shaped handle or waits on the harness adapter, and its proof changes
method: run Claude Code and one MCP client against a local TLS-terminating stand-in that logs the request head, with an opaque handle in place of the token
time box: 3d
resolves: OQ2
executor: human
deliverable: docs/spikes/2026-10-20-sealed-handle-bearer-clients.md
Tasks
T1 — Create the bep crate with the GitHub module and the sealed envelope
slice: S0
kind: task
covers: BEP-006, BEP-027
size: M
est: 300 code, 160 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: an envelope that fails to open names the bound field that differed (box id, host, module, host-set version, mode or expiry) in the error the proxy logs, never the value
observability: a failed open names the bound field that differed, which the proxy logs per failed redemption, never the value
layers:
- crate:
- Cargo.toml: adds
crates/bepto the members- Cargo.lock: the new crate and its dependencies
- crates/bep/Cargo.toml: the crate, its seal dependencies and
proptestas a dev-dependency- crates/bep/src/lib.rs: module lines
- crates/bep/src/module.rs: the GitHub module's v1 host set at port 443, identifier, version and the fixed deny set
- crates/bep/src/seal.rs: the envelope of BEP-006, in a form GitHub never accepts as a token
test: cargo nextest run -p bep sealed_context_binds_box_host_module_version_expiry
test: cargo nextest run -p bep --run-ignored ignored-only sealed_value_rejected_by_github
produces:
- crates/bep/src/module.rs
- crates/bep/src/seal.rs
consumes: none
proof: A sealed member fails to open when any bound field differs, and
api.github.comrejects it as a credential.T2 — Hold the proxy's keys non-exportable in the host keychain and publish their public halves
slice: S0
kind: task
covers: BEP-014, BEP-059, BEP-060, BEP-072
size: L
est: 400 code, 420 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: when the keychain refuses a key,
beprefuses to start and prints "The keychain refused the proxy its key. Runbep --replace-key <role>to make a new one; boxes created before then must be re-created." on stderr, pinned by refused_key_names_the_replacement_commandobservability: one info log line at start naming each key role and its fingerprint, and one warn line per replacement naming the role
layers:
- keys:
- crates/bep/src/keys.rs: a key-store seam; the macOS Keychain backend holds the sealing, root and signing keys non-exportable
- crates/bep/src/publish.rs: publishes and parses by role the sealing public key, fingerprints and root (BEP-072)
- crates/bep/src/main.rs: the
bepbinary with--replace-key <role>, which runs without the files a serving proxy reads- crates/bep/src/lib.rs: module lines
- crates/bep/Cargo.toml:
security-frameworkon macOS- Cargo.lock: the keychain dependency
test: cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file
test: cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable
test: cargo nextest run -p bep a_value_sealed_to_the_published_identity_unseals_under_the_keys
test: cargo nextest run -p bep a_published_identity_carrying_no_key_is_refused_by_role
test: cargo nextest run -p bep keys_never_regenerated_on_restart
test: cargo nextest run -p bep key_replacement_kills_outstanding_members
test: cargo nextest run -p bep refused_key_names_the_replacement_command
produces:
- crates/bep/src/keys.rs
- crates/bep/src/publish.rs
- crates/bep/src/main.rs (entry point)
consumes:
- crates/bep/src/seal.rs
proof: The proxy's keys survive a restart and appear in no file, and replacing the sealing key refuses every member sealed to the old one.
depends on:
- blocked by T1
- assumes OQ4
T3 — Issue leaf certificates under a name-constrained root and a keychain-held signing CA
slice: S0
kind: task
covers: BEP-013
size: M
est: 350 code, 140 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a leaf request for a name outside the union is refused with a log line naming the name and the union it fell outside
observability: one debug log line per leaf issued with the hostname, and one info line per root re-issue naming the changed constraints
layers:
- ca:
- crates/bep/src/ca.rs: the three-layer CA of BEP-013: constrained self-signed root, keychain signing CA, leaves for the union alone, re-issue under the same key
- crates/bep/src/lib.rs: module line
- crates/bep/Cargo.toml: the certificate dependency
- Cargo.lock: the certificate dependency
test: cargo nextest run -p bep prop_root_constraints_equal_registered_union
test: cargo nextest run -p bep root_recertified_under_same_key_and_subject
produces:
- crates/bep/src/ca.rs
consumes:
- crates/bep/src/keys.rs
proof: A leaf for each GitHub host validates against the root alone, and a leaf for any other name does not.
depends on:
- blocked by T2
T4 — Decide redemption as one pure function over host key, attribution and member bounds
slice: S0
kind: task
covers: BEP-019, BEP-020, BEP-058
size: L
est: 300 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: every refusal names its first failing check in the audit record's marker, which
min box audit <box>showsobservability: the connection span and the audit marker name a refusal's first failing check
layers:
- decide:
- crates/bep/src/redeem.rs: the pure decision over owned values, apart from the TLS shell; the BEP-019, BEP-020 and BEP-058 checks
- crates/bep/src/lib.rs: module line
- scripts/kani.sh: expects the bep crate's harnesses
test: cargo nextest run -p bep prop_off_host_sealed_value_is_refused
test: cargo nextest run -p bep prop_cross_box_sealed_value_is_refused
test: cargo nextest run -p bep prop_member_without_bounds_is_refused
test (deferred: kani): cargo nextest run -p bep kani_redeem_refuses_undecryptable
test (deferred: kani): cargo nextest run -p bep kani_redeem_refuses_misattributed
test (deferred: kani): cargo nextest run -p bep kani_redeem_refuses_unbounded_member
produces:
- crates/bep/src/redeem.rs
consumes:
- crates/bep/src/seal.rs
proof: A value sealed to another host, sent from another own-address box, or missing its bounds is refused.
depends on:
- blocked by T1
T5 — Refuse authorities outside both host sets, off-port connections, and expired or revoked values
slice: S0
kind: task
covers: BEP-021, BEP-024, BEP-031
size: L
est: 150 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: every refusal names its first failing check in the audit record's marker, which
min box audit <box>showsobservability: the connection span and the audit marker name a refusal's first failing check
layers:
- decide:
- crates/bep/src/redeem.rs: the BEP-021, BEP-024 and BEP-031 checks
- scripts/kani.sh: the added harnesses' count
test: cargo nextest run -p bep prop_authority_outside_bound_or_current_set_is_refused
test: cargo nextest run -p bep prop_expired_or_revoked_value_is_refused
test: cargo nextest run -p bep prop_off_port_connection_is_refused
test (deferred: kani): cargo nextest run -p bep kani_redeem_requires_authority_in_both_sets
test (deferred: kani): cargo nextest run -p bep kani_redeem_refuses_expired_or_revoked
test (deferred: kani): cargo nextest run -p bep kani_redeem_enforces_port_discipline
produces: none
consumes:
- crates/bep/src/redeem.rs
proof: A request outside either host set, on another port, or with an expired or revoked value is refused.
depends on:
- blocked by T4
T6 — Pin the request authority, re-check the box's egress, and admit only when every check passes
slice: S0
kind: task
covers: BEP-022, BEP-023, BEP-025
size: L
est: 150 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: every refusal names its first failing check in the audit record's marker, which
min box audit <box>showsobservability: the connection span and the audit marker name a refusal's first failing check
layers:
- decide:
- crates/bep/src/redeem.rs: the BEP-022 and BEP-023 checks, and Admit iff every check passes, else Refuse naming the first failing one
- scripts/kani.sh: the added harnesses' count
test: cargo nextest run -p bep prop_undeclared_egress_authority_is_refused
test: cargo nextest run -p bep prop_host_header_mismatch_is_refused
test: cargo nextest run -p bep prop_admit_iff_every_check_passes
test (deferred: kani): cargo nextest run -p bep kani_redeem_requires_egress_admission
test (deferred: kani): cargo nextest run -p bep kani_redeem_pins_request_authority
test (deferred: kani): cargo nextest run -p bep kani_redeem_admits_iff_all_checks_pass
produces: none
consumes: none
proof: A mismatched Host or an egress-denied authority is refused, and the decision admits only when every check passes.
depends on:
- blocked by T5
T7 — Verify signed handles against the client's registered key and the current rule
slice: S0
kind: task
covers: BEP-064
size: L
est: 300 code, 240 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a refused handle is audited
store_handle_invalid, andmin box audit <box>shows the marker with the store and identifierobservability: one debug log line per handle checked with the store, the identifier and the outcome, never the signature
layers:
- handle:
- crates/bep/src/handle.rs: the store-handle wire format, the client-key registry, and the BEP-064 check against the rule read per request
- crates/bep/src/redeem.rs: the handle check stands where the module checks read a host set, mode and breadth
- crates/bep/src/lib.rs: module line
- scripts/kani.sh: the added harness's count
test: cargo nextest run -p bep prop_store_handle_verified_against_current_rule
test: cargo nextest run -p bep edited_store_rule_applies_to_next_request
test: cargo nextest run -p bep sign_in_reference_is_judged_against_the_module
test (deferred: kani): cargo nextest run -p bep kani_redeem_refuses_invalid_store_handle
produces:
- crates/bep/src/handle.rs
consumes: none
proof: A handle with a bad signature, past expiry, or wider than its rule is refused, and an edited rule decides the next request.
depends on:
- blocked by T6
T8 — Parse requests strictly and forward a canonical head over HTTP/1.1
slice: S0
kind: task
covers: BEP-074, BEP-076, BEP-077
size: L
est: 450 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: a refused head is audited
framing_ambiguous, andmin box audit <box>shows the markerobservability: one debug log line per closed connection naming the framing rule that closed it
layers:
- http:
- crates/bep/src/http.rs: the strict HTTP/1.1 parser and canonical re-serializer of BEP-077, with BEP-074's close and h2c strip and BEP-076's Alt-Svc strip
- crates/bep/src/upstream.rs: upstream connections keyed per box, never shared across boxes
- crates/bep/src/lib.rs: module lines
test: cargo nextest run -p bep h2_prior_knowledge_preface_is_closed
test: cargo nextest run -p bep h2c_upgrade_is_stripped
test: cargo nextest run -p bep alt_svc_is_stripped_from_responses
test: cargo nextest run -p bep prop_forwarded_head_is_canonical
test: cargo nextest run -p bep prop_ambiguous_framing_is_refused
test: cargo nextest run -p bep upstream_connections_never_shared_across_boxes
produces:
- crates/bep/src/http.rs
- crates/bep/src/upstream.rs
consumes: none
proof: Ambiguous framing or the h2 preface closes the connection, and every forwarded head is canonical.
depends on:
- blocked by T1
T9 — Append hash-chained audit records the proxy alone writes
slice: S0
kind: task
covers: BEP-039, BEP-040, BEP-041, BEP-048
size: L
est: 300 code, 240 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the log is plain JSONL under the proxy's state directory, one record per decision, readable with
min box audit <box>once that task landsobservability: the audit log is the record; one warn log line if an append fails, naming the path
layers:
- audit:
- crates/bep/src/audit.rs:
min/v1JSONL records of BEP-039 built from the decision alone, chained per BEP-048, opened append-only- crates/bep/src/lib.rs: module line
test: cargo nextest run -p bep every_decision_appends_one_audit_record
test: cargo nextest run -p bep prop_audit_records_never_contain_secrets
test: cargo nextest run -p bep audit_log_is_opened_append_only
test: cargo nextest run -p bep audit_record_carries_previous_hash
produces:
- crates/bep/src/audit.rs
consumes: none
proof: Every decision appends one chained record naming the box and the authority, never a credential or a body.
depends on:
- blocked by T1
T10 — Accept only box connections and terminate TLS for the module's hosts over HTTP/1.1
slice: S0
kind: task
covers: BEP-026, BEP-029, BEP-073, BEP-075
size: L
est: 450 code, 240 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: a connection refused at the listener is logged with its reason and peer, and a request outside the modules is audited
off_module, whichmin box audit <box>showsobservability: one span per connection with the box id, the authority and the outcome; one warn log line per listener refusal with its reason
layers:
- listen:
- crates/bep/src/listener.rs: the acceptor for the switch peer's deliveries: per-boot token, header version and box id; BEP-026, BEP-029 and BEP-075
- crates/bep/src/proxy.rs: terminates the box's TLS with a leaf for the admitted name, offering
http/1.1alone in ALPN- crates/bep/src/control.rs: the control socket, separate from the listener, taking client key registrations into the handle registry
- crates/bep/src/main.rs:
bep serve- crates/bep/src/lib.rs: module lines
- crates/bep/Cargo.toml: the TLS and async dependencies
- Cargo.lock: the TLS dependencies
test: cargo nextest run -p bep host_shell_connection_is_refused
test: cargo nextest run -p bep request_outside_modules_is_refused_off_module
test: cargo nextest run -p bep proxy_offers_http11_only_on_both_legs
test: cargo nextest run -p bep connect_and_absolute_form_accepted_over_http11_only
produces:
- crates/bep/src/proxy.rs
- crates/bep/src/control.rs
consumes:
- crates/bep/src/ca.rs
- crates/bep/src/http.rs
- crates/bep/src/audit.rs
- crates/bep/src/handle.rs
proof: A host-shell connection is refused at the listener, and a box's CONNECT to a GitHub host completes TLS offering HTTP/1.1 alone.
depends on:
- blocked by T3
- blocked by T7
- blocked by T8
- blocked by T9
T11 — Validate the upstream before substituting, pass unsealed requests through, and stream responses
slice: S0
kind: task
covers: BEP-030, BEP-055, BEP-N01
size: L
est: 400 code, 300 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: a refused upstream is audited
upstream_tls_invalidorupstream_not_tls, and a passthrough carrying its own credentialforeign_credential, each shown bymin box audit <box>observability: the connection span gains the upstream's validation outcome and the substitution decision, never the credential
layers:
- forward:
- crates/bep/src/forward.rs: upstream TLS validated against the host store before forwarding (BEP-055), substitution on admit, passthrough (BEP-030), chunked relay (BEP-N01)
- crates/bep/src/proxy.rs: hands each parsed request to the decision and the forwarder
- crates/bep/src/lib.rs: module line
test: cargo nextest run -p bep unsealed_request_passes_through_and_is_audited
test: cargo nextest run -p bep prop_upstream_tls_validated_before_substitution
test: cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited
test: cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused
test: cargo nextest run -p bep streamed_response_is_not_buffered
produces:
- crates/bep/src/forward.rs
consumes:
- crates/bep/src/proxy.rs
- crates/bep/src/upstream.rs
proof: An upstream that fails validation receives nothing, an unsealed request passes unchanged, and responses stream.
depends on:
- blocked by T8
- blocked by T10
T12 — Sign in to GitHub with min auth and keep the sign-in in one keychain item
slice: S0
kind: task
covers: BEP-001, BEP-002, BEP-004
size: L
est: 410 code, 480 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min auth statusprints "Signed in to GitHub as . The token expires at ." or, with expired token and live refresh material, "Signed in to GitHub as . The token renews at the proxy on the next request that uses it.", never a token, pinned by auth_status_reports_expiry_without_tokenobservability: one info log line per sign-in naming the account, the flow and whether the instance is new
layers:
- cli:
- crates/minimal/src/cli.rs:
min auth login [--device],min auth status- crates/minimal/src/cmd/mod.rs: dispatch and module line
- crates/minimal/src/cmd/auth.rs: device and PKCE browser flows under the App (client id
Iv23liv6KetAFJp4Eb5K, callbackhttp://127.0.0.1/callback, any port), browser by default; the secret isoption_env!("MINIMAL_GITHUB_APP_CLIENT_SECRET"), and a build without it takes the device flow; status from the item's metadata- crates/minimal/Cargo.toml: the bep crate
- Cargo.lock: the new dependency edge
test: cargo nextest run -p minimal auth_login_completes_device_flow_and_reports_account
test: cargo nextest run -p minimal auth_login_completes_browser_pkce_flow
test: cargo nextest run -p minimal auth_login_without_built_in_secret_takes_device_flow
test: cargo nextest run -p minimal auth_status_reports_expiry_without_token
test: cargo nextest run -p minimal auth_status_reports_renewal_of_expired_sign_in
- item:
- crates/bep/src/signin.rs: the one sign-in keychain item and its instance identifier (BEP-002)
- crates/bep/src/lib.rs: module line
test: cargo nextest run -p minimal auth_login_stores_material_in_keychain_only
test: cargo nextest run -p minimal a_sign_in_with_no_held_item_is_a_new_instance
test: cargo nextest run -p minimal re_login_to_the_same_account_keeps_the_instance
produces:
- crates/bep/src/signin.rs
consumes: none
proof:
min auth logincompletes the device flow and names the account, andmin auth statusshows the expiry and no token.depends on:
- blocked by T1
T13 — Refuse GitHub grants at activation that the box or the un-enrolled host cannot honour
slice: S0
kind: task
covers: BEP-008, BEP-009, BEP-010, BEP-016, BEP-017, BEP-056
size: L
est: 400 code, 480 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: a refused activation exits 3 and prints, for a missing host, "This box declares a GitHub grant, but its egress does not allow: . Add them to egress.allow_dns_hosts." on stderr, pinned by prop_grant_host_set_outside_egress_is_exit_3
observability: one debug log line per activation refusal naming the rule and the fields
layers:
- spec:
- crates/boxes/src/core/bep.rs:
steering(defaultdns),proxy_env,no_proxy,quic443and the GitHub grants; the BEP-008, 009, 010, 017 and 056 refusals- crates/boxes/src/core/mod.rs: module line
- crates/boxes/src/lib.rs:
CredentialedUpstreamcarries the settings, defaulting so an empty declaration reads as before- crates/boxes/Cargo.toml:
proptestas a dev-dependency- Cargo.lock: the dev-dependency edge
test: cargo nextest run -p boxes prop_grant_host_set_outside_egress_is_exit_3
test: cargo nextest run -p boxes prop_none_mode_with_grant_is_exit_3
test: cargo nextest run -p boxes steering_off_with_grant_warns_and_injects_no_ca
test: cargo nextest run -p boxes steering_off_with_proxy_env_is_refused
test: cargo nextest run -p boxes steering_defaults_to_dns
test: cargo nextest run -p boxes prop_dns_steering_without_resolver_is_exit_3
test: cargo nextest run -p boxes prop_installation_mode_unenrolled_is_exit_3
- read:
- crates/mfile/src/lib.rs: reads
[session.network.bep]and[session.secrets]from the project's[session]table- crates/minimal/src/cmd/session.rs: validates before the create, passing the module's host set
- crates/minimal/src/main.rs: maps the refusal to exit 3
test: cargo nextest run -p minimal activation_refuses_grant_before_create_with_exit_3
produces:
- crates/boxes/src/core/bep.rs
consumes:
- crates/bep/src/module.rs
proof: Activating a grant box whose egress omits a GitHub host exits 3 naming each missing host.
depends on:
- blocked by T1
T14 — Mint a sealed sign-in reference for each GitHub grant and register the box with the proxy
slice: S0
kind: task
covers: BEP-003, BEP-005
size: L
est: 360 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: creating a grant box with no sign-in held exits with code
github_sign_in_requiredand prints "This box declares a GitHub grant, and no GitHub sign-in is held on this machine. Runmin auth login, then create the box again." on stderr, pinned by creation_without_sign_in_fails_with_defined_errorobservability: one info log line per mint naming the box id, the module and the member's expiry, never the handle or the sealed value; one debug line per
register_boxnaming the box id and the count of its declared authoritieslayers:
- publish:
- crates/bep/src/publish.rs: the published identity carries the root's validity end (
root_not_after), the root key's creation time plus the fixed default validity in one constant (OQ3), until gominimal/arch#85 rules otherwisetest: cargo nextest run -p bep published_identity_carries_the_root_validity_end
- mint:
- crates/minimal/src/mint.rs: the BEP-005 sign-in reference under the client's key, registered over the control socket, sealed from the published identity, with the member's expiry read from the identity's
root_not_after- crates/minimal/src/lib.rs: module line
- crates/minimal/src/tests.rs: the mint and registration tests
- crates/minimal/src/cmd/boxes/create.rs: refuses creation with no sign-in held and never prompts; mints and seals per grant; sends
register_boxover the proxy's control socket with the box id, its declared egress and itsown_iporhost_ipstance (T41)- crates/bep/src/main.rs: drops the box-egress interim sentence from the
bep servestartup warning and thewirecomment, the create half of that fliptest: cargo nextest run -p minimal local_member_is_a_sign_in_reference_expiring_with_the_root
test: cargo nextest run -p minimal grant_box_registers_its_declared_egress_with_the_proxy
- refuse:
- crates/boxes/src/core/bep.rs: the defined error
github_sign_in_requiredtest: cargo nextest run -p boxes creation_without_sign_in_fails_with_defined_error
- e2e:
- scripts/session-e2e.sh: adds the case
test: ./scripts/session-e2e.sh bep_second_box_needs_no_sign_in
produces:
- crates/minimal/src/mint.rs
consumes:
- crates/bep/src/signin.rs
- crates/bep/src/handle.rs
- crates/bep/src/publish.rs
- crates/bep/src/control.rs
- crates/boxes/src/core/bep.rs
proof: A grant box is created with no prompt, each grant's sealed value is minted under the client's key with the root's expiry, and the proxy holds the box's declared egress.
depends on:
- blocked by T2
- blocked by T7
- blocked by T10
- blocked by T12
- blocked by T13
- blocked by T41
- assumes OQ3
T42 — Deliver each sealed value into its grant's environment variable in the box
slice: S0
kind: task
covers: BEP-007
size: L
est: 260 code, 260 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box spec <box>shows each grant's variable name and never its value; a box whose create request carries no sealed value for a declared grant is refused at creation with exit 3 naming the grantobservability: one debug log line per delivered variable naming the box id and the variable, never the value
layers:
- carry:
- crates/minimald-rpc/src/lib.rs:
BoxConfigcarries the sealed deliveries and the root certificate, and the shared create-request constructor takes them- crates/minimal/src/cmd/boxes/create.rs: the
MintGrantsstep hands eachMinted.sealedto the create request as its grant's delivery in place ofdrop(minted)(arch C2 on #2490, residual filed here)- crates/minimald-rpc/src/tests.rs: the new fields in its full literals
- crates/minimald/src/box_manager.rs:
record_from_configcopies the deliveries into the box record- crates/boxes/src/lib.rs:
Recordholds the deliveries- crates/minimald/src/box_manager/tests.rs: the new fields in its full literals
test: cargo nextest run -p minimald sealed_deliveries_ride_the_record_to_the_host_params
- place:
- crates/minimald/src/box_actor/host.rs: builds
HostParamswith the deliveries- crates/minimald/src/box_runtime.rs: places each sealed value in its grant's environment variable and no plaintext
- crates/minimald/src/box_runtime/tests.rs: the placement test
test: cargo nextest run -p minimald sealed_value_is_placed_in_its_grant_variable_only
- e2e:
- scripts/session-e2e.sh: adds the case
test: ./scripts/session-e2e.sh bep_box_env_holds_sealed_value_only
produces: none
consumes:
- crates/minimal/src/mint.rs
- crates/minimald-rpc/src/lib.rs
proof: A second grant box starts with no prompt and holds only a sealed value in its grant's variable.
depends on:
- blocked by T14
T43 — Issue and publish the un-enrolled host root certificate and hand it to each grant box
slice: S0
kind: task
covers: BEP-072, BEP-013
size: L
est: 240 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box spec <box>shows the root certificate's SHA-256 fingerprint under the labelroot CA certificatein place of the key thumbprint, pinned by box_spec_shows_the_root_certificate_fingerprint; abepstart whose held root key cannot sign refuses with a message namingbep --replace-key rootobservability: one info log line at
bepstart naming the root certificate's fingerprint and the count of its permitted names, never a keylayers:
- issue:
- crates/bep/src/keys.rs:
CaKeyfor the keychain-held key throughSecKey::create_signature(ECDSA P-256, SHA-256), so the root and signing CA keys issue in production (#2440)- crates/bep/src/ca.rs: the root certificate is issued from the held root key at
bepstart with the permitted names of the configured modules less the deny set, and re-issued under the same key and subject when the union changes; the store rules join the union when T25 wires themtest: cargo nextest run -p bep root_certificate_is_issued_from_the_held_root_key
test: cargo nextest run -p bep keychain_root_key_signs_the_root_certificate
- publish:
- crates/bep/src/publish.rs:
Identitycarriesroot_certificate, the PEM of the current root certificate, beside the root key (BEP-072); a start or a root replacement publishes the certificate it issued- crates/bep/src/main.rs:
bepstart andbep --replace-key rootissue and publish the root certificate;bep servekeeps the issued root as the CA's anchortest: cargo nextest run -p bep published_identity_carries_the_root_certificate
test: cargo nextest run -p bep root_replacement_publishes_a_new_root_certificate
- hand:
- crates/minimal/src/cmd/boxes/create.rs: the
MintGrantsstep puts the identity'sroot_certificateon the create request for a grant box, and refuses a grant box whose identity publishes none with a message namingbep- crates/minimal/src/cmd/box_spec.rs: shows the root certificate's fingerprint (#2446)
- crates/minimal/src/tests.rs: the create-request and spec tests
test: cargo nextest run -p minimal grant_box_create_request_carries_the_published_root_certificate
test: cargo nextest run -p minimal box_spec_shows_the_root_certificate_fingerprint
produces: none
consumes:
- crates/bep/src/ca.rs
- crates/bep/src/keys.rs
- crates/bep/src/publish.rs
- crates/minimald-rpc/src/lib.rs
proof: A started proxy publishes a self-signed, name-constrained root certificate issued from its held root key, a grant box's create request carries it, and
min box specshows its fingerprint.depends on:
- blocked by T2
- blocked by T3
- blocked by T42
T15 — Show the root fingerprint, the upstream set, steering and the full marker in min box spec
slice: S0
kind: task
covers: BEP-038, BEP-057
size: L
est: 400 code, 360 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box specprintsroot CA <sha256 fingerprint>, onegrant github:user-token fullrow per grant with its upstream hosts,steering proxy_env, and, where the stance is allowed on a grant box,quic443 allowed (HTTP/3 bypasses the proxy), pinned by box_spec_renders_ca_upstreams_steering_and_referencesobservability: one debug log line per render naming the box and whether the published root was found
layers:
- cli:
- crates/minimal/src/cli.rs: the
min boxnoun withspec- crates/minimal/src/cmd/mod.rs: dispatch and module line
- crates/minimal/src/cmd/box_spec.rs: the BEP-038 rows: root fingerprint, upstreams with the
fullmarker, steering, store references, the QUIC flag- crates/minimal/src/config.rs: reads
[secrets] acknowledge_full_breadth_unenrolledfrom the user and organization client configurationtest: cargo nextest run -p minimal box_spec_renders_ca_upstreams_steering_and_references
test: cargo nextest run -p minimal box_spec_marks_full_breadth_member
test: cargo nextest run -p minimal box_spec_flags_allowed_quic_stance_on_grant_box
test: cargo nextest run -p minimal acknowledged_narrow_grant_renders_full
- spec:
- crates/boxes/src/core/bep.rs: the BEP-057 refusal and the
quic443stance- crates/mfile/src/lib.rs: ignores a project's acknowledgement with a warning
test: cargo nextest run -p boxes prop_narrow_scopes_unenrolled_need_acknowledgement
test: cargo nextest run -p boxes project_acknowledgement_is_ignored_with_warning
produces:
- crates/minimal/src/cmd/box_spec.rs (entry point)
consumes:
- crates/boxes/src/core/bep.rs
- crates/bep/src/publish.rs
proof:
min box specon the demo box shows the root fingerprint, the four GitHub hosts marked full, and proxy_env steering.depends on:
- blocked by T2
- blocked by T13
T16 — Put the interception root in the box's trust store
slice: S0
kind: task
covers: BEP-011
size: L
est: 330 code, 420 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a box spec that sets one of the three trust variables itself keeps its value and activation prints "This box sets itself, so tools that read it will not trust the proxy's certificate." on stderr, pinned by spec_trust_env_shadows_platform_with_warning
observability: one debug log line per box naming whether the root was injected
layers:
- carry:
- crates/boxes/src/lib.rs:
Record.root_certificate, the proxy's root certificate PEM besidesealed_deliveries, so every host launch of the box reads it, after a daemon restart included (stale layers found by the coder on 2026-10-22; the record fixtures are struct-literal additions)- crates/minimald/src/box_manager.rs: the record build copies
config.root_certificatefrom the create request- crates/minimald/src/box_manager/tests.rs: the record test
- crates/minimald/src/box_runtime/launcher.rs: the launch hands the record's root certificate to
EnvArgs, soEnv::buildwrites the box's bundle copy and hashed directorytest: cargo nextest run -p minimald record_carries_the_root_certificate_to_the_launch
- trust:
- crates/minimald/src/env.rs: the root in the box's own bundle copy and hashed directory, per BEP-011, never the package store's copy
- crates/minimald/src/box_runtime.rs: the trust variables of BEP-011
- crates/minimald/src/box_runtime/tests.rs: the trust-variable tests
- scripts/session-e2e.sh: adds three cases
test: ./scripts/session-e2e.sh bep_root_ca_present_in_box_trust_store
test: ./scripts/session-e2e.sh bep_root_ca_absent_from_box_without_credentialed_upstream
test: ./scripts/session-e2e.sh bep_trust_env_points_at_bundle_and_root
- spec:
- crates/boxes/src/core/bep.rs: the warning for a spec that sets a trust variable
test: cargo nextest run -p boxes spec_trust_env_shadows_platform_with_warning
produces: none
consumes:
- crates/minimald-rpc/src/lib.rs
- crates/boxes/src/core/bep.rs
proof: A grant box has the root in both trust-store forms, and a box with no grant has none.
depends on:
- blocked by T13
- blocked by T14
- blocked by T42
T40 — Point the box at the proxy
slice: S0
kind: task
covers: BEP-012
size: M
est: 60 code, 200 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box spec <box>shows the box's steering, which decides whether the proxy variables are setobservability: one debug log line per box naming the proxy variables set
layers:
- env:
- crates/minimald/src/box_runtime.rs:
HTTPS_PROXY,HTTP_PROXY,NO_PROXYandNODE_USE_ENV_PROXYunderproxy_envorboth- scripts/session-e2e.sh: adds two cases
test: ./scripts/session-e2e.sh bep_proxy_env_and_no_proxy_are_set
test: ./scripts/session-e2e.sh bep_proxy_env_sets_node_use_env_proxy
produces: none
consumes:
- crates/boxes/src/core/bep.rs
proof: A
proxy_envbox has the proxy address, the no-proxy list andNODE_USE_ENV_PROXY=1in its environment.depends on:
- blocked by T13
- blocked by T14
- blocked by T42
T17 — Read one box's audit trail with min box audit
slice: S0
kind: task
covers: BEP-042
size: L
est: 300 code, 300 tests
executor: agent
difficulty: routine
model: standard
diagnostics: inside a box,
min box audit selfprints "Reading your own audit trail from inside a box needs a Gatehouse. On the host, runmin box audit <box>." on stderr and exits non-zero, pinned by box_audit_self_unenrolled_refused_with_error; a name prints "Showing (box )." before the recordsobservability: one debug log line per read naming the box id and the segments read
layers:
- cli:
- crates/minimal/src/cli.rs:
min box audit <box|self> | --parent <box|self> [-o jsonl] [--follow]- crates/minimal/src/cmd/mod.rs: dispatch and module line
- crates/minimal/src/cmd/box_audit.rs: the BEP-042 grammar: one box id, a name,
--follow,--parent,selfrefused- crates/bep/src/audit.rs: a reader over the log for the client
test: cargo nextest run -p minimal box_audit_filters_to_one_box
test: cargo nextest run -p minimal box_audit_name_reads_live_else_latest_creation
test: cargo nextest run -p minimal box_audit_follow_replays_then_tails
test: cargo nextest run -p minimal box_audit_parent_merges_children
test: cargo nextest run -p minimal box_audit_self_unenrolled_refused_with_error
produces:
- crates/minimal/src/cmd/box_audit.rs (entry point)
consumes:
- crates/bep/src/audit.rs
proof:
min box audit weblists only that box's decisions and names the id it read.depends on:
- blocked by T9
- blocked by T15
T18 — Run the proxy beside gvproxy so git and gh reach GitHub from a box
slice: S0
kind: task
covers: BEP-015
size: L
est: 280 code, 420 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the support bundle carries the proxy's log tail and the audit log segments, collected beside the VM host daemon's log
observability: one info log line when the VM host daemon starts the proxy, naming its socket and pid, and one warn line when it exits
layers:
- host:
- crates/minvmd/src/cmd/run.rs: starts
bep servebeside gvproxy with the per-boot token in production, the stub staying test-onlytest: cargo nextest run -p minvmd proxy_is_started_with_the_per_boot_token
- ship:
- justfile: builds and signs the bep binary beside minvmd
- crates/minimal/src/diag/collect.rs: collects the proxy log and the audit segments
test: cargo nextest run -p minimal bundle_collects_the_proxy_log_and_audit_segments
- e2e:
- scripts/session-e2e.sh: adds the case; the T14 leg
bep_second_box_needs_no_sign_inrecords NOT RUN on every lane until this task startsbep serve(do not change that leg to pass without a proxy); the T16 grant legsbep_root_ca_present_in_box_trust_store, the grant half ofbep_root_ca_absent_from_box_without_credentialed_upstreamandbep_trust_env_points_at_bundle_and_rootrecord NOT RUN off macOS the same way; the macOS passes against the live proxy that #2490 (arch C1) and #2495 (arch C2) filed here moved to #2500 (arch C3 on #2497)test: ./scripts/session-e2e.sh bep_git_and_gh_against_private_repo
test: ./scripts/session-e2e.sh bep_second_box_needs_no_sign_in
test: ./scripts/session-e2e.sh bep_box_env_holds_sealed_value_only
produces: none
consumes:
- crates/bep/src/main.rs
- crates/bep/src/forward.rs
proof: From a box holding only a sealed member, git clone, git push and gh api succeed against a private repository.
depends on:
- blocked by T2
- blocked by T11
- blocked by T14
- blocked by T16
- blocked by T40
- blocked by T41
- blocked by T42
T19 — Clone, push and gh from a box with no token in it, end to end
slice: S0
kind: integration
covers: none
size: M
est: 120 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows every admit of the run and the refusals of the copied valueobservability: one connection span per request of the run, with the box id, the authority and the outcome
layers:
- e2e:
- scripts/session-e2e.sh: one case: sign in, create the demo box, read
min box spec, clone and push, see the copied value refused on the host and in a second box, readmin box audittest: ./scripts/session-e2e.sh bep_first_slice_end_to_end
test: ./scripts/session-e2e.sh bep_git_and_gh_against_private_repo
produces: none
consumes: none
proof:
proxy_envsteering, the GitHub v1 host set (Gatehouse §6.10 Module host sets:github.com,api.github.com,uploads.github.com,codeload.github.com; pre-signed content hosts such asrelease-assets.githubusercontent.comstay ordinary egress) as the one module, a sealed reference to the device-flowmin auth loginsign-in, redemption with every check, the local audit log with its one-shotmin box audit <box>read, andmin box specshowing the CA and the upstream set. The demo box declaresgithub:user-token, the honest spelling of afullmember, so the slice also exercisesmin box spec'sfullmarker (BEP-038). Keychain references withmin secret,dnssteering, the browser sign-in flow,min box audit --followand--parent, audit segment rotation, signing-CA rotation, the PAC recipe andhost_ipcohort handling are later slices.depends on:
- blocked by T1
- blocked by T2
- blocked by T3
- blocked by T4
- blocked by T5
- blocked by T6
- blocked by T7
- blocked by T8
- blocked by T9
- blocked by T10
- blocked by T11
- blocked by T12
- blocked by T13
- blocked by T14
- blocked by T15
- blocked by T16
- blocked by T17
- blocked by T18
- blocked by T40
- blocked by T41
- blocked by T42
- blocked by T43
T41 — Wire the redemption decision and each box's declared egress into bep serve
slice: S0
kind: task
covers: BEP-019, BEP-020, BEP-022, BEP-025
size: L
est: 400 code, 400 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box audit <box>shows each redemption's admit with the member identifier and each refusal with its check marker (undecryptable,cross_box,outside_declared_egress,host_mismatch), never the credential; a box whose declared egress the proxy has not registered is refusedoutside_declared_egressobservability: one info log line when
bep serveinstalls the redeemer naming the registry path, and one debug line per registered box naming its id and the count of its declared authorities, never a credentiallayers:
- registry:
- crates/bep/src/boxes.rs: per box id, the declared egress authorities and the
own_iporhost_ipstance, registered over the control socket at creation and removed at destroy- crates/bep/src/control.rs: a
RegisterBoxrequest carrying the box id, its declared egress and its stance, and aForgetBoxrequest- crates/bep/src/lib.rs: module line
test: cargo nextest run -p bep registered_box_egress_is_read_per_request
test: cargo nextest run -p bep forgotten_box_is_refused_no_live_box
- decide:
- crates/bep/src/redeemer.rs: the
forward::Redeemeroverredeem::decide: opens the sealed value under the proxy key, verifies the handle against the registered client keys, resolves a sign-in reference to the member's credential from the Keychain item, takes attribution and the declared egress from the registry, and passes the revocations in force- crates/bep/src/forward.rs:
Unwiredstays the test default; the serving proxy installs the real redeemertest: cargo nextest run -p bep sealed_member_is_redeemed_within_declared_egress
test: cargo nextest run -p bep cross_box_value_is_refused_cross_box
test: cargo nextest run -p bep prop_serve_redeems_iff_decide_admits
- serve:
- crates/bep/src/main.rs:
bep serveinstalls the redeemer throughProxy::with_redeemerand gives the proxy the control registry; theNoSigningKeydoc and the startupwarn!say that #2440 alone gates leaf signing now that the decision is wiredtest: cargo nextest run -p bep serve_installs_the_redeemer_and_registry
produces: none
consumes:
- crates/bep/src/redeem.rs
- crates/bep/src/forward.rs
- crates/bep/src/handle.rs
- crates/bep/src/signin.rs
- crates/bep/src/seal.rs
- crates/bep/src/control.rs
proof: A request carrying a sealed member from a registered box inside its declared egress reaches the upstream with the credential in place, and the same request from another box, or to a host the box did not declare, is refused before anything goes upstream.
depends on:
- blocked by T1
- blocked by T4
- blocked by T7
- blocked by T10
- blocked by T11
- blocked by T12
T20 — Record a revocation when a live box is destroyed and refuse its values within a minute
slice: S1
kind: task
covers: BEP-043, BEP-067, BEP-071
size: L
est: 380 code, 440 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box audit <box>shows the box'srevocationrecord and each later refusal of its valuesobservability: one info log line per revocation recorded, naming the box id; one warn line when a destroy completes with the proxy unreachable
layers:
- proxy:
- crates/bep/src/revoke.rs: revocations by box id, consulted before any resolution (BEP-071); its module line goes in the crate root beside the other modules
- crates/bep/src/control.rs: takes mint and revocation submissions and appends them to the one log
- crates/bep/src/proxy.rs:
AuditWriter::try_writereports an append failure torevoke_box, so a lost revocation record is logged as an error and never silent (widened 2026-10-25 per review finding 4369863226 on #2548)- crates/bep/src/redeemer.rs: the interim doc comments say that
revoke_boxon the control socket puts a revocation in force and that the one interim left is the empty set at start until T33 (#2290);revokedis built withcrate::revoke::in_force_forfor this box only (widened 2026-10-25 per arch C2 and C3 on #2548)test: cargo nextest run -p bep prop_revocation_covers_exactly_its_box
test: cargo nextest run -p bep revoked_value_never_triggers_renewal
- client:
- crates/minimal/src/mint.rs: submits a
mintrecord per member- crates/minimal/src/tests.rs: the mint tests and their stub control servers live here, so the stubs answer the new
record_mintandrevoke_boxrequests and the revocation test sits beside them (widened 2026-10-25)- crates/minvmd/src/box_registry.rs: on a live box's destroy submits its revocation and sends
forget_boxover the control socket (T41), completing the destroy when the proxy is down- crates/minvmd/src/cmd/run.rs: the supervisor start path sets the proxy control socket on the registry through
with_proxy_control, so only the daemon, never a test, sends revocations (widened 2026-10-25 per review finding 4369790164 on #2548)- crates/bep/src/main.rs: drops the box-egress interim from the
bep servestartup warning and thewirecomment if T14 has not already, the destroy half of that fliptest: cargo nextest run -p minimal mint_and_destroy_append_audit_records
- e2e:
- scripts/session-e2e.sh: adds two cases
test: ./scripts/session-e2e.sh bep_destroyed_box_values_refused_within_60s
test: ./scripts/session-e2e.sh bep_destroy_with_proxy_down_refuses_by_attachment
produces:
- crates/bep/src/revoke.rs
consumes:
- crates/bep/src/control.rs
- crates/minimal/src/mint.rs
proof: After
min session destroy web, a value copied from web is refused within a minute, even if the proxy was down.depends on:
- blocked by T10
- blocked by T14
- blocked by T19
T21 — Log out: delete the sign-in and refuse every reference to it
slice: S1
kind: task
covers: BEP-044
size: M
est: 220 code, 220 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min auth logoutprints "Signed out of GitHub. Boxes created before now lose GitHub access at their next request." and, when GitHub cannot be reached, "GitHub did not confirm the sign-out; the sign-in is removed from this machine." on stderr, pinned by logout_revokes_at_github_best_effortobservability: one info log line per logout naming the account and whether GitHub confirmed the revocation
layers:
- cli:
- crates/minimal/src/cli.rs:
min auth logout- crates/minimal/src/cmd/auth.rs: deletes the item, asks GitHub to revoke best effort, reports either outcome without failing
test: cargo nextest run -p minimal logout_revokes_at_github_best_effort
test: cargo nextest run -p minimal sign_in_after_logout_revives_no_earlier_reference
- e2e:
- scripts/session-e2e.sh: adds the case
test: ./scripts/session-e2e.sh bep_logout_refuses_every_reference_at_next_request
produces: none
consumes: none
proof: After
min auth logout, every running box's next GitHub request is refused, and signing in again revives none of them.depends on:
- blocked by T19
T22 — Prove a box re-created under the same name gets an id of its own that redeems
slice: S1
kind: task
covers: BEP-070
size: M
est: 0 code, 220 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box audit <name>names the id it read, so a re-created box's records read apart from its predecessor'sobservability: the registration log line already names each minted id; this task adds tests only
layers:
- tests:
- crates/minimal/src/tests.rs: the spec's named tests over the host-side mint and the collision refusal
- scripts/session-e2e.sh: adds the case
test: cargo nextest run -p minimal box_id_is_a_csprng_uuidv7_per_creation
test: cargo nextest run -p minimal colliding_box_id_refuses_creation
test: ./scripts/session-e2e.sh bep_box_recreated_under_its_name_redeems
produces: none
consumes: none
proof: A box re-created under its old name redeems its new values while the old ones stay refused.
depends on:
- blocked by T20
T23 — Revoke a box or sign out, end to end
slice: S1
kind: integration
covers: none
size: M
est: 80 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows the revocation and the refusals that follow itobservability: one info log line per revocation and per logout, as the tasks emit them
layers:
- e2e:
- scripts/session-e2e.sh: one case: destroy a box, re-create it under its name, log out, sign in again, and see each refusal and admit
test: ./scripts/session-e2e.sh bep_revoke_and_sign_out_end_to_end
produces: none
consumes: none
proof: Destroy a box or log out, and every value naming it is refused within a minute; a re-created box works, and a later sign-in revives nothing.
depends on:
- blocked by T20
- blocked by T21
- blocked by T22
T24 — Renew an expiring sign-in once at the proxy
slice: S2
kind: task
covers: BEP-069
size: L
est: 350 code, 420 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box audit <box>shows each renewal as its own record, and a sign-in that cannot renew as a refusal markedgithub_sign_in_requiredobservability: one info log line per renewal naming the account and the new expiry, and one warn line when GitHub refuses it
layers:
- renew:
- crates/bep/src/signin.rs: renews from the refresh material when the token has expired or expires within 5 minutes, writing the item update-only before first use
- crates/bep/src/forward.rs: one renewal in flight per item, the others waiting for its outcome; refuses when the write fails or the item is gone
- crates/bep/src/redeemer.rs:
BoxRedeemer::credentialno longer refuses an item whose standing isLivebut expired; it hands the item to the renewal path (widened 2026-10-24)- crates/bep/src/main.rs: wires a GitHub token-endpoint client into the proxy, with the app's client id and the client secret embedded at build time through
option_env!("MINIMAL_GITHUB_APP_CLIENT_SECRET"), asmin'sBUILT_IN_SECRETis; no launcher passes it at run time (widened 2026-10-24; wording per arch C3 on #2518)- crates/bep/src/audit.rs:
Kind::Renewal(serderenewal), the renewal's own audit record kind (added 2026-10-24 per arch C2 on #2518)- crates/minimal/src/cmd/box_audit.rs: 10BC8 maps
Kind::Renewaltorenewalin the kind match (added 2026-10-24 per arch C2 on #2518)test: cargo nextest run -p bep an_expiring_sign_in_is_renewed_at_the_proxy
test: cargo nextest run -p bep concurrent_redemptions_share_one_renewal
test: cargo nextest run -p bep renewal_not_persisted_is_refused
test: cargo nextest run -p bep renewal_finding_item_deleted_refuses_and_creates_nothing
test: cargo nextest run -p bep a_sign_in_with_time_to_live_is_not_renewed
test: cargo nextest run -p bep unrenewable_sign_in_refuses_with_sign_in_required
- client:
- crates/minimal/src/cmd/auth.rs: reads only the item's metadata after sign-in
test: cargo nextest run -p minimal client_never_presents_or_refreshes_the_token
produces: none
consumes: none
proof: A box running past the token's 8 hours still clones and pushes, renewed once at the proxy, with no new sign-in.
depends on:
- blocked by T19
T25 — Read client-owned store rules and refuse rules in the deny set
slice: S3
kind: task
covers: BEP-034, BEP-035, BEP-036, BEP-037
size: L
est: 350 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a refused rule prints "The secret-store rule in names , which a secret can never be sent to. Remove or change the rule." on stderr when the configuration is read, pinned by store_rule_in_deny_set_is_refused
observability: one debug log line per configuration read naming each rule refused and why
layers:
- config:
- crates/minimal/src/config.rs: reads
[secret-store-rules]from the client configuration- crates/boxes/src/core/bep.rs: the BEP-034, BEP-035 and BEP-036 refusals
- crates/mfile/src/lib.rs: ignores a project's
[secret-store-rules]with a warning- crates/boxes/src/client/config.rs:
Configgains thesecret-store-rulesfield, so a client file that holds rules still parses underdeny_unknown_fieldson every read, the attach path included (arch C1 on #2514)test: cargo nextest run -p boxes store_rule_in_deny_set_is_refused
test: cargo nextest run -p boxes store_rule_naming_sign_in_item_is_refused
test: cargo nextest run -p boxes ask_rule_without_tty_denies
test: cargo nextest run -p boxes prop_store_upstream_outside_egress_is_exit_3
test: cargo nextest run -p boxes project_store_rules_are_ignored_with_warning
produces: none
consumes: none
proof: A store rule aimed at a GitHub host or the Host header is refused by name when the configuration is read.
depends on:
- blocked by T19
T26 — Store, list and remove secrets with min secret under proxy-only access
slice: S3
kind: task
covers: BEP-050, BEP-051, BEP-052, BEP-053
size: L
est: 380 code, 480 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min secret set <id> <value>is refused with "min secret set reads the value from the terminal or standard input, never from an argument or an environment flag." on stderr, pinned by secret_set_refuses_value_argument_and_env_flag;min secret listshows store, authorities, injection form, consent rule and whether the proxy's access entry is present, never a valueobservability: one info log line per set and remove naming the store and the identifier, never the value
layers:
- cli:
- crates/minimal/src/cli.rs:
min secret set | list | rm [--store <s>]- crates/minimal/src/cmd/mod.rs: dispatch and module line
- crates/minimal/src/cmd/secret.rs:
set,list,rmper BEP-050, BEP-052 and BEP-053test: cargo nextest run -p minimal secret_set_stores_in_keychain_and_prints_id_only
test: cargo nextest run -p minimal secret_set_refuses_value_argument_and_env_flag
test: cargo nextest run -p minimal secret_set_without_tty_or_stdin_fails_immediately
test: cargo nextest run -p minimal secret_set_refuses_gatehouse_store
test: cargo nextest run -p minimal secret_list_shows_metadata_without_values
test: cargo nextest run -p minimal secret_rm_removes_item
- store:
- crates/bep/src/store.rs: Keychain store items with the BEP-051 access control
- crates/bep/src/lib.rs: module line
test: cargo nextest run -p minimal secret_item_acl_admits_proxy_only
test: cargo nextest run -p minimal secret_set_refuses_when_proxy_binary_absent
produces:
- crates/bep/src/store.rs
consumes: none
proof:
min secret set claudeprints only the identifier, andmin secret listshows its metadata and no value.depends on:
- blocked by T25
T39 — Write the store rule from min secret set, with fixed-token presets
slice: S3
kind: task
covers: BEP-079
size: L
est: 280 code, 600 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: an unknown preset prints "There is no preset named . Known presets: anthropic, anthropic-api-key, cloudflare, linear, sentry." on stderr, pinned by secret_set_unknown_preset_lists_known_ones
observability: one info log line per rule written naming the identifier and upstream, never the value
layers:
- cli:
- crates/minimal/src/cli.rs:
--upstream,--header,--prefix,--basic-auth,--presetand--yesonmin secret set- crates/minimal/src/cmd/secret.rs: presets, the rule check before the value is read, the change prompt, the partial-failure restore
- crates/minimal/src/config.rs: writes one
[secret-store-rules]rule to the user-levelconfig.tomltest: cargo nextest run -p minimal secret_set_writes_user_level_store_rule
test: cargo nextest run -p minimal secret_set_writes_no_project_or_org_config
test: cargo nextest run -p minimal secret_set_presets_resolve_to_their_rules
test: cargo nextest run -p minimal secret_set_unknown_preset_lists_known_ones
test: cargo nextest run -p minimal secret_set_refuses_deny_set_rule_before_reading_value
test: cargo nextest run -p minimal secret_set_shows_rule_change_and_asks
test: cargo nextest run -p minimal secret_set_rule_change_without_tty_needs_yes
test: cargo nextest run -p minimal secret_set_yes_replaces_rule_without_asking
test: cargo nextest run -p minimal secret_set_partial_failure_restores_both
test: cargo nextest run -p minimal secret_set_without_rule_flags_leaves_rule_unchanged
produces: none
consumes: none
proof:
min secret set claude --preset anthropicstores the key and writes its rule to the user-level configuration.depends on:
- blocked by T25
- blocked by T26
T27 — Mint store handles and keep the client's key registered across proxy restarts
slice: S3
kind: task
covers: BEP-063
size: M
est: 250 code, 240 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box speclists each store reference with its authorities; a refused registration prints "The proxy did not accept this client's signing key, so the box was not created." on stderr, pinned by store_handle_signed_and_key_registeredobservability: one info log line per key registration with its fingerprint, and one at start with the count restored
layers:
- mint:
- crates/minimal/src/mint.rs: the BEP-063 store handle; a refused key replaced and registered before minting
- crates/minimal/src/mint.rs: refuses a mint whose
root_not_afteris not later than now with an error namingbep --replace-key root, so an expired root never mints already-expired members (review MEDIUM 4347249409 on #2490, residual filed here)test: cargo nextest run -p minimal store_handle_signed_and_key_registered
- proxy:
- crates/bep/src/control.rs: registrations persisted across restarts; the socket owned by the operator's user at mode 0600
test: cargo nextest run -p bep client_key_registrations_survive_restart
test: cargo nextest run -p bep a_client_key_this_program_is_refused_is_replaced
test: cargo nextest run -p bep control_socket_is_owner_only_and_separate_from_listener
produces: none
consumes:
- crates/minimal/src/mint.rs
- crates/bep/src/control.rs
proof: A box created before a proxy restart keeps redeeming its store references after it.
depends on:
- blocked by T10
- blocked by T14
- blocked by T25
- assumes OQ3
T28 — Inject a stored key in the registered form, read from the Keychain per request
slice: S3
kind: task
covers: BEP-032, BEP-033, BEP-045, BEP-054, BEP-065, BEP-066
size: L
est: 380 code, 420 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box audit <box>shows each injection's store and identifier, and a refused one markedinjection_invalidor as a deleted item, never the valueobservability: one debug log line per injection naming the store, the identifier and the header, never the value
layers:
- inject:
- crates/bep/src/inject.rs: emits the registered prefix and the value as the header's value, or one
basic_authfield; refuses CR or LF in a value or prefix- crates/bep/src/store.rs: reads the value per request, writes it nowhere, refuses a deleted or disabled item
- crates/bep/src/forward.rs: injects for an admitted store reference; refuses a sign-in reference naming another instance
- crates/bep/src/lib.rs: module line
test: cargo nextest run -p bep store_reference_injects_in_registered_form
test: cargo nextest run -p bep store_value_read_per_request_never_written
test: cargo nextest run -p bep deleted_keychain_item_refuses_next_injection
test: cargo nextest run -p bep reference_to_another_sign_in_instance_is_refused
test: cargo nextest run -p bep replaced_secret_is_injected_on_next_request
test: cargo nextest run -p bep injected_value_is_exactly_prefix_plus_value
test: cargo nextest run -p bep crlf_in_value_or_prefix_refuses_injection
produces: none
consumes:
- crates/bep/src/store.rs
proof: A request to the rule's upstream carries the stored key in the registered header, and a replaced key is sent next time.
depends on:
- blocked by T26
- blocked by T27
- blocked by SP1
T29 — Keychain secrets by reference, end to end
slice: S3
kind: integration
covers: none
size: M
est: 80 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows each injection of the run by store and identifierobservability: one connection span per injected request, with the store and identifier, never the value
layers:
- e2e:
- scripts/session-e2e.sh: one case: set a secret, add a rule, reference it in a box spec, see the upstream receive it, and find it nowhere in the box
test: ./scripts/session-e2e.sh bep_store_reference_end_to_end
produces: none
consumes: none
proof: Store an API key with
min secret, reference it in a box spec, and the box's requests to its upstream carry it while the key never enters the box.depends on:
- blocked by T25
- blocked by T26
- blocked by T27
- blocked by T28
- blocked by T39
T30 — Steer credentialed names to the proxy and fail closed while it is down
slice: S4
kind: task
covers: BEP-062, BEP-078
size: M
est: 280 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the bundle's daemon log tail carries each SERVFAIL answered for a steered name while the proxy was down, with the name and the box
observability: one rate-limited warn log line per steered name answered SERVFAIL, and one info line when the proxy's listener comes back
layers:
- dns:
- crates/minimald/src/net/dns_gate.rs: steered names answer the proxy's address; SERVFAIL while it is down; NODATA for AAAA, HTTPS, SVCB
- crates/minimald/src/net/dns.rs: carries the proxy's listener state to the gate
test: cargo nextest run -p minimald steered_name_servfail_while_proxy_down
test: cargo nextest run -p minimald steered_name_aaaa_https_svcb_are_nodata
produces: none
consumes: none
proof: With the proxy stopped, a dns-steered box resolving github.com gets SERVFAIL, never a real address.
depends on:
- blocked by T19
T31 — Reject UDP to port 443 where a box's QUIC stance is blocked
slice: S4
kind: task
covers: BEP-018
size: L
est: 280 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the bundle's daemon log tail carries each rejected UDP 443 datagram's box and destination, rate-limited
observability: one rate-limited debug log line per rejected datagram with the box and the destination
layers:
- net:
- crates/minimald/src/net/switch.rs: rejects UDP to 443 with ICMP port unreachable for a blocked box, and to the steered proxy address always
- crates/minimald/src/net/classifier.rs: blocks the
host_ipcohort when any resident member is blockedtest: cargo nextest run -p minimald quic443_blocked_rejects_udp_443
test: cargo nextest run -p minimald udp_443_to_steered_proxy_rejected_in_every_stance
test: cargo nextest run -p minimald host_ip_cohort_quic_stance_most_restrictive_wins
- spec:
- crates/boxes/src/core/bep.rs: the property test over the stance resolution
test: cargo nextest run -p boxes prop_quic443_auto_resolves_on_grants_and_steering
- policy:
- crates/boxes/src/lib.rs:
CredentialedUpstreamcarries the declaredquic443stance to the daemon (arch C1 on #2513)- crates/minimal/src/cmd/boxes/create.rs:
credentialed_upstream_forfills it from[session.network] quic443- crates/minimald/src/net/gvproxy_network.rs: builds the gate with the declared stance, so
blockandallowoverrideauto(spec §5.3 line 150)test: cargo nextest run -p minimald declared_quic443_block_rejects_without_test_hook
produces: none
consumes: none
proof: A dns-steered grant box's QUIC to port 443 gets an immediate port-unreachable.
depends on:
- blocked by T19
T32 — DNS steering fails closed, end to end
slice: S4
kind: integration
covers: none
size: M
est: 80 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the bundle's daemon log tail carries the SERVFAIL answers and the rejected datagrams of the run
observability: the warn and debug lines both tasks emit, in one run
layers:
- e2e:
- scripts/session-e2e.sh: one case: stop the proxy, resolve a steered name, query its HTTPS record, send QUIC to 443
test: ./scripts/session-e2e.sh bep_dns_steering_fails_closed_end_to_end
produces: none
consumes: none
proof: A steered name answers SERVFAIL while the proxy is down, AAAA, HTTPS and SVCB answer NODATA, and UDP 443 from a blocked box is rejected at once.
depends on:
- blocked by T30
- blocked by T31
T33 — Rotate audit segments with the chain carried across and revocations restored
slice: S5
kind: task
covers: BEP-068
size: M
est: 250 code, 240 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>reads across every retained segment, so a box's trail is whole after rotationobservability: one info log line per rotation with the closed segment and its final hash, and one at start with the revocations restored
layers:
- audit:
- crates/bep/src/audit.rs: segments rotated with the chain carried, revocation segments retained (BEP-068)
- crates/bep/src/main.rs:
bep servepasses the restored revocations toBoxRedeemer::with_revocationsand drops the revocation interim from the startup warning (T41)- crates/bep/src/revoke.rs: restores every revocation from every retained segment at start
test: cargo nextest run -p bep segment_rotation_continues_chain
test: cargo nextest run -p bep revocations_restored_from_every_segment_on_start
test: cargo nextest run -p bep segment_with_live_revocation_is_retained
- cli:
- crates/minimal/src/cmd/box_audit.rs: reads every retained segment
test: cargo nextest run -p minimal box_audit_reads_across_segments
produces: none
consumes:
- crates/bep/src/revoke.rs
proof: After rotation,
min box auditstill shows a box's whole trail, and a restart still refuses a destroyed box.depends on:
- blocked by T19
- blocked by T20
T34 — Report an edited audit chain in min doctor
slice: S5
kind: task
covers: BEP-049
size: M
est: 220 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the chain check prints "Audit log: the hash chain breaks at record of ; a record was changed or removed." or "Audit log: chain verified." on stdout, pinned by prop_audit_chain_detects_unrecomputed_edit
observability: one info log line per check naming the segments verified and the outcome
layers:
- verify:
- crates/bep/src/audit.rs: chaining and verification as pure functions over bytes, apart from the writer
- scripts/kani.sh: the added harness's count
test: cargo nextest run -p bep prop_audit_chain_detects_unrecomputed_edit
test (deferred: kani): cargo nextest run -p bep kani_audit_chain_detects_edit
- cli:
- crates/minimal/src/cli.rs:
min doctor- crates/minimal/src/cmd/mod.rs: dispatch and module line
- crates/minimal/src/cmd/doctor.rs: runs the audit chain check across every segment
test: cargo nextest run -p minimal doctor_reports_a_broken_audit_chain
produces:
- crates/minimal/src/cmd/doctor.rs (entry point)
consumes: none
proof: Editing one audit record makes
min doctorname where the chain breaks.depends on:
- blocked by T33
T35 — Audit segments rotate and the chain is checked, end to end
slice: S5
kind: integration
covers: none
size: M
est: 120 code, 180 tests
executor: agent
difficulty: routine
model: standard
diagnostics: the case's last step prints the chain verified across the rotated segments
observability: the rotation log lines of the run, and one info line at
bep servestart naming the segment bound when the test override sets itlayers:
- bound:
- crates/bep/src/main.rs:
open_audit_logtakes the segment bound fromBEP_AUDIT_SEGMENT_BYTESwhen it is set to a positive integer, elseaudit::SEGMENT_BYTES; the variable is a test surface, read only bybep serve, whichminvmdspawns with its own environment (widened 2026-10-31, owner, #2295 noop: no runtime knob)test: cargo nextest run -p bep audit_segment_bound_follows_the_test_override
- e2e:
- scripts/session-e2e.sh: one case: set
BEP_AUDIT_SEGMENT_BYTESfor the restarted host daemon, drive requests past the bound, read the trail, edit a record, runmin doctortest: ./scripts/session-e2e.sh bep_audit_rotation_and_doctor_end_to_end
produces: none
consumes: none
proof: The audit log rotates with its chain carried across,
min box auditreads every segment, andmin doctorreports an edited chain.depends on:
- blocked by T33
- blocked by T34
T36 — Rotate the signing certificate under an unchanged root
slice: S6
kind: task
covers: BEP-061
size: S
est: 120 code, 60 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
bep --rotate-signingprints "Signing certificate rotated. Running boxes keep working; nothing inside them changes." on stdout, pinned by signing_ca_rotation_invisible_to_boxesobservability: one info log line per rotation naming the old and new signing fingerprints
layers:
- ca:
- crates/bep/src/ca.rs: issues a new signing CA under the same root and key identifier
- crates/bep/src/main.rs:
bep --rotate-signing- crates/bep/src/main.rs:
bep serverenews its signing CA throughCa::renew_signingbeforesigning_expiry(), so a long-running proxy keeps signing leaves past the 365-day signing validity (arch C2 on #2493, residual filed here)test: cargo nextest run -p bep signing_ca_rotation_invisible_to_boxes
produces: none
consumes: none
proof: Rotate the signing certificate under the same root, and every running box keeps cloning and pushing unchanged.
depends on:
- blocked by T19
T37 — Attribute a host-address redemption to its cohort and mark the audit
slice: S7
kind: task
covers: BEP-028
size: S
est: 100 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows a cohort redemption markedcohort_attributedobservability: the connection span gains the cohort flag
layers:
- decide:
- crates/bep/src/redeem.rs: admits a host-address sender presenting a value naming a live host-address box, marked
cohort_attributedtest: cargo nextest run -p bep host_ip_redemption_is_cohort_attributed
- audit:
- crates/bep/src/redeemer.rs: carries
Decision::marker()throughVerdict::Admit, so a cohort admit reaches the record with its marker (arch C1 on #2511)- crates/bep/src/forward.rs: writes the admitted audit record with that marker and records
cohort_attributedon thebox_connectionspan (arch C1 on #2511)- crates/bep/src/proxy.rs: declares the
cohort_attributedfield on thebox_connectionspan that forward.rs recordstest: cargo nextest run -p bep host_ip_redemption_is_cohort_attributed_in_the_audit_record
produces: none
consumes: none
proof: A host-address box redeems a value naming another live host-address box, and its audit record is marked cohort-attributed.
depends on:
- blocked by T19
T57 — Design the provider directory and socket ownership for a split-user VMM
slice: S8
kind: enablement
covers: none
size: S
est: 20 code, 60 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the design document names, for each socket and file in the provider directory, the user that creates it, the mode it gets and the user that opens it
observability: none; this task ships a design document and no code
layers:
- design:
- crates/minvmd/src/sock.rs: a test reads the design document and checks that every socket and file
prepare_socket_dirand the VMM child create is named with an owner, a mode and an opening user- docs/design/2026-10-25-split-user-vmm-provider-dir.md: with gvproxy and the VMM running as a dedicated unprivileged user, how the marker, report, telemetry, gate and ssh bridge sockets, the data volume and the boot log in the provider directory stay reachable by
minand the proxy as the login user, given thatsock::prepare_socket_dirmakes that directory 0700 owned by the caller and each socket 0600; the hypothesis is a group shared by the two users, a 0770 directory and 0660 sockets, with the uid change handed over by the sudoers drop-inmin finalize-installinstalls; the document reads crates/minvmd/src/sock.rs, vm.rs, image.rs, volume.rs, cmd/vmm_child.rs and crates/bep/src/listener.rs, lists every caller that opens each path, and ends with the ownership and permission model that arch ruled on, or with the fallback that only gvproxy runs as the dedicated usertest: cargo nextest run -p minvmd split_user_provider_dir_design_names_every_socket_owner
produces: none
consumes: none
proof: The design document names an owner, a mode and the opening user for every socket and file in the provider directory, and arch has ruled on it, so T38 can be cut against it.
depends on:
- blocked by T19
T65 — Lay out the split users, directories and socket modes for a split-user VMM
slice: S8
kind: integration
covers: none
size: L
est: 380 code, 320 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min finalize-installnames the vm-users item and why it cannot set up the split users (a user-writable binary path, or a home directory the dedicated users cannot search)observability: one debug log line when minvmd gives a socket 0600 because it does not hold the socket's group
layers:
- install:
- crates/minimal/src/cmd/finalize_install.rs: the vm-users script creates the
minimal-vmmandminimal-gvproxyusers and groups, the provider dir at 0710 groupminimal-vmm,vmm/,<gvproxy dir>,<gvproxy config dir>and<gate dir>asdocs/design/2026-10-25-split-user-vmm-provider-dir.mdlays them out, and renders the sudoers drop-in; the item is not offered until T66 spawns throughsudo -n(narrowed 2026-10-25 by the epic owner to the foundation #2561 delivered; arch C3 on #2561)test: cargo nextest run -p minimal finalize_install_creates_the_split_users_dirs_and_drop_in
- sockets:
- crates/minvmd/src/sock.rs:
prepare_socket_dir,restrict_owned_dirandverify_provider_dir_ownershipaccept 0710 with groupminimal-vmm;enforce_socket_permissionsandverify_socket_permissionstake the mode and the group from the design's table- crates/minvmd/src/cmd/vmm_child.rs: creates the boot log with mode 0640
- crates/minvmd/src/volume.rs:
create_sparse_rawcreates the data volume with mode 0600- crates/minvmd/src/control.rs: the guest door admits the
minimal-vmmuid and the supervisor's own uid; the host door keeps its check- crates/minvmd/src/guest_telemetry.rs:
accept_loopadmits theminimal-vmmuid and the supervisor's own uidtest: cargo nextest run -p minvmd split_user_provider_dir_modes_are_enforced
produces: none
consumes: none
proof: The proxy runs as the login user, and the EGW process, gvproxy and the VM run as separate processes sharing no writable state, as dedicated unprivileged users where the vm-users drop-in is installed. Moved 2026-10-31: the spawn through
sudo -n(#2566) and the EGW process (#2285) go to gominimal/inbox#1184 with BEP-047; this slice keeps the design and the users it laid out.depends on:
- blocked by T57
T50 — Ship a git credential helper in grant boxes so git sends a header the proxy substitutes
slice: S9
kind: task
covers: BEP-015
size: M
est: 160 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
git credential fillinside a grant box answers for the grant's hosts with the placeholder, andmin box speclists the helper under the grantobservability: one debug log line per helper answer naming the host, never the value
layers:
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: adds the requirement for the git credential helper a grant box carries (Gatehouse §6.4 line 718), under BEP-015
- box:
- crates/minimald/src/box_actor.rs: declares the new module
- crates/minimald/src/box_runtime/launcher.rs: the
connectionenv layer carriesGIT_CONFIG_COUNT,GIT_CONFIG_KEY_<n>andGIT_CONFIG_VALUE_<n>for a box that holds sealed deliveries, so the shipped helper is git's sole helper for each grant host withstoreandcachedisabled, at a scope git reads after every config file (widened 2026-10-25 per arch C1 on #2547)- crates/minimald/Cargo.toml: depends on
bep, so the helper reads the host set frombep::module::GITHUBinstead of a copy (widened 2026-10-25 per arch C2 on #2547)- Cargo.lock: regenerated by the new
bepdependency ofminimald(widened 2026-10-25)- crates/minimald/src/box_actor/tests.rs: the helper's tests
- crates/minimald/src/box_actor/bep_git_helper.rs: writes the helper and its
git config credential.helperline into a grant box at creation, for the grant's hosts only, with the sealed value as the placeholder the proxy substitutestest: cargo nextest run -p minimald grant_box_git_credential_helper_answers_for_grant_hosts
- e2e:
- scripts/session-e2e.sh: a case that clones and pushes from a grant box with the shipped helper and no helper of its own; records NOT RUN off macOS, without a sign-in or without E2E_BEP_PRIVATE_REPO (#2500)
test: ./scripts/session-e2e.sh bep_git_credential_helper_sends_header
produces: none
consumes:
- crates/bep/src/forward.rs
proof: From a grant box with no git configuration of its own, git clone and git push send an Authorization header the proxy substitutes.
depends on:
- blocked by T11
- blocked by T18
T51 — Rotate a running proxy's signing certificate through the control socket
slice: S9
kind: task
covers: BEP-061
size: M
est: 220 code, 140 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the command prints one line naming the old and new signing fingerprints, and the proxy's info log line records the same rotation
observability: the info log line T36 added, for operator-driven and scheduled rotations alike
layers:
- control:
- crates/bep/src/control.rs: accepts a rotate-signing request from the host and runs
Ca::renew_signingon the serving proxy's CA under the same root and key identifier- crates/bep/src/main.rs:
bep rotate-signingsends the request over the control socket and prints the resulttest: cargo nextest run -p bep rotate_signing_over_control_socket_keeps_leaves_valid
produces: none
consumes: none
proof: An operator rotates the serving proxy's signing certificate now, and a leaf issued before and one issued after both validate against the root a box holds.
depends on:
- blocked by T36
T52 — A signing rotation under a running grant box keeps clone and push working, end to end
slice: S9
kind: task
covers: BEP-061
size: M
est: 40 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the e2e case prints the root fingerprint from
min box specbefore and after the rotation, and they matchobservability: none beyond the case's own output
layers:
- e2e:
- scripts/session-e2e.sh: starts a grant box, clones and pushes, rotates through
bep rotate-signing, clones and pushes again from the same box; records NOT RUN off macOS, without a sign-in or without E2E_BEP_PRIVATE_REPO, asbep_first_slice_end_to_enddoestest: ./scripts/session-e2e.sh bep_signing_rotation_keeps_running_box_working
produces: none
consumes: none
proof: A running grant box clones and pushes before and after a signing rotation without being re-created, and its root fingerprint is unchanged.
depends on:
- blocked by T51
T53 — Enforce the host_ip cohort's blocked QUIC stance in the host-address ruleset
slice: S9
kind: task
covers: BEP-018
size: M
est: 200 code, 160 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the bundle's daemon log tail carries one line per cohort stance change naming the box that flipped it
observability: one info log line when the cohort flag turns on or off, with the blocking members
layers:
- ruleset:
- crates/minimald/src/net/classifier.rs:
CohortQuic443learns the path of the cohort's blocked leaf and the daemon's move of ahost_ipbox's processes into and out of it (acgroup.procswrite inside the delegated slice, which needs no CAP_NET_ADMIN and never rewrites the table); its doc stops saying the type is unenforced- scripts/install-host-classifier.sh:
render_rulesetgains one rule keyed on a new delegated leaf under the boxes directory, the cohort's blocked leaf:socket cgroupv2 level <n> "<blocked leaf>" udp dport 443 reject with icmpx port-unreachable; the install step creates that leaf with the rest of the tree, so the privileged step ships the rule and the daemon flips it by membership (widened 2026-10-25: classifier.rs only embeds this script withinclude_str!, so the table is rendered here)- scripts/install_test.sh: the install harness models the blocked leaf
boxes/quic443-blockedbesidedenyandallow(the expected delegation list and bothdrop_cgroup_filesloops before--uninstall), so the un-enrolled and installed trees uninstall again (widened 2026-10-25:common::shell_harnesses all_script_test_harnesses_passruns this harness on the native lane)test: cargo nextest run -p minimald host_ip_cohort_blocked_stance_rejects_udp_443
- lifecycle:
- crates/minimald/src/box_actor/host.rs: the
host_iplaunch path callsCohortQuic443::joinwith the box's resolved stance and moves every residenthost_ipbox into the blocked leaf whenblocked()turns true; the stop path callsleaveand moves them back to their own leaves when the last blocking member goes- crates/minimald/src/box_runtime.rs:
command_in_sessionhands the shim the cohort's blocked leaf instead of the box's own leaf while the cohort is blocked, and records the shim pid under the box id so the injected process goes home when the block lifts (widened 2026-10-25 per arch C1 on #2553:min box execand injected hooks must not escape the stance)- crates/minimald/src/box_runtime/tests.rs: the exec-placement tests live here, where
box_runtime.rskeeps its tests (widened 2026-10-25)test: cargo nextest run -p minimald host_ip_cohort_join_block_leave_drives_the_rule
produces: none
consumes: none
proof: A host_ip cohort with one blocked member sends no UDP 443 and gets ICMP port unreachable; when that member stops, the cohort sends UDP 443 again.
depends on:
- blocked by T31
T54 — Call the store-rule checks from box creation and expansion
slice: S9
kind: task
covers: BEP-034, BEP-035, BEP-036
size: M
est: 240 code, 220 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a refused store reference exits 3 with the rule and the reason on stderr; an
askrule without a terminal prints the denialobservability: one info log line per creation naming the store references checked and their outcome
layers:
- create:
- crates/minimal/src/cmd/boxes/create.rs: reads the rules through
read_client_config_and_store_rules, callscheck_store_referencesfor every host-store reference the spec declares andstore_consentforaskrules, and passes the accepted rules to the proxy registration- crates/minimal/src/config.rs:
read_client_configno longer drops the rules on the creation pathtest: cargo nextest run -p minimal store_reference_without_rule_is_refused_at_creation
test: cargo nextest run -p minimal store_rule_outside_egress_exits_3
test: cargo nextest run -p minimal ask_rule_without_a_terminal_denies
produces: none
consumes:
- crates/boxes/src/core/bep.rs
proof: A box spec that references a host-store secret is refused at creation when no rule matches or the rule's upstream is outside the box's egress, and an ask rule prompts on a terminal and denies without one.
depends on:
- blocked by T13
- blocked by T25
T55 — Replace the provisional CA deny set with the decided infrastructure deny set
slice: S9
kind: task
covers: BEP-013
size: S
est: 60 code, 120 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box specshows the root's excluded subtrees, and a refused rule names the deny-set entry it hitobservability: none beyond the refusal text
layers:
- ca:
- crates/bep/src/ca.rs:
DENY_SETbecomes the decided set from one shared definition:min.internal,minimal.dev, the tenant issuer, the BEP, EGW and relay endpoints, the registry and build cache, and the node-plane baseline set;localhost,.local,home.arpaand.internalstay allowed (owner's ruling of 2026-10-24 on #2441)- crates/boxes/src/core/bep.rs: the BEP-034 check reads the same definition
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: names the set
test: cargo nextest run -p bep root_excludes_every_deny_set_name
test: cargo nextest run -p boxes every_deny_set_name_is_refused_as_a_store_rule
produces: none
consumes: none
proof: The root certificate excludes every name of the decided deny set, and a store rule naming any of them is refused.
depends on:
- blocked by T3
- blocked by T50
T58 — Steer a dns or both box's credentialed names through the relay's gate in production
slice: S9
kind: task
covers: BEP-062, BEP-078
size: L
est: 260 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: with the proxy stopped, a dns-steered box resolving github.com gets SERVFAIL, and the daemon log names the box and the name once per minute
observability: one info line when the proxy's listener is reported live and the steered names resolve to it again
layers:
- host:
- crates/minimald-rpc/src/lib.rs: a
BoxControlRequest::ProxyListeningask and itsBoxControlReply::ProxyListening { listening }answer, admitted on the guest reports door besideRowStanding(re-planned 2026-10-25 by the epic owner: the listener state lives in minvmd on the host and the guest relay cannot linkbep_host, so it asks over the report door)- crates/minvmd/src/control.rs: the guest reports door answers
ProxyListeningfrom the proxy'slisteningflag, and answersfalsewhen no proxy runs- crates/minvmd/src/cmd/run.rs: hands
BepProxy::listeningto the control server so the guest door can read ittest: cargo nextest run -p minvmd guest_door_answers_proxy_listening
- relay:
- crates/minimald/src/net/listeners.rs:
proxy_listeningasks over the report door the wayhost_row_standingdoes;None(a native host, which runs no proxy, or a door that does not answer within its deadline) reads as not live, so steered names fail closed with SERVFAIL- crates/minimald/src/net/dns.rs:
ProxyListenerStaterefreshes fromproxy_listeningwith a short cached answer; theallow(dead_code)interim goes- crates/minimald/src/net/dns_gate.rs: the
allow(dead_code)interims onwith_steeringgo, and the comment stops naming #2523- crates/minimald/src/net/switch.rs:
BoxGate::for_sessioncallsDnsGate::with_steering(policy, <credential module hosts>, <proxy address on the switch>, <shared ProxyListenerState>)for every box whose steering resolves todnsorboth- crates/minimald/src/net/gvproxy_network.rs: passes the shared
ProxyListenerStateand the control channel it asks over intoBoxGate::for_session- scripts/session-e2e.sh: in
bep_dns_steering_fails_closed_end_to_end, leg 1'sknown_gapfor the steered A lookup becomes an::error::line andds_fail, andds_a_gapwith its known-gap branch goes, so the leg is a hard assertion of Networking §5.3 line 147 once steering is wired (arch C1 on #2550, widened 2026-10-25)test: cargo nextest run -p minimald a_dns_steered_box_gets_servfail_while_the_proxy_is_down
test: cargo nextest run -p minimald a_steered_name_resolves_to_the_proxy_once_its_listener_is_live
test: cargo nextest run -p minimald a_native_host_steered_name_gets_servfail
produces: none
consumes: none
proof: In a running daemon with the proxy stopped, a dns-steered box resolving github.com gets SERVFAIL and never a real address; once the listener is live the same lookup gets the proxy's address; on a native host, which runs no proxy, a steered name always gets SERVFAIL.
depends on:
- blocked by T30
T59 — Sign store handles under the client's one local key from a host-keychain ClientKeyStore
slice: S9
kind: task
covers: BEP-063
size: L
est: 260 code, 240 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics:
min box specnames the client key's identifier under the grant, and the proxy log records one registration per client, not one per boxobservability: one info line at first use naming the registered key identifier
layers:
- key-store:
- crates/minimal/src/mint.rs: a host-keychain
ClientKeyStorebackend behind the existing key-store seam holds the client's one local signing key (macOS Keychain; the Linux host behind the same seam), and theInterim:notes andexpect(dead_code)reasons that name #2524 go- crates/minimal/src/cmd/boxes/create.rs:
mint_grantsreadslocal_client_keyover that store and signs the store handle and the grant registrations with it, neverClientKey::generate()per box- crates/bep/src/control.rs: the module doc stops naming #2524; the registry keeps one key per client
test: cargo nextest run -p minimal two_creations_register_one_client_key
test: cargo nextest run -p minimal a_refused_client_key_is_replaced_and_registered_before_the_mint
produces: none
consumes: none
proof: Two box creations register one client key with the proxy, and a key the proxy refuses is replaced and registered before the mint.
depends on:
- blocked by T27
- assumes OQ3
T60 — Carry a store handle sealed as a store member kind
slice: S9
kind: task
covers: BEP-063
size: M
est: 140 code, 160 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows a redemption of a store member with its store reference, and a store member whose handle fails verification as a refusalobservability: one debug line per store member redeemed naming the store reference, never the value
layers:
- seal:
- crates/bep/src/seal.rs:
MemberKindgainsStore(serdestore), so a store handle travels sealed as{kind: "store", jws}- crates/bep/src/redeem.rs: a store member's handle is verified against the registered client key and resolved to the store reference it names
test: cargo nextest run -p bep a_store_member_round_trips_sealed
test: cargo nextest run -p bep a_store_handle_with_an_unregistered_key_is_refused
produces: none
consumes: none
proof: A store handle sealed as a store member round-trips through the envelope, redeems against the registered client key, and is refused under an unregistered one.
depends on:
- blocked by T27
- assumes OQ3
T61 — List each store reference with its authorities in min box spec
slice: S9
kind: task
covers: BEP-063
size: S
est: 60 code, 80 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box spec <box>lists each store reference the box declares with the authorities its rule registersobservability: none beyond the printed listing
layers:
- spec:
- crates/minimal/src/cmd/box_spec.rs: prints each store reference under the grant with the authorities the matching store rule registers
test: cargo nextest run -p minimal box_spec_lists_store_references_with_their_authorities
produces: none
consumes: none
proof:
min box specon a box with one store reference prints the reference and the authorities its rule registers.depends on:
- blocked by T27
- assumes OQ3
T62 — Give the proxy-binary resolver one owner shared by min secret and minvmd
slice: S9
kind: task
covers: BEP-051
size: S
est: 80 code, 100 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min secret setandminvmd runprint the same proxy path in their logs for one PATH and oneMINVMD_BEP_BINobservability: one debug line naming the resolved proxy path at each resolution
layers:
- resolver:
- crates/bep/src/store.rs:
proxy_binary_pathis the one rule, and its doc stops naming #2527- crates/minvmd/src/cmd/run.rs:
resolve_bep_binarycallsbep::store::proxy_binary_pathinstead of its own copy- crates/minimal/src/cmd/secret.rs:
resolve_proxyuses the shared rule and its doc stops naming #2527test: cargo nextest run -p minvmd minvmd_and_min_secret_resolve_the_same_proxy_binary
produces: none
consumes: none
proof: Under one PATH and one
MINVMD_BEP_BIN,min secret setandminvmd runresolve the same proxy binary, and the rule has one definition.depends on:
- blocked by T26
T63 — Revoke or replace a registered client key at the proxy and bound the registry
slice: S9
kind: task
covers: BEP-063
size: M
est: 180 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the proxy log records each key removal with its identifier, and a store handle under a revoked key is refused with a reason naming the key
observability: one info line per removal; one warn line when the bound drops a key
layers:
- control:
- crates/bep/src/control.rs: a revoke request from the socket's owner removes a key from the registry and the key file; the registry drops keys past a configured count or idle period
- crates/minimal/src/mint.rs:
local_client_keyrevokes the key it replaces after a refusaltest: cargo nextest run -p bep a_revoked_client_key_no_longer_verifies_a_store_handle
test: cargo nextest run -p bep a_key_removal_survives_a_restart
test: cargo nextest run -p minimal a_replaced_client_key_is_revoked_at_the_proxy
produces: none
consumes: none
proof: A store handle signed under a key the client revoked or replaced is refused by the proxy, after a restart included, and the registry never holds more keys than its bound.
depends on:
- blocked by T27
- blocked by T59
- assumes OQ3
T64 — Leave the host_ip QUIC cohort when a box's shell exits on its own
slice: S9
kind: task
covers: BEP-018
size: S
est: 40 code, 80 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: the daemon log tail carries one line when a box whose shell exited on its own leaves the cohort, naming the box and the members still blocking
observability: one info log line per departure through the exit path, with the blocking members left
layers:
- lifecycle:
- crates/minimald/src/box_actor/host.rs: the path that observes the host process's exit calls
leave_quic443_cohortthe waystop_runningdoes, so a box whose shell exits on its own stops blocking the cohort and the remaining members move home when the last blocker goestest: cargo nextest run -p minimald host_ip_cohort_shell_exit_leaves_and_lifts_the_block
produces: none
consumes: none
proof: A host_ip cohort whose only blocked member's shell exits on its own sends UDP 443 again without a Stop or destroy.
depends on:
- blocked by T53
T56 — Conditions closed, end to end
slice: S9
kind: integration
covers: BEP-015, BEP-061, BEP-018, BEP-034
size: M
est: 0 code, 220 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each e2e case prints PASS or NOT RUN with its gate, and the daemon log tail names the cohort flag changes
observability: none beyond the cases' own output
layers:
- e2e:
- scripts/session-e2e.sh: one walk that runs the helper, rotation and cohort cases on a lane with a sign-in and E2E_BEP_PRIVATE_REPO, and records NOT RUN elsewhere (#2500)
test: ./scripts/session-e2e.sh bep_conditions_closed_end_to_end
produces: none
consumes: none
proof: the residuals the arch left open at merge are done: git in a grant box sends a header the proxy substitutes, an operator rotates a running proxy's signing certificate and the box keeps pushing, a blocked
host_ipcohort sends no UDP 443, every store reference is checked at creation, and the CA deny set is the decided one. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-24 so the residuals dispatch as one slice.depends on:
- blocked by T50
- blocked by T51
- blocked by T52
- blocked by T53
- blocked by T54
- blocked by T55
- blocked by T58
- blocked by T59
- blocked by T60
- blocked by T61
- blocked by T62
- blocked by T63
- blocked by T64
T67 — Inject store values in bep serve
slice: S10
kind: task
covers: BEP-032
size: M
est: 120 code, 160 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: an admitted store reference that the serve path cannot read is refused as
store_item_unreadableinmin box audit, never forwarded without the keyobservability: one debug line per store injection naming the store and identifier, never the value
layers:
- redeem:
- crates/bep/src/redeemer.rs:
BoxRedeemeroverridesRedeemer::redeem_referenceand returnsRedeemed::Storefor an admitted store member (Gatehouse §6.10 line 849, redemption step 4), with theheadercheck still beforeredeemruns- crates/bep/src/proxy.rs: the
bep servepath callsFlow::with_store(Arc::new(HostStore))where it builds eachFlow- crates/bep/src/forward.rs: the two interim notes on
redeem_referenceandwith_store(#2586) are removedtest: cargo nextest run -p bep serve_injects_a_stored_key_for_an_admitted_store_member
produces: none
consumes: none
proof: A box's request to a store rule's registered upstream leaves
bep servewith the stored key in the registered form, and the box never holds the key.depends on:
- blocked by SP1
- blocked by T28
T68 — Carry the handle jti into the audit record
slice: S10
kind: task
covers: BEP-067
size: M
est: 110 code, 100 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box audit <box>shows the handle'sjtion each store-handle record, empty when the handle does not parseobservability: the audit record carries
jtilayers:
- audit:
- crates/bep/src/audit.rs:
DecisionandRecordgainjti: String, empty when the handle does not parse- crates/bep/src/redeemer.rs:
facts()carrieshandle::Read::jtiinto the verdict (Gatehouse §6.10 line 842)- crates/bep/src/forward.rs:
Verdict::AdmitandVerdict::Refusecarry thejti, andrecordandaudit_refusalwrite it into theDecision(stale layers amended 2026-10-26 by the epic owner's rule)- crates/bep/src/control.rs: its
Decisionliteral sets an emptyjti- crates/bep/src/proxy.rs: its
Decisionliteral sets an emptyjti- crates/minimal/src/cmd/box_audit.rs:
min box auditshows thejti, and the test helper'sDecisionsets ittest: cargo nextest run -p bep a_store_handle_audit_record_carries_its_jti
produces: none
consumes: none
proof: A refused store member (
store_handle_invalid) and a redeemed one both write an audit record that carries the handle'sjti.depends on:
- blocked by T60
T69 — Wire the client-key 10BC8 bound into bep serve
slice: S10
kind: task
covers: BEP-063
size: S
est: 50 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
bep serve --helpnames the bound flag, and a bad value is refused before the proxy startsobservability: one info line at start with the key bound in force
layers:
- serve:
- crates/bep/src/main.rs:
bep servebuilds its control socket throughControl::bind_with_key_boundwith aKeyBoundfrom a flag; the idle period staysNoneuntil gominimal/arch#85 decides the root validitytest: cargo nextest run -p bep serve_passes_its_key_bound_to_the_control_socket
produces: none
consumes: none
proof:
bep servestarted with a smaller key bound keeps at most that many client keys.depends on:
- assumes OQ3
- blocked by T63
T70 — Name the kid when a store handle is refused as unknown_key
slice: S10
kind: task
covers: BEP-063
size: S
est: 20 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics: the
store handle checkedlog line names the handle's headerkidwhen the handle is refused asunknown_keyobservability: the existing debug line gains the
kidlayers:
- handle:
- crates/bep/src/handle.rs:
read()puts the headerkidin thestore handle checkedline for anunknown_keyrefusal; thestore_handle_invalidaudit marker andjtistay as they aretest: cargo nextest run -p bep an_unknown_key_refusal_names_the_kid
produces: none
consumes: none
proof: A store handle signed under a key the proxy does not hold is refused, and the log line names that key's
kid.depends on:
- assumes OQ3
- blocked by T63
T71 — Amend BEP-063 for client-key revocation, replacement and the registry bound
slice: S10
kind: task
covers: BEP-063
size: S
est: 0 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics: none, a spec change
observability: none: a spec change writes no log line
layers:
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: the BEP-063 sub-requirements and the
[NEEDS CLARIFICATION]entry that #2578 lists (revocation bykid, replacement in three steps with the pending-revoke retry, the registry bound, the verify-line move, the rationale at line 475, gominimal/arch#144)test: cargo nextest run -p minimal a_replaced_client_key_is_revoked_at_the_proxy
produces: none
consumes: none
proof: Spec 24 states client-key revocation, replacement and the registry bound, each with a verify line that names an existing test.
depends on:
- assumes OQ3
- blocked by T63
T72 — Reject UDP 443 from a blocked cohort's own leaves, sockets opened before the flip included
slice: S10
kind: task
covers: BEP-018
size: M
est: 220 code, 200 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the daemon log names each leaf added to or removed from the blocked set, and
min finalize-installnames the helper item and why it cannot install itobservability: one info line per set change with the leaf and the cohort's blocking members
layers:
- ruleset:
- scripts/install-host-classifier.sh:
render_rulesetdeclares a set of cgroup v2 leaves (typeof socket cgroupv2 level N) and the UDP 443 reject rule also matches a socket whose leaf is in it, so a socket created in a member's own leaf before the flip is rejected from its next datagram; the install step adds a root-owned helper that only adds or removes one leaf under the delegatedboxesslice in that set, refusing any other path, and a sudoers drop-in that lets the login run only that helper withsudo -n(owner's decision 2026-10-26: a narrow helper, not CAP_NET_ADMIN for minimald)- scripts/install_test.sh: the rendered set and rule, and the helper refusing a path outside the slice
test: cargo nextest run -p minimald the_ruleset_rejects_udp_443_from_leaves_in_the_blocked_set
- cohort:
- crates/minimald/src/net/classifier.rs:
CohortQuic443keeps every residentallowmember's own leaf in the blocked set while the cohort blocks: all of them when it turns blocked, a joining member's at admit, none once the last blocker leaves; the process moves stay- crates/minimald/src/box_actor/host.rs: runs the helper through
sudo -nfor each set change, and logs a refusal without failing the boxtest: cargo nextest run -p minimald cohort_flip_puts_member_leaves_in_the_blocked_set
produces: none
consumes: none
proof: A host_ip cohort member's UDP 443 socket opened before the cohort turns blocked stops sending when the block flips, and sends again when it lifts.
depends on:
- blocked by T53
T73 — Close the race between the cohort lock and the shim's leaf join
slice: S10
kind: task
covers: BEP-018
size: S
est: 60 code, 100 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the daemon log names an injected process that the post-join check moved into the blocked leaf
observability: one debug line per post-join move
layers:
- inject:
- crates/minimald/src/box_runtime.rs: after the shim joins its leaf,
Host::commandchecks the cohort again under the lock and moves the shim into the blocked leaf and records it, or refuses the injection- crates/minimald/src/box_runtime/tests.rs: the interleaving test
test: cargo nextest run -p minimald an_admit_between_injection_leaf_and_the_join_ends_in_the_blocked_leaf
produces: none
consumes: none
proof: A process injected while a blocking member's admit runs ends in the blocked leaf and is recorded.
depends on:
- blocked by T53
T74 — Refuse a minted box id that a retained revocation record holds
slice: S10
kind: task
covers: BEP-070
size: M
est: 100 code, 100 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the
CollidingBoxIdrefusal names every source the check readsobservability: one warn line per colliding mint
layers:
- collide:
- crates/bep/src/control.rs: an owner-only control op returns the box ids the proxy's restored revocations hold
- crates/minvmd/src/box_registry.rs:
holds_box_idalso refuses an id that op returns, and theAllocationError::CollidingBoxIdtext names live rows, kept creations, attachments and revocation recordstest: cargo nextest run -p minvmd a_minted_id_a_revocation_holds_is_refused
produces: none
consumes: none
proof: A minted box id that only a retained revocation record holds is refused as a collision.
depends on:
- blocked by T22
T75 — Give bep serve a chain-preserving way out of an unreadable audit line
slice: S10
kind: task
covers: BEP-068
size: M
est: 120 code, 140 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the refusal names
bep serve --quarantine-unreadablein place of a hand editobservability: one warn line per quarantined segment, and a
quarantineaudit recordlayers:
- recover:
- crates/bep/src/audit.rs: a quarantine moves each segment holding an unreadable line into
<audit dir>/quarantine/and appends aquarantinerecord that carries the moved segment's final hash, so the chain stays verifiable; a record of a kind this bep does not know is told apart from a corrupt line- crates/bep/src/main.rs:
bep serve --quarantine-unreadableruns the quarantine and starts;refuse_unreadablenames that flagtest: cargo nextest run -p bep quarantine_keeps_the_chain_and_starts
produces: none
consumes: none
proof:
bep serve --quarantine-unreadablestarts past an unreadable audit line, and the chain still verifies across the quarantine.depends on:
- blocked by T33
T83 — Give bep serve the client's store rules so a store member is admitted
slice: S10
kind: task
covers: BEP-032
size: M
est: 140 code, 140 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: a CONNECT to an authority that no module and no store rule admits stays refused
off_moduleinmin box audit, and a store handle with no matching rule stays refusedstore_handle_invalidobservability: one info log line at start naming the count of store rules
bep serveholds, never a valuelayers:
- rules:
- crates/bep/src/main.rs:
bep servegets the client's[secret-store-rules]with the BEP-034 refusals applied and gives them toBoxRedeemer::with_rules; the rules' authorities joinregistered_unionand the admitted set, less the deny set; the three interim notes that still name T25 (#2280) are removed (#2599); how the rules reachbep serve(a read of the user-levelconfig.toml, or a control-socket op thatminsends) is this task's choice, left to the implementer and the arch review by the epic owner on 2026-10-26- crates/bep/src/redeemer.rs: the
factsdoc names the rulesbep servegives in place of the interim note- crates/bep/Cargo.toml:
bepdepends onboxesandpathsto read and check the client's rules (layer amended 2026-10-26: the arch review on #2614 accepted this delivery)- Cargo.lock: regenerated for the new
bepdependencies- crates/boxes/src/client/config.rs: one function merges the user and organization
[secret-store-rules]with the action floor and the deny check, forminandbep(arch C2 on #2614)- crates/minimal/src/config.rs:
mincalls that merge in place of its own (arch C2 on #2614)test: cargo nextest run -p bep serve_admits_a_store_member_under_the_clients_rule
produces: none
consumes: none
proof: A box's request to a
[secret-store-rules]rule's registered upstream, sent through a runningbep serve, reaches that upstream with the stored key in the rule's injection form.depends on:
- blocked by SP1
- blocked by T67
- blocked by T25
T84 — Keep a replaced client key verifying until the boxes minted under it end
slice: S10
kind: task
covers: BEP-063
size: M
est: 160 code, 160 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: when every registered client key is in use and the bound refuses a registration, the box creation fails with this text on stderr: "min did not create the box: the Box Egress Proxy already holds 16 client keys, and running boxes use each one. A key is freed when the last box created under it ends."
observability: one info log line when a kept key is retired, naming its
kidlayers:
- keep:
- crates/bep/src/control.rs:
replace_client_keykeeps a key registered while a running box's store handles or sign-in references are signed under it, and revokes it when the last such box ends; the bound drops the least recently registered key that no running box uses, and refuses the registration when every key is in use (owner ruling on gominimal/arch#144, 2026-10-26; #2605)- crates/minimal/src/mint.rs: the replacement keeps its pending-revoke file and lock, and a bound refusal fails the creation with the diagnostics text
- crates/bep/src/main.rs: the test literals of the registration record set its new
box_idfield (layer amended 2026-10-26 after round 1 of #2615)- crates/minimal/src/tests.rs: the test inventory names the new verify tests in place of
a_replaced_client_key_is_revoked_at_the_proxy(layer amended 2026-10-26 after round 1 of #2615)- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: the BEP-063 replace and bound sub-requirements state the keep and the in-use bound, name the two verify tests below, drop their known-gap notes, and the pending-revoke comment says the client removes the file (arch C1 on #2594)
test: cargo nextest run -p minimal a_replaced_client_key_keeps_verifying_until_its_boxes_end
test: cargo nextest run -p bep the_registry_bound_never_drops_a_key_a_running_box_uses
produces: none
consumes: none
proof: After a client key replacement, a box created under the old key keeps redeeming its store handles and sign-in references until it ends, and the registry bound never drops a key that a running box uses.
depends on:
- assumes OQ3
- blocked by T63
- blocked by T71
T76 — Round-two conditions closed, end to end
slice: S10
kind: integration
covers: BEP-032, BEP-067, BEP-063, BEP-018, BEP-070, BEP-068
size: M
est: 0 code, 120 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: none beyond the cases' own output
layers:
- e2e:
- scripts/session-e2e.sh: one walk over the round-two conditions: a store value injected by
bep serveunder a client rule, a replaced client key that keeps verifying, the auditjti, the key bound, the cohort flows and race, the revoked-id collision and the quarantine start; NOT RUN where its gate is missing (#2500)test: ./scripts/session-e2e.sh bep_round_two_conditions_closed
produces: none
consumes: none
proof: The second round of residuals is done: bep serve injects store values under the client's store rules, a replaced client key keeps verifying until its boxes end, the audit record carries the handle's jti, the key bound and kid diagnostics are in place, BEP-063 states revocation and replacement, a blocked cohort ends its earlier UDP 443 flows and its injected processes, a revoked id never returns, and an unreadable audit line has a chain-preserving way out. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-26 so the residuals dispatch as slices.
depends on:
- blocked by SP1
- assumes OQ3
- blocked by T67
- blocked by T68
- blocked by T69
- blocked by T70
- blocked by T71
- blocked by T72
- blocked by T73
- blocked by T74
- blocked by T75
- blocked by T83
- blocked by T84
T77 — Run the real-Keychain proof of min secret on a Mac
slice: S11
kind: task
covers: BEP-050, BEP-051
size: S
est: 0 code, 60 tests
executor: human
difficulty: routine
model: standard
diagnostics: the case prints PASS or NOT RUN with its gate
observability: the case output, posted on #2562
layers:
- mac:
- crates/bep/src/store.rs: run on a Mac against
main, unchanged;min secret set,listandrmrun against the login Keychain and the transcript is posted on #2562test: cargo nextest run -p bep --run-ignored ignored-only host_store_set_list_rm_round_trip
produces: none
consumes: none
proof: On a Mac,
min secret setstores a key in the real Keychain with access control that lets the proxy read it without a prompt, and the case records OK.depends on:
- blocked by T26
- blocked by T111
T78 — Run bep_git_credential_helper_sends_header on the macOS lane
slice: S11
kind: task
covers: BEP-015
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics: the case prints PASS or NOT RUN with its gate
observability: the case output, posted on #2563
layers:
- mac:
- scripts/session-e2e.sh: run on the manual macOS lane of #2500 against
main, with a GitHub sign-in andE2E_BEP_PRIVATE_REPO, unchangedtest: ./scripts/session-e2e.sh bep_git_credential_helper_sends_header
produces: none
consumes: none
proof: On the macOS lane, git in a grant box clones and pushes through the shipped credential helper, and the case records OK.
depends on:
- blocked by T50
- blocked by T111
T79 — Run the two revocation e2e legs on the macOS lane
slice: S11
kind: task
covers: BEP-043
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: the case output, posted on #2564
layers:
- mac:
- scripts/session-e2e.sh: run on the manual macOS lane of #2500 against
main, with a GitHub sign-in, unchangedtest: ./scripts/session-e2e.sh bep_destroyed_box_values_refused_within_60s
produces: none
consumes: none
proof: On the macOS lane, a destroyed box's values are refused within 60 seconds, a destroy completes with the proxy down (
bep_destroy_with_proxy_down_refuses_by_attachment), and both cases record OK.depends on:
- blocked by T20
- blocked by T111
T80 — Run bep_signing_rotation_keeps_running_box_working on the macOS lane
slice: S11
kind: task
covers: BEP-061
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics: the case prints PASS or NOT RUN with its gate
observability: the case output, posted on #2565
layers:
- mac:
- scripts/session-e2e.sh: run on the manual macOS lane of #2500 against
main, with a GitHub sign-in, unchangedtest: ./scripts/session-e2e.sh bep_signing_rotation_keeps_running_box_working
produces: none
consumes: none
proof: On the macOS lane, a running box keeps cloning and pushing across a signing-certificate rotation, and the case records OK.
depends on:
- blocked by T52
- blocked by T111
T81 — Run bep_box_recreated_under_its_name_redeems on the macOS lane
slice: S11
kind: task
covers: BEP-070
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics: the case prints PASS or NOT RUN with its gate
observability: the case output, posted on #2587
layers:
- mac:
- scripts/session-e2e.sh: run on the manual macOS lane of #2500 against
main, with a GitHub sign-in, unchangedtest: ./scripts/session-e2e.sh bep_box_recreated_under_its_name_redeems
produces: none
consumes: none
proof: On the macOS lane, a box re-created under the same name gets an id of its own and redeems, and the case records OK.
depends on:
- blocked by T22
- blocked by T111
T82 — Run the Box Egress Proxy legs on the macOS lane
slice: S11
kind: integration
covers: BEP-050, BEP-051, BEP-015, BEP-043, BEP-061, BEP-070
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: the walk's output, posted on #2500
layers:
- mac:
- scripts/session-e2e.sh: the full BEP walk on the manual macOS lane against
main, with a GitHub sign-in andE2E_BEP_PRIVATE_REPO, unchanged- scripts/session-e2e.sh:
bep_round_two_conditions_closedon the same lane withE2E_BEP_QUARANTINE=1, for its quarantine leg, on a host whose audit log may be moved aside (widened 2026-10-27: T76's quarantine leg needs a live proxy, which only this lane has)test: ./scripts/session-e2e.sh bep_live_gate
produces: none
consumes: none
proof: Every Box Egress Proxy leg that records NOT RUN on Linux records OK on the macOS lane against a live proxy with a sign-in. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-26 so the residuals dispatch as slices. The debug-build key task re-covers the key-custody requirements; the owner accepted that double coverage on 2026-10-31.
depends on:
- blocked by T77
- blocked by T78
- blocked by T79
- blocked by T80
- blocked by T81
- blocked by T111
T111 — Accept a key the keychain made exportable in a debug build of bep, and scope the custody rule to release builds
slice: S11
kind: task
covers: BEP-014, BEP-059
size: M
est: 90 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a debug build of
bepthat keeps an exportable key writes one warn line per role tobep.log,kept the proxy's <role> key although the keychain made it exportable: this is a debug build, and a release build refuses it; a release build still prints "the keychain made the key exportable" and "the proxy root key is exportable, so the proxy does not use it for a CA", pinned by a_release_build_refuses_an_exportable_key_at_both_sitesobservability: the warn line above, once per role at each start of a debug build, in the proxy's log; a release build writes no new line
layers:
- keys:
- crates/bep/src/keys.rs: the refusal at
if key.exportable()in the keychain'sgenerate()(about line 653) goes through one custody decision that both refusal sites share; in a debug build (cfg!(debug_assertions)) the decision keeps the key and writes the warn line, and in a release build it refuses as today; the decision takes the build as an argument, so a test on every host pins both builds- crates/bep/src/ca.rs: the CA key check at
if key.exportable()(about line 721), which returnsCaError::Exportable, takes the same decision, so a debug build that kept an exportable root or signing key also starts its CA;ca_refuses_a_wrong_role_or_exportable_keypins the release refusal and the debug keeptest: cargo nextest run -p bep ca_refuses_a_wrong_role_or_exportable_key
test: cargo nextest run -p bep signing_key_is_non_exportable_and_has_no_file
test: cargo nextest run -p bep sealing_and_root_keys_persist_non_exportable
- start:
- crates/bep/src/main.rs: two start tests over keys that report
exportable(): a debug build creates its keys, builds its CA and starts serving, and a release build refuses at the generation site and at the CA site, each with its own texttest: cargo nextest run -p bep a_debug_build_starts_with_exportable_keys
test: cargo nextest run -p bep a_release_build_refuses_an_exportable_key_at_both_sites
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: BEP-014 and BEP-059 say a release build of the proxy holds its keys non-exportable; a note on each says why a debug build keeps an exportable key with a warning: the macOS login keychain lets the process that made a key export it although
kSecAttrIsExtractableis false (the probe on #2436), and the keychain that enforces it needs an entitlement only a Developer ID signature carries, so a debug build would never start on a developer's Mac (decided 2026-10-31, owner: this spec note is enough, and the architecture's key-custody rule stays as it is)produces: none
consumes: none
proof: A debug build of bep on a developer's Mac starts and serves with keys the login keychain made exportable, and writes a warning for each, while a release build still refuses such a key.
depends on:
- blocked by T2
- blocked by T3
- assumes OQ4
T85 — Name the handle jti in spec 24 BEP-039
slice: S12
kind: task
covers: BEP-039
size: S
est: 0 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics: none: the task changes only the spec text, so no tool prints anything new.
observability: none: the task changes only the spec text, so no trace, metric or log line changes.
layers:
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: BEP-039's field list and the property under it name
jti, the handle's correlation id, which on a refusal is the unverified claim (Gatehouse §6.10 line 842; #2600)test: cargo nextest run -p bep a_store_handle_audit_record_carries_its_jti
produces: none
consumes: none
proof: BEP-039 lists every field of a decision record, the handle's jti included.
depends on:
- blocked by T68
T86 — State the audit quarantine in spec 24 BEP-041 and BEP-068
slice: S12
kind: task
covers: BEP-041
size: S
est: 0 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics: none: the task changes only the spec text, so no tool prints anything new.
observability: none: the task changes only the spec text, so no trace, metric or log line changes.
layers:
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: BEP-041 lets
bep serve --quarantine-unreadablemove a segment with an unreadable line out of the restored set after it re-records the segment's revocations, and BEP-068 names thequarantinerecord, thequarantine_restatedmarker and the warn line (#2613)test: cargo nextest run -p bep forged_quarantine_record_bridges_no_gap
produces: none
consumes: none
proof: BEP-041 and BEP-068 state the quarantine move, its record and its marker.
depends on:
- blocked by T75
T87 — Prove on a real kernel that a blocked cohort rejects a pre-flip UDP 443 socket
slice: S12
kind: task
covers: BEP-018
size: M
est: 40 code, 160 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: the leg records NOT RUN with its gate where the host has no root netns or no nft
observability: one line per step of the leg
layers:
- kernel:
- crates/minimald/tests/netns_root_integration.rs: a leg that opens a UDP 443 socket in a host_ip member's leaf before the cohort flips to blocked, expects ECONNREFUSED on the next datagram after the flip, and a send again after the block lifts, against the loaded classifier table (#2606)
test: cargo nextest run -p minimald --run-ignored only a_blocked_cohort_rejects_a_pre_flip_udp_443_socket
produces: none
consumes: none
proof: On a real kernel, a host_ip member's UDP 443 socket opened before the cohort turns blocked is rejected after the flip and sends again after the block lifts.
depends on:
- blocked by T72
T88 — Name the QUIC block helper in min finalize-install
slice: S12
kind: task
covers: BEP-018
size: S
est: 60 code, 60 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: when the helper or its sudoers drop-in is missing or not root-owned, the item prints: "QUIC block helper: not installed at /usr/local/lib/minimal/quic443-leaf. Run
min finalize-installto install it."observability: none: finalize-install is a one-shot command whose item line is its whole output.
layers:
- finalize:
- crates/minimal/src/cmd/finalize_install.rs: an item
quic443-helperthat is done when the helper and its sudoers drop-in exist root-owned, and otherwise names the path and the step (#2607)test: cargo nextest run -p minimal finalize_install_names_the_quic_block_helper
produces: none
consumes: none
proof: min finalize-install shows the QUIC block helper item as done or names its path and the step that installs it.
depends on:
- blocked by T72
T89 — Check a minted box id against retained revocations when no proxy runs
slice: S12
kind: task
covers: BEP-070
size: M
est: 100 code, 100 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: a refused id names the revocation record that holds it
observability: one warn line per colliding mint
layers:
- collide:
- crates/minvmd/src/box_registry.rs: when no proxy answers,
holds_box_idreads the box ids that the retained audit segments' revocation records hold, or refuses creation; the choice is the implementer's and the arch review's (#2616)- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: the BEP-070 limit comment states the no-proxy case as the code does it
test: cargo nextest run -p minvmd a_minted_id_a_revocation_holds_is_refused_with_no_proxy
produces: none
consumes: none
proof: A minted box id that a retained revocation record holds is refused with no proxy running.
depends on:
- blocked by T74
T90 — Free the client key of a box that ended without forget_box
slice: S12
kind: task
covers: BEP-063
size: M
est: 120 code, 120 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: none beyond the bound's refusal text
observability: one info log line per key freed at start, naming its kid
layers:
- keys:
- crates/bep/src/control.rs: at start, each key's
boxes=set is reconciled against the boxes that are still registered, so a box that ended with noforget_box(a Ctrl-C before minvmd records it, a stopped minvmd, a failed release write) no longer holds its key in use (#2617)- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: the BEP-063 known-gap text that links #2617 is removed
test: cargo nextest run -p bep a_stranded_box_frees_its_key_at_start
produces: none
consumes: none
proof: After a restart, a box that ended without forget_box no longer holds its client key in use, and the bound counts only keys that running boxes use.
depends on:
- blocked by T84
- assumes OQ3
T91 — Admit a widened store rule without restarting bep serve
slice: S12
kind: task
covers: BEP-032
size: M
est: 120 code, 100 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: none: a widened rule admits its upstream as any other admitted upstream, and the log line names the re-sign.
observability: one info log line when the root re-signs or the admitted set changes
layers:
- admit:
- crates/bep/src/main.rs: a changed
[secret-store-rules]re-signs the root with the new names under the same key and subject, and rebuilds theAdmissionset (#2618)- crates/bep/src/proxy.rs:
Admissiontakes a source that it asks at each CONNECTtest: cargo nextest run -p bep a_widened_store_rule_is_admitted_without_a_restart
produces: none
consumes: none
proof: A store rule that adds an upstream is admitted without restarting bep serve.
depends on:
- blocked by T83
- blocked by SP1
T92 — Keep store rules under min --config-dir in step with bep serve
slice: S12
."kind: task
covers: BEP-032
size: S
est: 60 code, 60 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: when min would mint under a rule bep serve does not read, it refuses: "min did not mint a store handle for : bep serve reads store rules from , not from --config-dir
observability: none: the refusal prints to the person running min, and min logs nothing for a mint.
layers:
- config:
- crates/minimal/src/mint.rs: a store handle under a rule that exists only below a
--config-diroverride is refused with the diagnostics text, or the override reaches bep serve; the choice is the implementer's and the arch review's (#2620)- crates/bep/src/main.rs: the
client_store_rulesdoc names the behaviour in place of the #2620 linktest: cargo nextest run -p minimal a_store_rule_under_config_dir_is_minted_only_when_bep_reads_it
produces: none
consumes: none
proof: A store rule written under min --config-dir is either read by bep serve or refused at mint with the reason.
depends on:
- blocked by T83
- blocked by SP1
T93 — Remove the QUIC block helper and its sudoers rule on uninstall
slice: S12
kind: task
covers: BEP-018
size: M
est: 20 code, 180 tests
executor: agent
difficulty: routine
model: standard
diagnostics: uninstall names the helper and its sudoers rule among the finalize-install files it lists.
observability: none: the installer is a shell script that writes no log.
layers:
- uninstall:
- scripts/install.sh: the uninstall list and its
_cls_foundcheck name/usr/local/lib/minimal/quic443-leafand/etc/sudoers.d/minimal-quic443-leaf(#2631)- scripts/install_test.sh: after the uninstall rehearsal neither file exists
test: just test-installer finalize_install_uninstall
produces: none
consumes: none
proof: Uninstall removes the QUIC block helper and its sudoers rule.
depends on:
- blocked by T72
T94 — Round-three conditions closed, end to end
slice: S12
kind: integration
covers: BEP-039, BEP-041, BEP-018, BEP-070, BEP-063, BEP-032
size: M
est: 0 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: none beyond the cases' own output
layers:
- e2e:
- scripts/session-e2e.sh: one walk over the round-three conditions; NOT RUN where its gate is missing
test: ./scripts/session-e2e.sh bep_round_three_conditions_closed
produces: none
consumes: none
proof: The third round of residuals is done: the spec names the audit jti and the quarantine, a real kernel rejects a blocked cohort's pre-flip UDP 443 socket, min finalize-install names the QUIC block helper and uninstall removes it, a minted id is checked with no proxy running, a stranded box frees its client key, a widened store rule and a min --config-dir rule reach bep serve without a restart. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-26 so the residuals dispatch as slices.
depends on:
- blocked by T85
- blocked by T86
- blocked by T87
- blocked by T88
- blocked by T89
- blocked by T90
- blocked by T91
- blocked by T92
- blocked by T93
- blocked by SP1
- assumes OQ3
T95 — Stop node_port_assigned_on_host_and_handed_to_daemon racing a parallel test for its port
slice: S13
kind: enablement
covers: none
size: S
est: 20 code, 60 tests
executor: agent
difficulty: routine
model: standard
diagnostics: none: the task changes a unit test, so no tool prints anything new.
observability: none: the task changes a unit test, so no trace, metric or log line changes.
layers:
- flake:
- crates/minvmd/src/cmd/run.rs: the step that releases a port and expects
assign_node_portto hand it back as asked no longer fails when another test in the process takes the released port first (#2653)test: cargo nextest run -p minvmd node_port_assigned_on_host_and_handed_to_daemon
produces: none
consumes: none
proof: node_port_assigned_on_host_and_handed_to_daemon passes under a loaded parallel run.
T96 — Run bep_revoke_and_sign_out_end_to_end live on a Mac
slice: S13
kind: integration
covers: BEP-043, BEP-044
size: M
est: 0 code, 200 tests
executor: human
difficulty: routine
model: standard
diagnostics: the case prints each step line and
Revoke and sign out OK, or NOT RUN with its gateobservability: the case output, posted on #2655
layers:
- mac:
- scripts/session-e2e.sh: run by a person on a Mac with a live Box Egress Proxy and a GitHub sign-in, with
E2E_BEP_LOGOUT=1, againstmain, unchanged; the person enters the device code (#2655)test: ./scripts/session-e2e.sh bep_revoke_and_sign_out_end_to_end
produces: none
consumes: none
proof: The fourth round of residuals is done: the minvmd node-port test no longer races a parallel test for its released port, a person has run the revoke and sign-out case live on a Mac, a box spec writes a store reference as source = "store" with store and id, and box creation seals a store handle for each accepted store rule. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-26 so the residuals dispatch as slices.
depends on:
- blocked by T23
- blocked by T95
- blocked by T97
- blocked by T98
- blocked by T111
T97 — Accept source = "store" with store and id in a box spec's grant
slice: S13
kind: task
covers: BEP-032
size: M
est: 160 code, 240 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a store reference spelled with
source = "store"and nostoreoridis refused with text that names the missing fieldobservability: none: the parse runs inside
minbox creation, which logs no line for a grantlayers:
- grant:
- crates/boxes/src/core/bep.rs:
GrantSourceacceptssource = "store"withstoreandid, andstore_references()reads store references from that form, not from brokerscopes(#2656)- scripts/session-e2e.sh: the seed of
proof_bep_store_reference_end_to_endwritessource = "store",store = "keychain"andid, and the# Interim spelling:comment goestest: cargo nextest run -p boxes a_store_grant_parses_store_and_id
- review:
- crates/minimal/src/cmd/box_spec.rs:
renderreads each store row fromboxes::core::bep::store_references()of thesource = "store"form, not fromgrant.scopes; its test builds the reference in that form (widened 2026-10-31, owner, #2656 noop)test: cargo nextest run -p minimal box_spec_lists_store_references_with_their_authorities
- create:
- crates/minimal/src/cmd/boxes/create.rs: the tests of
accept_store_referencesbuild store references assource = "store"withstoreandid(widened 2026-10-31, owner, #2656 noop)test: cargo nextest run -p minimal store_reference_without_rule_is_refused_at_creation
test: cargo nextest run -p minimal store_rule_outside_egress_exits_3
produces: none
consumes: none
proof: A box spec that declares a store reference as source = "store" with store and id mints and redeems it, and
min box speclists it with its authorities.depends on:
- blocked by T29
- blocked by SP1
T98 — Seal a store handle per accepted store rule at box creation
slice: S13
kind: task
covers: BEP-063, BEP-032
size: M
est: 160 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box specandmin box audit <box>show each store member the box carries, as T60 and T85 print them today, now with a callerobservability: none new: each redemption of the sealed member writes the BEP-039 audit record T85 writes, with the handle's jti
layers:
- mint:
- crates/minimal/src/mint.rs:
mint_store_handleandstore_claimsgain their caller, and thedead_codeexpectations that name #2525 go, and so do the module doc's sentences that saymint_store_handlehas no caller and that #2526 owns themin box specstore references- crates/minimal/src/cmd/boxes/create.rs: box creation mints a store handle for each accepted store rule and seals it as a
storemember (#2525), in place of_accepted_store_rulestest: cargo nextest run -p minimal box_creation_seals_a_store_member_per_accepted_rule
- audit:
- crates/minimal/src/cmd/box_audit.rs: a test renders a refused store-handle record and pins
store_handle_invalid, the item as<store>:<id>andjti=<jti>on its line, with the subject in the spelling the reader matches (moved here 2026-10-31, owner, from #2468)test: cargo nextest run -p minimal box_audit_shows_a_refused_store_handle_with_its_item_and_jti
produces: none
consumes: none
proof: A box created with an accepted store rule carries its store handle sealed as a store member, and the proxy redeems it into the store value that the rule admits. Its tasks re-cover requirements whose origin tasks merged with an arch condition held on a filed residual; the owner accepted that double coverage on 2026-10-26 so the residuals dispatch as slices.
depends on:
- blocked by T27
- blocked by T60
- blocked by T97
- blocked by SP1
- assumes OQ3
T100 — Stage bep beside minvmd in the macOS release and install it
slice: S14
kind: enablement
covers: none
size: S
est: 30 code, 160 tests
executor: agent
difficulty: routine
model: standard
diagnostics: the installer prints
bepin its component list besideminvmdon a macOS installobservability: none: the stager and the installer are shell scripts that write no log
layers:
- stage:
- scripts/stage-release.sh: a darwin row
"bep|darwin|arm64|file|bin/bep|bep-macos-arm64"beside theminvmdrow, so the installer putsbepwhereminvmdresolves it (#2498)- scripts/stage-release_test.sh: the darwin manifest carries the
beprow- scripts/install_test.sh: a mock
bep-macos-arm64in the darwin release, and a case that findsbin/bepbesidebin/minvmdafter install and gone after uninstalltest: just test-installer bep_installs_beside_minvmd
produces: none
consumes: none
proof: A macOS install from a release puts
bepbesideminvmd, and an uninstall removes it.depends on:
- blocked by T101
T101 — Build, sign and notarize bep in the macOS release
slice: S14
kind: enablement
covers: none
size: M
est: 40 code, 60 tests
executor: human
difficulty: needs care
model: standard
diagnostics: the release run's artefact list names
bep-macos-arm64, andcodesign -dvon it shows the Developer ID teamobservability: none: the release workflow's own job log is the record
layers:
- release:
- .github/workflows/release.yml: the macOS job builds
cargo build --release --locked -p bep --bin bepwithMINIMAL_GITHUB_APP_CLIENT_SECRETset (a browser-flow sign-in renews at the proxy only with it), renames itbep-macos-arm64, signs it with the Developer ID besideminvmd, notarizes it and uploads it (#2498); the first signedbepthis job ships is the key-custody probe for #2436 (owner decision 2026-10-31): it must start on a stock Mac with non-exportable keys, and if it refuses an exportable key the custody fix lands before T102test: just test-installer bep_installs_beside_minvmd
produces: none
consumes: none
proof: A release carries a Developer-ID-signed, notarized
bep-macos-arm64built with the App's client secret.T102 — Installed hosts run the proxy, end to end
slice: S14
kind: integration
covers: none
size: M
est: 0 code, 100 tests
executor: human
difficulty: routine
model: standard
diagnostics:
bep.logbeside the host daemon's state shows the proxy listening, andmin box auditshows the admitobservability: the proxy's connection spans in
bep.logname the box and the authoritylayers:
- mac:
- scripts/session-e2e.sh:
bep_first_slice_end_to_endon a Mac installed from the release, with a GitHub sign-in andE2E_BEP_PRIVATE_REPO, unchanged; the person also runs the close-out walk's stories 01 to 03 and posts both results on the epictest: ./scripts/session-e2e.sh bep_first_slice_end_to_end
produces: none
consumes: none
proof: On a Mac installed from a release, sign in with
min auth login, and a GitHub grant box runsgh api userthrough the proxy with no extra step.depends on:
- blocked by T100
- blocked by T101
T103 — Match min box audit on the box id spelling the proxy writes
slice: S15
kind: task
covers: BEP-042
size: M
est: 60 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <id>andmin box audit <name>list the records the proxy wrote for that box; the line "Showing (box )." keeps the hyphenated id a person sees inmin ls, pinned by box_audit_reads_a_record_the_proxy_wrote_for_a_minted_idobservability: the existing debug line of each read names the box ids it matched in the proxy's spelling, 32 lowercase hex digits
layers:
- audit:
- crates/minimal/src/cmd/box_audit.rs:
resolve(),render()andchildren()match a record'ssubjectagainst the box id in the proxy's one text form,bep::listener::box_id_text(32 lowercase hex digits), not the hyphenatedSessionIddisplay form, somin box auditshows the records the proxy writes (#2462); the name rule keeps choosing the live box, else the newest creation by UUIDv7 order; the test fixtures write subjects in the proxy's form, asbep servedoestest: cargo nextest run -p minimal box_audit_filters_to_one_box
test: cargo nextest run -p minimal box_audit_name_reads_live_else_latest_creation
test: cargo nextest run -p minimal box_audit_reads_a_record_the_proxy_wrote_for_a_minted_id
produces: none
consumes: n 10BC8 one
proof:
min box audit <box>lists the decisions the running proxy recorded for a box created on this host, by id and by name.depends on:
- blocked by T17
- blocked by T22
T104 — Read each box's parent so min box audit --parent merges its children
slice: S15
kind: task
covers: BEP-042
size: M
est: 80 code, 120 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box audit --parent <box>lists each child box's records with the child's id at the start of each line; a box with no child prints "Box has no child boxes." on stderr; the refusal "min box audit --parentcannot run here: this host does not record which boxes are children of box . Runmin box audit <box>for each child box." goes, pinned by box_audit_parent_reads_parents_from_the_box_recordsobservability: the existing debug line of each read names the child box ids it merged
layers:
- audit:
- crates/minimal/src/cmd/box_audit.rs:
known_boxesreads each listed box's parent from its record (boxes::Record::parent, which the daemon stamps at creation since #2386),LISTING_RECORDS_PARENTS,refuse_parent_before_listingand the refusal text go, and the tests driverunwith the parentsknown_boxesgives, the pathmin box audittakes, not with a hard-codedparents_recorded = true(BEP-042--parent)test: cargo nextest run -p minimal box_audit_parent_merges_children
test: cargo nextest run -p minimal box_audit_parent_reads_parents_from_the_box_records
produces: none
consumes: none
proof:
min box audit --parent <box>merges the records of every child of that box, each line naming its child, on a host whose boxes carry a parent.depends on:
- blocked by T103
T105 — Strip Alt-Svc on the live response relay and point the HTTP checks at the live path
slice: S15
kind: task
covers: BEP-076
size: L
est: 260 code, 360 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: a box that requests through the proxy sees no
Alt-Svcheader in any relayed response, so a client never learns an HTTP/3 endpoint to try past the proxy; the box sees the first chunk of a response before the upstream sends the rest, as beforeobservability: one debug line per relayed response that carried an
Alt-Svcheader, naming the box and the authority, beside the flow's existing spanlayers:
- relay:
- crates/bep/src/forward.rs:
relay_responsesreads each response head on the flow, removes everyAlt-Svcheader (BEP-076), writes the head, then streams the body chunks as they arrive by the response's framing (BEP-N01), for every response on a kept-alive flow; the testsalt_svc_is_stripped_from_responses,h2_prior_knowledge_preface_is_closed,prop_forwarded_head_is_canonicalandprop_ambiguous_framing_is_refuseddriveforward::forward_requestsandrelay_responses, the pathbep serverunstest: cargo nextest run -p bep alt_svc_is_stripped_from_responses
test: cargo nextest run -p bep h2_prior_knowledge_preface_is_closed
test: cargo nextest run -p bep prop_forwarded_head_is_canonical
test: cargo nextest run -p bep prop_ambiguous_framing_is_refused
test: cargo nextest run -p bep streamed_response_is_not_buffered
- http:
- crates/bep/src/http.rs: the four tests of the same names, which drive
relay_response_headandforward_request_headthat no live path calls, go; the response-head parse andstrip_alt_svcserve the live relaytest: cargo nextest run -p bep ambiguous_response_framing_is_refused
produces: none
consumes: none
proof: A response relayed to a box through the running proxy carries no Alt-Svc header, and the box still gets each chunk as the upstream sends it.
depends on:
- blocked by T8
- blocked by T11
T106 — Audit a refused ambiguous request head on the live path and show it in min box audit
slice: S15
kind: task
covers: BEP-039
size: S
est: 30 code, 120 tests
executor: agent
difficulty: routine
model: light
diagnostics:
min box audit <box>lists a request head the proxy refused for ambiguous framing as arefuseline with the markerframing_ambiguous, pinned by box_audit_lists_a_refused_ambiguous_headobservability: the proxy's log line for the refused head names the box and the framing rule; the audit record carries the marker, since BEP-039's record has no field for the rule
layers:
- proxy:
- crates/bep/src/forward.rs: a test sends an ambiguous head through
forward::forward_requestsand reads oneframing_ambiguousrecord for the box from the audit log; the refusal's log line names the rule (#2435)- crates/bep/src/http.rs: the
AuditSinkdoc says the live path writes throughFlow::record, and that the trait serves this module's own teststest: cargo nextest run -p bep live_path_audits_a_framing_ambiguous_head
- audit:
- crates/minimal/src/cmd/box_audit.rs: a test renders a
framing_ambiguousrecord thatbep servewrote, in the proxy's id spellingtest: cargo nextest run -p minimal box_audit_lists_a_refused_ambiguous_head
produces: none
consumes: none
proof: After the proxy refuses a box's request head for ambiguous framing,
min box audit <box>lists the refusal with the marker framing_ambiguous.depends on:
- blocked by T103
- blocked by T105
T107 — Prove that the log bep serve writes is the log min box audit reads
slice: S15
kind: task
covers: BEP-068
size: S
est: 0 code, 60 tests
executor: agent
difficulty: routine
model: light
diagnostics:
min box audit <box>lists the records thatbep servewrote toaudit-<n>.jsonlunder~/.minimal/bep/auditobservability: the proxy's start line names the active segment, as today
layers:
- serve:
- crates/bep/src/main.rs: a test starts the audit log as
bep servedoes (start_audit_logover a directory), appends a record, and reads it back withLogReader::read_newover the same directory, under theaudit-<n>.jsonlname (#2460)test: cargo nextest run -p bep serve_writes_audit_segments_the_reader_reads
produces: none
consumes: none
proof: A record bep serve appends is read back by the reader min box audit uses, from audit-.jsonl in the same directory.
depends on:
- blocked by T33
T108 — Say in control.rs that bep serve sets the client-key bound
slice: S15
kind: task
covers: BEP-063
size: S
est: 5 code, 60 tests
executor: agent
difficulty: routine
model: light
diagnostics: none: the task changes a doc comment, so nothing prints anything new.
observability: none: the task changes a doc comment, so no trace, metric or log line changes.
layers:
- docs:
- crates/bep/src/control.rs: the doc of
Control::bind_with_key_boundsaysbep servecalls it with the value of--max-client-keys, in place of "The proxy binary does not call it, so its bound is the fixed default." (#2598)test: cargo nextest run -p bep the_registry_never_holds_more_keys_than_its_bound
produces: none
consumes: none
proof: The control socket's docs say that bep serve sets the client-key bound with --max-client-keys.
depends on:
- blocked by T69
- assumes OQ3
T109 — Name the cause and the next step in the grant and proxy refusals
slice: S15
kind: task
covers: BEP-003
size: M
est: 180 code, 300 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each refusal below states its cause and one next step in the words the person types, pinned by the tests in its layer
observability: the existing info line of each refused creation keeps its rule, fields and code; each proxy refusal keeps its audit marker
layers:
- create:
- crates/minimal/src/cmd/boxes/create.rs: when no proxy serves boxes, the check tells apart a
bepthat is not installed (the proxy binary thatbep::store::proxy_binary_pathresolves does not exist) from an installed proxy that does not answer, and passes the cause to the refusal- crates/boxes/src/core/bep.rs: the exit-5 refusal no longer says only "Remove the grant". With no
bepinstalled it reads "Cannot create the box: it declares the GitHub grant , andbep, the Box Egress Proxy, is not installed at , so no proxy serves boxes on this machine. To create the box without the grant, remove from the [secrets] table in minimal.toml." Withbepinstalled and not answering, it says the proxy at is installed and does not answer, names the log file the host daemon writes for it, and keeps the same sentence about removing the grant- crates/minimal/src/tests.rs:
a_grant_box_with_no_proxy_is_refused_as_an_unenrolled_nodepins both texts and exit 5test: cargo nextest run -p minimal a_grant_box_with_no_proxy_is_refused_as_an_unenrolled_node
test: cargo nextest run -p boxes creation_without_sign_in_fails_with_defined_error
- json:
- crates/minimal/src/main.rs:
machine_mode_answergives a refused activation's defined error as the-o jsoncode(github_sign_in_required,gatehouse_unenrolled_node), notunspecified, with the refusal's next step as the hint: "runmin auth login, then create the box again" for the sign-intest: cargo nextest run -p minimal machine_mode_names_the_grant_refusal_code
- proxy:
- crates/bep/src/forward.rs:
refusal_bodysplits the one text "it did not accept the sealed credential in the request" by cause.github_sign_in_requiredreads "The Box Egress Proxy refused the request to , because the GitHub sign-in on the host has ended. On the host, runmin auth login, then send the request again."expired,revoked,undecryptableandunbounded_membersay the box's sealed credential no longer works on this host, and that re-creating the box gives it a new one.no_live_boxandcross_boxsay the sealed credential belongs to another box.off_port,outside_bound_setandoutside_current_setsay is not a host this credential is sent to.store_handle_invalidsays no rule in[secret-store-rules]on the host admits this stored secret fortest: cargo nextest run -p bep each_refusal_names_its_cause_and_next_step
produces: none
consumes: none
proof: A grant box refused for a missing proxy, a refusal read as -o json, and a request the proxy refuses each tell the person the cause and one next step they can take.
depends on:
- blocked by T13
- blocked by T62
T110 — Make bep --replace-key keep the old key until the new key exists
slice: S15
kind: task
covers: BEP-060
size: M
est: 140 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: when no new key can be made,
bep --replace-key <role>prints "The key was not replaced, because no new key was made: . The old key is still in use." and exits non-zero; the text "Runbep --replace-key <role>again." goes, pinned by replace_key_failure_keeps_the_old_key_in_useobservability: the warn line "replaced a proxy key" names the new key's fingerprint, as today; a failed generation writes one warn line naming the role and the cause, and that the old key is kept
layers:
- keys:
- crates/bep/src/keys.rs:
replacegenerates the new key beside the held one under a second label, then removes the old key and moves the new key to the role's label; when the generation fails, the old key stays andreplacereturns an error that says so; the memory store and the keychain store both do this, and a failed move of the label removes the new key and keeps the old onetest: cargo nextest run -p bep a_failed_generation_keeps_the_old_key
test: cargo nextest run -p bep refused_key_names_the_replacement_command
- command:
- crates/bep/src/main.rs:
replace_keyprints the text above for a failed generation, and the identity file is left as it wastest: cargo nextest run -p bep replace_key_failure_keeps_the_old_key_in_use
produces: none
consumes: none
proof: When bep --replace-key cannot make a new key, the old key stays in use and the message names the cause, not the same command again.
depends on:
- blocked by T2
- blocked by T111
T112 — Audit, HTTP and message conditions, end to end
slice: S15
kind: integration
covers: none
size: M
est: 0 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: none beyond the cases' own output
layers:
- e2e:
- scripts/session-e2e.sh: one walk over the slice:
min box audit <box>lists a running grant box's decisions by id and by name,--parentmerges a child box's records, a relayed response through the proxy carries noAlt-Svc, and a grant box withMINVMD_BEP_BINnaming a missing file is refused with exit 5 and the install text; NOT RUN where its gate is missingtest: ./scripts/session-e2e.sh bep_audit_http_and_messages_closed
produces: none
consumes: none
proof:
min box auditshows the records the proxy wrote for a box and merges a parent's children with--parent, a relayed response carries no Alt-Svc header on the live path, a refused ambiguous request head shows inmin box audit, the logbep servewrites is the logmin box auditreads, the missing-proxy refusal, the sign-in refusal read as-o jsonand the proxy's credential refusals each name their cause and one next step, andbep --replace-keykeeps the old key when it cannot make a new one. The owner added these conditions from the close-out walk on 2026-10-31. Its tasks re-cover requirements whose origin tasks merged with the gap open; the owner accepted that double coverage on 2026-10-31 so the conditions dispatch as one slice.depends on:
- blocked by T103
- blocked by T104
- blocked by T105
- blocked by T106
- blocked by T107
- blocked by T108
- blocked by T109
- blocked by T110
T113 — Admit dns and both steering where the host's switch resolves box names
slice: S16
kind: task
covers: BEP-062, BEP-078
size: M
est: 120 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: on a host whose switch steers box lookups, a grant box with
steering = "dns"or"both"is created, andmin box specshows the steering it resolves to; on a native host it is still refused with exit 3 and "This box declares a secret with steering = "dns", but this host has no box DNS resolver. Set steering = "proxy_env" in [network.bep].", pinned by dns_steering_is_admitted_where_the_switch_resolves_boxesobservability: the existing info line of each creation's activation check carries the host fact it used,
box_zone_resolver, beside its rule and fieldslayers:
- activation:
- crates/minimal/src/cmd/boxes/create.rs:
refuse_unhonourable_grantssetsHostFacts::box_zone_resolverfrom the host, not fromHostFacts::LOCAL: true where the box's lookups go through the VM host's switch, whose DNS gate steers the credential module's names and asks the proxy's listener (ProxyListenerState::for_switch, T58), and false on a native host, where a steered name always fails closed; the doc line "Every host today is un-enrolled and has no box-zone resolver." goes (#2523)- crates/minimal/src/cmd/box_spec.rs: the preview reads the same host fact, so
min box specand creation agree- crates/boxes/src/core/bep.rs: the doc of
HostFacts::box_zone_resolverand ofHostFacts::LOCALsay where the fact comes from; the BEP-017 refusal is unchanged where the fact is falsetest: cargo nextest run -p minimal dns_steering_is_admitted_where_the_switch_resolves_boxes
test: cargo nextest run -p boxes prop_dns_steering_without_resolver_is_exit_3
- gate:
- crates/minimald/src/net/switch.rs: a test builds a
dns-steered box's gate as the VM host attaches it, and pins that github.com resolves to the proxy's address while its listener is live and gets SERVFAIL while it is down, never a real addresstest: cargo nextest run -p minimald a_dns_steered_grant_box_resolves_the_module_through_the_switch
produces: none
consumes: none
proof: On a host whose switch resolves box names, a GitHub grant box with dns steering is created, git reaches GitHub through the proxy with no proxy variables, and its credentialed names fail closed while the proxy is down.
depends on:
- blocked by T13
- blocked by T30
- blocked by T58
T114 — Steer a dns box's store-rule authorities to the proxy
slice: S16
kind: task
covers: BEP-062
size: M
est: 140 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: a
dns-steered box with a store reference resolves its rule's upstream, such as api.openai.com, to the proxy's address while the proxy listens, and gets SERVFAIL while it does not, pinned by store_authority_of_a_dns_box_resolves_to_the_proxyobservability: the gate's existing debug line for a steered answer names the box and the steered name
layers:
- policy:
- crates/boxes/src/lib.rs:
CredentialedUpstreamcarries the upstream authorities of the store rules the creation accepted, written only when the box has one, so a daemon that predates the field still reads a policy without store references- crates/minimal/src/cmd/boxes/create.rs: creation puts each accepted store rule's
upstreaminto the box's policy beside its grants (#2575)test: cargo nextest run -p minimal accepted_store_rule_authorities_reach_the_box_policy
- gate:
- crates/minimald/src/net/switch.rs: the steered set of a
dnsorbothbox is the GitHub module's hosts and the store-rule authorities its policy carries, in place ofcredential_module_hosts()alone; the full literals ofCredentialedUpstreamin this file's tests gain the field- crates/minimald/src/net/dns_gate.rs: the full literals of
CredentialedUpstreamin this file's tests gain the field- crates/minimald/src/box_manager/tests.rs: the full literals of
CredentialedUpstreamgain the fieldtest: cargo nextest run -p minimald store_authority_of_a_dns_box_resolves_to_the_proxy
produces: none
consumes: none
proof: A dns-steered box that references a stored key resolves its rule's upstream to the proxy, and the request carries the stored key while the key never enters the box.
depends on:
- blocked by T113
- blocked by T98
T115 — DNS steering through the box-zone resolver, end to end
slice: S16
kind: integration
covers: none
size: M
est: 0 code, 200 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each case prints PASS or NOT RUN with its gate
observability: none beyond the cases' own output
layers:
- e2e:
- scripts/session-e2e.sh: one walk over the slice on a VM host: a grant box with
steering = "dns"and no proxy variables clones a private repository through the proxy, a store reference's upstream resolves to the proxy and its request carries the stored key, and with the proxy stopped both names get SERVFAIL; NOT RUN where its gate is missingtest: ./scripts/session-e2e.sh bep_dns_steering_through_the_box_zone_resolver
produces: none
consumes: none
proof: On a host whose switch resolves box names, a box with dns steering reaches GitHub and its stored-key upstreams through the proxy with no proxy variables, and those names fail closed while the proxy is down. Its tasks re-cover requirements whose first tasks merged with the steering wired and the refusal still in place; the owner planned them into this epic on 2026-10-31 so they dispatch as one slice.
depends on:
- blocked by T113
- blocked by T114
- blocked by T116
T116 — Carry dns-steered TLS to the proxy on port 443
slice: S16
kind: task
covers: BEP-080
size: L
est: 320 code, 380 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>shows a request admitted by its SNI authority on port 443, and a refused one markedoff_module; the proxy's debug log names the entry form; adns-steered GitHub grant box on a VM host is created, where before it was refused with exit 3 (#2805)observability: the proxy's
box_connectionspan and its debug line carry the entry form,connectordirect_tls, beside the authority; the per-request audit record keeps the BEP-039 fieldslayers:
- proxy:
- crates/bep/src/proxy.rs: a connection whose first bytes are a TLS ClientHello, not a CONNECT or an absolute-form request, takes its authority from the SNI and enters the same termination, substitution and egress re-check a CONNECT to that authority enters; no SNI, or an SNI outside the module host sets and the registered store authorities, is refused
off_moduletest: cargo nextest run -p bep direct_tls_on_443_routes_by_sni
test: cargo nextest run -p bep direct_tls_without_admitted_sni_is_refused_off_module
- listener:
- crates/switch/src/bep_host.rs: the proxy's address takes TCP connections on port 443 beside
PROXY_PORT, delivered to the proxy with the same box attribution, and the pool counts both ports as one lanetest: cargo nextest run -p switch proxy_address_delivers_port_443_with_box_attribution
- relay:
- crates/boxes/src/core/egress.rs:
proxy_lane_admitsadmits TCP 443 at the proxy's address for a box that declares a credentialed upstream, beside the listener's port, and nothing else; UDP 443 there stays rejectedtest: cargo nextest run -p boxes proxy_lane_admits_tls_443_and_listener_port_only
- activation:
- crates/boxes/src/core/bep.rs: the interim refusal
DnsWithoutDirectTlsand its test go, sodnsis admitted wherever the host factbox_zone_resolveris truetest: cargo nextest run -p boxes dns_steering_is_admitted_with_the_resolver_and_no_interim_refusal
produces: none
consumes: none
proof: On a VM host, a box with
steering = "dns"and no proxy variables opens TLS to github.com, the proxy routes it by its SNI and substitutes the sealed value, and a ClientHello naming another host is refused and auditedoff_module.T117 — Give a host-address grant box an id and open the proxy lane to its cohort
slice: S17
kind: task
covers: BEP-082, BEP-083
size: L
est: 480 code, 580 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics:
min box show <box>shows the host-minted box id of ahost_ipgrant box, andmin box audit <box>reads its records by that id; ahost_ipgrant box on a VM host is no longer refused with "no Box Egress Proxy serves boxes on this machine";min session policy <box>shows the credentialed-upstream row for ahost_ipgrant box, and the gate's drop log namesProxyLanefor a cohort connection while no resident member declares a laneobservability: the existing info line of each registration names the box id, the network mode and whether a switch address was allocated, and the gate's existing debug line for a host-address verdict names the lane state of the cohort and the count of members that declare one
layers:
- registry:
- crates/minvmd/src/control.rs:
RegisterBoxaccepts ahost_ipbox and returns a creation-unique box id with no switch address;mint_box_id_replyanswers that id whenhost_ip_namenames the registration, claimed by onecreate_tokenasclaim_row_box_idclaims a row's id, another token refused, and a name with no registration keeps the fresh mint- crates/minvmd/src/box_registry.rs: a
host_ipregistration records the minted id with its registration name and no row in the address table, answers a lookup by that name, and its revocation check is the one anown_ipid takestest: cargo nextest run -p minvmd host_ip_box_registration_mints_a_creation_unique_id
- client:
- crates/minimal-client/src/box_registration.rs:
register_box_for_activationregisters ahost_ipbox on a VM host when its spec declares a credentialed upstream, and still returnsNonefor one that declares none- crates/minimal/src/cmd/boxes/create.rs:
proxy_box_factsgets the minted id for ahost_ipgrant box, so theNotServedrefusal prints only where no proxy serves the hosttest: cargo nextest run -p minimal host_ip_grant_box_on_vm_host_is_not_refused_as_unserved
- rpc:
- crates/minimald-rpc/src/lib.rs:
HostIpBoxRequestcarries whether the box declares a credentialed upstream, andMintBoxIdRequestcarrieshost_ip_name, defaulted and skipped when absent- crates/minimald-rpc/src/tests.rs: a round-trip test pins the new field and its default for an older client
test: cargo nextest run -p minimald-rpc host_ip_box_request_carries_the_lane_declaration
- client:
- crates/minimal-client/src/attach.rs:
AdmitHostIpBoxsends the lane declaration, and the admission completes before the box's first process starts, not after the createtest: cargo nextest run -p minimal-client host_ip_admission_carries_the_lane_and_waits_for_the_answer
- gate:
- crates/minvmd/src/box_registry.rs:
HostIpBoxRulescarries the lane bit, and the cohort's lane is the union of its resident members' bits- crates/minvmd/src/net/egress_gate.rs:
host_ip_verdictadmits the cohort address to the proxy's listener port and to TCP 443 at the proxy's address while the cohort's lane is open, and drops it asProxyLanewhile it is closedtest: cargo nextest run -p minvmd host_ip_cohort_reaches_proxy_while_a_member_declares_a_lane
test: cargo nextest run -p minvmd host_ip_cohort_proxy_lane_dropped_without_a_declaring_member
- identity:
- crates/minimald/src/box_manager.rs: the box id mint for a
HostNetbox with no box addresses sends its creation name ashost_ip_name, so the record takes the registration's idtest: cargo nextest run -p minvmd host_ip_box_record_takes_the_registration_id
produces:
- the cohort's proxy lane at the host gate
consumes: none
proof: On a Mac, a GitHub grant box started with
--network host_ipis created with a box id and reaches the proxy's listener, and after it ends, a host-address box with no grant cannot reach the proxy's address.T123 — Revoke a host-address grant box's minted id at the proxy when the box ends
slice: S17
kind: task
covers: BEP-043
size: S
est: 60 code, 120 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min box audit <box>for an endedhost_ipgrant box shows the revocation of its id, as it does for an ended own-address boxobservability: the existing
bep-revocationthread's warning names the box id and the step that failed, for ahost_ipbox as for an own-address boxlayers:
- revoke:
- crates/minvmd/src/box_registry.rs:
retire_host_ip_boxsendsspawn_revocationfor the box's minted id when the retired box declared a credentialed upstream and the id is oneHostIpIdsminted, as the own-address destroy does (BEP-043); the id stays in the id set so it never returnstest: cargo nextest run -p minvmd retiring_a_host_ip_grant_box_revokes_its_minted_id
test: cargo nextest run -p minvmd retiring_a_host_ip_box_with_no_grant_sends_no_revocation
produces:
- the revocation of an ended host-address grant box
consumes: none
proof: After a host-address grant box ends, the proxy refuses a sealed value under its id, and
min box auditshows the revocation.depends on:
- blocked by T117
T124 — Refuse a host-address box with no grant the proxy's address inside the VM
slice: S17
kind: task
covers: BEP-083
size: L
est: 300 code, 420 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics: inside a
host_ipbox with no grant on a Mac, a TCP connect to the proxy's address on its listener port or on 443 fails at once with "No route to host", while ahost_ipgrant box in the same VM connects; the VM's kernel log carries the classifier's rate-limited "minimal-classifier: refused" line for the refused connectobservability: one info line per change of the classifier's proxy-lane set names the box id, its leaf and the lane members, as the UDP 443 set's change line does; the table's existing refusal counter counts each refused connect; the host gate's existing debug line for a cohort frame to the proxy's address still names the declaring members
layers:
- spec:
- docs/specs/24-spec-box-egress-proxy/24-spec-box-egress-proxy.md: BEP-083 gains a sub-requirement that refuses, inside the VM, a resident member that declares no credentialed upstream the proxy's listener and TCP 443, verified by the kernel test below
- table:
- scripts/install-host-classifier.sh: a VM-guest-only
--proxy-lane ADDR:PORTrenders a setbep_lane_leavesof box leaves and an output rule that refuses a new TCP connection from the boxes subtree to ADDR at PORT or 443 unless the socket's leaf is in the set, with the deny chain's counter, log prefix and reject; the helper takeslane-addandlane-removefor that set as it takesaddandremovefor the UDP 443 set- crates/minimald/src/net/classifier.rs:
GuestRendercarries the proxy's listener, andrender_guest_ruleset_overpasses--proxy-lane; the cohort records each member's lane bit, a member that declares a credentialed upstream has its own leaf inbep_lane_leaveswhile resident and stays in its own leaf when the cohort blocks UDP 443, a member that declares none is never listed, andinjection_leafanswers none for a lane member- crates/minimald/src/main.rs: the guest's render names
DEFAULT_SUBNET.box_egress_proxy_address()andPROXY_LISTENER_PORTas the proxy's listener- crates/minvmd/tests/guest_classifier_integration.rs:
native_ruleset_digestpasses the same--proxy-lane, so the guest's loaded table still digests equal to the native rendertest: cargo nextest run -p minimald guest_render_refuses_the_proxy_address_outside_the_lane_set
- cohort:
- crates/minimald/src/box_actor/host.rs:
HostMoveschanges the lane set through the helper's new verbs, andjoin_quic443_cohortpasses the record's lane bit to the cohorttest: cargo nextest run -p minimald lane_less_cohort_member_stays_out_of_the_proxy_lane_set
- kernel:
- crates/minimald/tests/netns_root_integration.rs: a leg that loads the table rendered with
--proxy-laneat a namespace address, places two members in their own leaves with only the first in the lane set, and shows the first's connects to the listener port and to 443 accepted and the second's refused with EHOSTUNREACH, both while the cohort blocks UDP 443 and while it does not- crates/minvmd/src/net/egress_gate.rs: the interim paragraph of
host_ip_verdict's doc comment goes; the gate logic stays as the lane's host halftest: cargo nextest run -p minimald --run-ignored ignored-only only_the_declaring_cohort_member_reaches_the_proxy_address
produces:
- the in-VM refusal of a host-address box with no grant at the proxy's address
consumes:
- the cohort's proxy lane at the host gate
proof: On a Mac, while a GitHub grant box runs with
--network host_ip, a second host-address box with no grant cannot connect to the proxy's address, and the grant box still reaches the proxy's listener.depends on:
- blocked by T117
T125 — Register each fresh host-address grant box under a name of its own creation
slice: S17
kind: task
covers: BEP-082
size: M
est: 90 code, 260 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: none: no verb on this tree creates a fresh-instance box that declares a credentialed upstream, so nothing new prints until one does; then
min box show <entry>-1andmin box show <entry>-2print different box ids, and a second create under a fixed name a running box holds still exits 2 naming the holderobservability: the VM host daemon's existing "registered box with the VM host daemon; a host_ip box gets an id and no address" line names the creation key, and the in-VM daemon's existing "took the box id the VM host daemon minted" line names it as
host_ip_namebeside the<base>-<n>namelayers:
- rpc:
- crates/minimald-rpc/src/lib.rs:
BoxConfigcarrieshost_ip_registration, the name ahost_ipgrant box registered under when it is not the box's creation name, defaulted and skipped when absent- crates/minimald-rpc/src/tests.rs: a round trip pins the field, and its absence from an older client's config
test: cargo nextest run -p minimald-rpc box_config_carries_the_host_ip_registration_name
- client:
- crates/minimal/src/cmd/boxes/create.rs:
activate_boxregisters a fresh-instancehost_ipgrant box under<base>~<random hex>, a key no box name can spell, carries it on the config, and withdraws by it on a failed create; a create that is not a fresh instance keepscreated_box_nametest: cargo nextest run -p minimal fresh_instance_grant_box_registers_under_a_creation_key
- identity:
- crates/minimald/src/box_manager.rs:
host_ip_mint_namesendshost_ip_registrationwhen the config carries one, else the creation name, so the mint after the<base>-<n>rename finds the registration- crates/minimald/src/box_manager/tests.rs: the mint test drives a fresh-instance config through the
<base>-<n>renametest: cargo nextest run -p minimald fresh_instance_mint_finds_its_registration_after_the_rename
- registry:
- crates/minvmd/src/control.rs: a test drives two concurrent
RegisterBoxasks for one base under two creation keys, both mints by those keys and both admissions, and a second register of the fixed name, which is refusedtest: cargo nextest run -p minvmd concurrent_fresh_instance_grant_boxes_get_distinct_ids
produces: none
consumes: none
proof: Two fresh instances of one host-address grant entry started at once each run under their own box id, and a second create under a held fixed name is still refused.
depends on:
- blocked by T117
T126 — Keep a host-address grant box's minted id across a VM host daemon restart, so its destroy revokes it
slice: S17
kind: task
covers: BEP-043
size: M
est: 80 code, 180 tests
executor: agent
difficulty: needs care
model: standard
diagnostics:
min box audit <box>for ahost_ipgrant box destroyed after a VM host daemon restart shows the revocation of its id, as it does with no restart between; the ids sit inbox-host-ip-ids.jsonbesidebox-registry.jsonin the VM host daemon's state dirobservability: the existing
bep-revocationthread's warning names the box id and the step that failed; a failed write of the id file is a warn line naming the path, as a failed write of the held names is; the reload logs one info line with the count of minted and credentialed ids it readlayers:
- persist:
- crates/minvmd/src/box_registry.rs:
HostIpIdsgainscredentialed, the minted ids admitted with a credentialed lane, in place ofstopped_credentialed, and a destroy revokes an id in it;idsandcredentialedpersist to a newHOST_IP_IDS_FILEbesideREGISTRY_FILEafter each change throughwrite_registry_file, andpersisting_toreloads them, so a reloaded id is held for the collision check and revoked at its box's destroy; an unusable file is set aside and reloads no idtest: cargo nextest run -p minvmd restarted_registry_revokes_a_destroyed_grant_box_id
test: cargo nextest run -p minvmd restarted_registry_never_mints_an_id_it_minted_before
produces: none
consumes:
- the revocation of an ended host-address grant box
proof: A host-address grant box destroyed after the VM host daemon restarted has its id revoked at the proxy, and
min box auditshows the revocation.depends on:
- blocked by T123
T119 — Deliver cohort connections and redeem them for the named box
slice: S17
kind: task
covers: BEP-084
size: L
est: 260 code, 360 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics:
min box audit <box>shows a host-address box's requests markedcohort_attributedunder the box the sealed value names, with that box's egress verdictobservability: the proxy's existing audit record carries the attribution,
boxorcohort, and the subject box id it was evaluated forlayers:
- delivery:
- crates/switch/src/bep_host.rs: while the cohort's lane is open, the node address holds a share of the connection pool,
box_id_for_sourceanswers the cohort for it, and the delivery header carries the reserved cohort id, never a member's id- crates/minvmd/src/box_registry.rs: the
box_id_for_sourceimpl answers the cohort for the node address while a lane member is resident- crates/minvmd/src/cmd/run.rs: the second
box_id_for_sourceimpl answers the same- crates/bep/src/listener.rs: the header doc and its parse name the reserved cohort id, which maps to
Attribution::HostIpCohorttest: cargo nextest run -p switch cohort_delivery_names_no_box
- redeem:
- crates/bep/src/redeemer.rs: under
Attribution::HostIpCohortthe egress re-check and the audit subject read the box the sealed value names, which must be a livehost_ipbox of this host; a value naming anown_ipbox, an ended box or a box of another host is refused and auditedcohort_misattributedtest: cargo nextest run -p bep cohort_redemption_uses_named_box_egress_and_subject
test: cargo nextest run -p bep cohort_value_naming_no_live_host_ip_box_is_refused
produces:
- cohort-attributed delivery
consumes:
- the cohort's proxy lane at the host gate
- the revocation of an ended host-address grant box
proof: A host-address box's request through the proxy is admitted for the box its sealed value names, checked against that box's egress, and recorded in that box's audit log as cohort-attributed.
depends on:
- blocked by T117
- blocked by T123
T120 — Treat the proxy lane as in effect for a host-address box on a VM host
slice: S17
kind: task
covers: BEP-085
size: M
est: 140 code, 200 tests
executor: agent
difficulty: routine
model: standard
diagnostics:
min session policy <box>and themin dashdetail pane show the credentialed-upstream row of ahost_ipbox on a VM host without the "(not in effect: host_ip box)" mark, and ahost_ipbox on a native Linux host keeps itobservability: the launch's existing debug line for the proxy variables names the box's network mode, the host kind and whether the lane is in effect, so a
host_ipbox that got no proxy variables shows whylayers:
- rule:
- crates/boxes/src/lib.rs:
CredentialedUpstream::in_effecttakes the host kind with the network mode, and is true forown_ipand forhost_ipon a VM hosttest: cargo nextest run -p boxes credentialed_upstream_in_effect_by_mode_and_host
- runtime:
- crates/minimald/src/box_runtime.rs:
launch_proxy_envreads the new rule, so ahost_ipbox on a VM host withproxy_envorbothsteering getsHTTPS_PROXY,HTTP_PROXY,NO_PROXYandNODE_USE_ENV_PROXY- crates/minimald/src/box_runtime/tests.rs: the test that a
host_ipbox on a VM host gets the four variables, and one on a native host gets nonetest: cargo nextest run -p minimald host_ip_box_on_vm_host_gets_the_proxy_variables
- surfaces:
- crates/minimal/src/cmd/boxes/policy.rs: the policy row reads the new rule
- crates/minimal/tests/dash_snapshots.rs: a snapshot pins the detail pane of a
host_iplane in effect on a VM host- crates/minimal/src/cli.rs: the
--credentialed-upstreamhelp ends "Takes effect with--network own_ip, and with--network host_ipon a host that runs boxes in a VM." in place of "Takes effect only with--network own_ip."test: cargo nextest run -p minimal detail_pane_shows_a_host_ip_lane_in_effect_on_a_vm_host
produces:
- the in-effect rule for a host-address lane
consumes: none
proof: On a Mac, a host-address grant box with
steering = "proxy_env"gets the proxy variables, andmin session policyshows its credentialed-upstream row with no not-in-effect mark.depends on:
- blocked by T117
T121 — Steer a host-address grant box's lookups and admit dns on a VM host
slice: S17
kind: task
covers: BEP-081, BEP-086
size: L
est: 340 code, 380 tests
executor: agent
difficulty: unknown ground
model: strongest
diagnostics:
min box spec <entry> --network host_ipon a Mac showssteering = "dns"resolved and no refusal; inside a host-address grant boxgetent hosts github.comprints the proxy's address, and inside a host-address box with no grant it prints GitHub's real addressobservability: the steering answerer's debug line for a steered answer names the box and the steered name, as the switch's DNS gate does for an
own_ipboxlayers:
- resolver:
- crates/minimald/src/net/provider.rs: a
host_ipbox with a credentialed upstream anddnsorbothsteering gets a resolver plan that names the steering answerer; every otherhost_ipbox keeps the resolver it has today- crates/minimald/src/net/dns.rs: a steering answerer in the guest's namespace answers the box's module host set with the proxy's address, NODATA for AAAA, HTTPS and SVCB, SERVFAIL while the listener is down, and forwards every other name to the node's resolver
- crates/minimald/src/net/classifier.rs: the box's cgroup leaf may reach the steering answerer's port, and no other leaf may
test: cargo nextest run -p minimald host_ip_grant_box_steered_names_resolve_to_proxy
test: cargo nextest run -p minimald host_ip_lookup_outside_a_steered_box_resolves_real
- activation:
- crates/minimal/src/cmd/boxes/create.rs:
host_resolves_box_namesis true on a VM host forown_ipandhost_ipboxes, so BEP-017 refusesdnsandbothonly on a host with no VM- crates/minimal/src/cmd/box_spec.rs: the preview reads the same host fact for
host_iptest: cargo nextest run -p minimal vm_host_admits_dns_and_both_for_own_ip_and_host_ip
produces:
- per-box dns steering for a host-address box
consumes:
- the in-effect rule for a host-address lane
- cohort-attributed delivery
proof: On a Mac, a host-address grant box with no steering line resolves github.com to the proxy and clones through it, while a host-address box with no grant resolves github.com to GitHub's real address.
depends on:
- blocked by T116
- blocked by T119
- blocked by T120
T122 — Host-address boxes reach GitHub, end to end
slice: S17
kind: integration
covers: BEP-087
size: M
est: 0 code, 260 tests
executor: agent
difficulty: needs care
model: standard
diagnostics: each case prints PASS, or NOT RUN with its gate where a GitHub sign-in or
E2E_BEP_PRIVATE_REPOis missingobservability: on a failure each case prints the box's
min box auditrecords and the proxy's debug lines for the box id, so a refused or unsteered request shows where it stoppedlayers:
- e2e:
- scripts/session-e2e.sh: three cases on a VM host with a
host_ipgrant box: under the default steering, and understeering = "proxy_env", git clones and pushes and gh api reads a private repository; a secondhost_ipbox with no grant resolves github.com to a real address in the first case; a third case proves T117 on a Mac: a GitHub grant box started with--network host_ipgets a box id frommin box showand reaches the proxy's listener, and after it ends ahost_ipbox with no grant cannot reach the proxy's addresstest: ./scripts/session-e2e.sh bep_host_ip_git_and_gh_under_dns_steering
test: ./scripts/session-e2e.sh bep_host_ip_git_and_gh_under_proxy_env
test: ./scripts/session-e2e.sh bep_host_ip_grant_box_reaches_the_proxy_lane
produces: none
consumes:
- per-box dns steering for a host-address box
- the in-VM refusal of a host-address box with no grant at the proxy's address
proof: A box started on the default host network clones, pushes and runs gh against a private repository through the proxy under its default dns steering with no proxy variables, and a host-address box with no grant still resolves GitHub's real address.
depends on:
- blocked by T117
- blocked by T119
- blocked by T120
- blocked by T121
- blocked by T123
- blocked by T124
- blocked by T125
- blocked by T126
Already exists
None.
Residue
None.
ci(kani): run the proof lane for crates/bep changes
norrietaylor commentedon Sep 18, 2026
/dispatch dry-run
minimal-foundry commentedon Sep 18, 2026
Walking, started by @norrietaylor at 2026-10-24T23:54:29.836Z.
max parallel 16 · spend $423.39 of $2500.00 · revise cap 10 per PR
Human tasks (the walk waits for these to close)
Cross-repo blockers
Not walked
<!-- plan:v1 ... -->marker and## Task/## Spikeon line 3)Updated 2026-11-06T21:36:12.518Z. Commands:
/dispatch,/dispatch stop,/dispatch dry-run.ci(kani): run the proof lane for crates/bep changes (#1537)
norrietaylor commentedon Oct 3, 2026
Re-plan input: one task for BEP-018's active UDP/443 reject
The BEP re-plan needs a task that owns BEP-018 and its two sub-requirements in spec 24. Nothing implements them today. None of the three verify tests exists on main, and the retired tree's T23 (#1527) was closed with that plan.
quic443stance is blocked, UDP to port 443 is rejected with ICMP port unreachable. Verifyquic443_blocked_rejects_udp_443.udp_443_to_steered_proxy_rejected_in_every_stance.host_ipcohort blocks UDP/443 when any resident member's stance is blocked. Verifyhost_ip_cohort_quic_stance_most_restrictive_wins.Design basis: networking design §5.3 says
quic443is enforced by active reject in every stance where it is blocked, and steered h3 is rejected node-side. §5.7's determinism rule says every refusal is an active reject, never a silent timeout, so a blocked h3 attempt falls back to TCP at once.What the task must carry, per the architecture review:
crates/switch/src/refusal.rsand emits through that file's shared refusal emitter and line format. NET's T70 (#1800) removed the builder because v1 ingress has no UDP caller; this task is its first consumer.Depends on T70 #1800 for
refusal.rs.norrietaylor commentedon Oct 4, 2026
Rollout note for whoever extends the proxy document: the session policy's credentialed-upstream declaration (
CredentialedUpstream, added by T45 in #1885) isdeny_unknown_fields, asSessionPolicyis. Adding a field to it therefore needs a lockstep rollout: a client that sends the new field to an older guest daemon is refused at activation. That fails closed, which the design accepts, but plan the client and daemon release together.norrietaylor commentedon Oct 4, 2026
When the real Box Egress Proxy acceptor lands, run the session e2e case
proxy_sees_boxes_by_address(#1930, NET walk T46) against it unchanged. Today that case proves box attribution, per-box attachments and lane-only reach against theMINVMD_BEP_STUBstand-in acceptor. It does not cover the real acceptor's own checks. The task that ships the acceptor should list that case among its test lines.56 remaining items
docs(spec): move BEP-047 (split-user VM) to spec 24's non-goals (gomi…