8000 feat(bep): validate the upstream, pass unsealed requests through, and stream replies by minimal-foundry[bot] · Pull Request #2481 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

feat(bep): validate the upstream, pass unsealed requests through, and stream replies - #2481

Merged
norrietaylor merged 6 commits into
mainfrom
agent/2272-validate-the-upstream-before-substituting-pass-u
Oct 21, 2026
Merged

norrietaylor merged 6 commits into
mainfrom
agent/2272-validate-the-upstream-before-substituting-pass-u

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Oct 21, 2026 •
Copy link
Copy Markdown
Contributor

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_invalid or upstream_not_tls, and a passthrough carrying its own credential foreign_credential, each shown by min 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 (new): upstream TLS validated against the host store before forwarding (BEP-055), substitution on admit, passthrough (BEP-030), chunked relay (BEP-N01)
- crat 8000 es/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 #2261
- blocked by #2271

Proof

A CONNECT to a credentialed host whose upstream certificate does not validate against the host trust store gets a 502. The upstream receives no request byte, and min box audit <box> shows the refusal marked upstream_tls_invalid. A chain under the interception root is refused even when the host store holds that root. A request with no sealed value reaches the upstream byte-for-byte in its canonical form, and the audit shows an admit, marked foreign_credential when the request carries its own Authorization header. The box receives the first chunk of a response while the upstream still holds the rest.

Notes from the agent: The proxy has no proxy key, box registry or sign-in resolution yet. So the default redeemer (forward::Unwired) refuses every sealed value with the undecryptable marker. Proxy::with_redeemer takes the real decision when a later task wires it. Substitution itself is implemented and tested with a stub redeemer, and it needs a ValidatedUpstream proof that only a validated stream gives. The proxy also does not know a box's declared egress yet, so Unwired declares an empty egress for every box, and decide_plain refuses every passthrough as outside_declared_egress until #2483 wires the real redeemer and each box's declared egress. Sealed values are found only in Authorization headers (Bearer, token, Basic user or password, bare value), not in URLs or bodies. A refused request inside a tunnel gets a 403 at once when no earlier request went upstream. Otherwise the 403 follows after the upstream closes its side. The response relay copies bytes as received. Alt-Svc stripping on responses (BEP-076) is not part of this task. LeafIssuer gained a default root() method so the proxy can take the interception root out of the upstream roots, and main.rs needs no change. The test helpers in proxy.rs tests became pub(crate) so forward.rs tests reuse them. One full run of cargo nextest run -p bep passed (114 tests). A mutation check, which kept the interception root in the upstream roots, made the property test fail on the `InterceptionRoo…[truncated]

Tests

line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0d22... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 13.22s ──────────── Nextest run ID 19a7aeac-4948-4703-9083-cb22734cd73e with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-58c3... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID 636a34a8-7eea-4d10-911d-ddce4d56527b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-8b78... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID b23c2495-9da7-4ad2-9fcb-4ab5a7cde665 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-c739... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID 287094c2-f493-41ea-b98f-2073c5fe370b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-7a23... Finished 'test' profile [unoptimized] target(s) in 0.33s ──────────── Nextest run ID 595951bc-cec0-4abc-9deb-fd555da6c1b3 with nextest profile: default Starting 1 test across 2

Verify

min task run agent-verify passed ([tasks.verify]).

Closes #2272


Generated by Claude Code

The proxy now refuses a CONNECT whose upstream chain or hostname does not
validate against the host trust store. It answers 502 and audits the
refusal `upstream_tls_invalid`. It takes the interception root out of the
upstream roots, so a chain under that root never validates (BEP-055).

Each parsed request inside a tunnel goes to a decision. A sealed value
goes to the redeemer, and its credential is substituted only on a leg
that validated: the forwarder requires a proof that only a validated
stream gives. An unsealed request goes upstream unchanged and is audited,
marked `foreign_credential` when it carries its own Authorization header
(BEP-030). The response half writes and flushes each chunk as it arrives
(BEP-N01).

The default redeemer refuses every sealed value, because the proxy has
no proxy key or box registry yet.
@coderabbitai
coderabbitai Bot commented Oct 21, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dc5bdedb-17b5-4b24-9fa1-ccd11ed6441f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@minimal-foundry
minimal-foundry Bot commented Oct 21, 2026 •
Copy link
Copy Markdown
Contributor Author

Reviewed: 1 finding, none blocking at 5fc57d665af8.

Address the comments on the diff.

Clobber: none seen.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/proxy.rs Resolved
Comment thread crates/bep/src/proxy.rs Resolved
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 2 conditions · the full ruling

The forwarder validates the upstream before it substitutes a credential, and it streams responses. Both conform. The passthrough egress check does not use the box's declared egress. Unwired::egress gives every module host to every box, and Gatehouse §6.10 line 823 says the box's declared egress must admit the host. C1 makes the unwired default deny. This PR also closes #2272, but main.rs names #2272 as the flip for the NoSigningKey interim. C2 restates that flip and links the issue that wires the real redeemer.

Ruled at 64b47c6 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/bep/src/forward.rs Resolved
Comment thread crates/bep/src/proxy.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
Proxy::new skipped the removal of the interception root from the
upstream roots when RootCertStore::add failed. It now returns the
error, so the upstream leg never trusts that root (BEP-055).
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on the runtime's review findings, pushed as 168c6e86a1a5.

line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-68b5... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 10.38s ──────────── Nextest run ID 1916ce4a-aeaf-46b5-89dd-6940faad6988 with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-98fe... Finished 'test' profile [unoptimized] target(s) in 0.26s ──────────── Nextest run ID 0a0dfd22-0d01-4c2f-8729-65a2c72d6e74 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-7b05... Finished 'test' profile [unoptimized] target(s) in 0.26s ──────────── Nextest run ID 9b5e9a10-63f5-4194-86b0-f72c06711b8c with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-05d6... Finished 'test' profile [unoptimized] target(s) in 0.28s ──────────── Nextest run ID e4fffa26-077b-4ae5-9216-afcafff1c198 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-eb79... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID 6cc3d641-9d2e-4756-b60c-59a43ddb57c3 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/proxy.rs: Proxy::new now returns the error when interception.add(root) fails, and a test pins it. root() stays optional: the only issuer that returns None (NoSigningKey in main.rs, outside this task) issues no leaves, so no TLS is intercepted.
  • 👍 valid — crates/bep/src/proxy.rs:318: interception.add(root)? now makes Proxy::new fail when the root cannot be added. Test upstream_tls_refuses_to_start_when_interception_root_unparsable pins it.

An upstream that fails validation receives nothing, an unsealed request passes unchanged, and responses stream. The proxy now also refuses to start when it cannot remove the interception root from the upstream roots, so the upstream leg never trusts that root.

Notes from the agent: Revise round 1. Proxy::new now propagates the RootCertStore::add error with ?, and a new unit test pins the behavior. LeafIssuer::root() keeps its None default. The only issuer that returns None is NoSigningKey in crates/bep/src/main.rs. It issues no leaves, so the proxy terminates no TLS and forwards nothing. To make root() required, main.rs must change, and main.rs is outside this task's layers. The doc comment on root() now says that None is only for an issuer that issues no leaves. Clippy for -p bep --all-targets is clean, and cargo fmt --check passes. I did not run just clippy-strict.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE withheld: 5 unresolved threads — minimal-foundry[bot] crates/bep/src/proxy.rs, minimal-foundry[bot] crates/bep/src/proxy.rs:318, minimal-arch[bot] crates/bep/src/forward.rs:216, minimal-arch[bot] crates/bep/src/proxy.rs:325, minimal-arch[bot] crates/bep/src/forward.rs:23.

minimal-review — advisory review

Verdict COMMENT — No findings.

Panel: 2 of 3 personas ran (correctness, security); did not complete: supply-chain.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 4 conditions · the full ruling

The upstream validation (BEP-055) and the response streaming (BEP-N01) conform. The new commit fails closed on an unparsable interception root. The unsealed passthrough (BEP-030) is not egress-checked. Unwired gives the admitted set as every box's egress, and §6.10 line 823 says the box's declared egress MUST admit the host. Make Unwired refuse (C1), and prove the passthrough with a declared egress and a refusal case (C2). This PR closes #2272, but main.rs still names #2272 as the lift for the NoSigningKey interim. Point it at a new issue (C3). C4 covers sealed values in a named injection header.

Ruled at 168c6e8 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/bep/src/forward.rs Resolved
Comment thread crates/bep/src/proxy.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter review of T11 at 168c6e8 (round 1 over 64b47c6). Not an approval yet.

Verified line by line against spec 24: validate is the only maker of Validated, and Flow::new and substitute need the ValidatedUpstream proof only it gives, so no path forwards or substitutes on a leg that did not validate (BEP-055); the interception root is taken out of the upstream roots and an unparsable root now fails Proxy::new closed, which settles the two runtime threads at 64b47c6; an unsealed request goes upstream byte for byte and is audited, foreign_credential when it carries its own Authorization (BEP-030); relay_responses writes and flushes each chunk as it arrives (BEP-N01). The credential is zeroized in the substituted header, the serialized head and the verdict's Debug. Both commits lint.

The arch ruling's four conditions stand and go to the next round: Unwired must declare no egress, so a proxy that does not know a box's declared egress refuses a passthrough (§6.10 line 823); the passthrough test needs a redeemer with the declared egress and an outside_declared_egress case that sends nothing upstream; main.rs must stop naming #2272 as the lift of the NoSigningKey interim, since this PR closes it (the wiring of the real redeemer and declared egress is now #2483); and carried must say that a store member in a named header is #2292's.

Low, for the same round or none: the UpstreamError doc says Invalid covers an upstream that ends the handshake, but an EOF there is an io error and so Unreachable; a request with one sealed and one foreign Authorization header is treated as sealed and the foreign header rides upstream beside the substituted credential; and the Redeemer trait doc should say the sealed path applies none of decide_plain's checks, so a redeemer carries every BEP-025 check itself.


Generated by Claude Code

Copy link
Copy Markdown
Member

/revise

Carry the four arch-review conditions at 168c6e8 (ruling 5605208954), and three low notes from the babysitter review.

C1 (arch, threads 4341137473, 4341137488, 4341060874, 4341060885). Unwired::egress returns the admitted set, so decide_plain never checks the box's declared [network] egress (Gatehouse §6.10 line 823, step 3). Make Unwired declare no egress: egress returns an empty Vec, drop Unwired::new and its field, and have Proxy::new install Arc::new(Unwired). admission.authorities() is the CONNECT admission set, not a box's declared egress; keep it only if something else uses it. Update the module doc in forward.rs and the Proxy::new doc, which both state the relaxed interim.

C2 (arch, thread 4341137495). unsealed_request_passes_through_and_is_audited and streamed_response_is_not_buffered pass only through the relaxed gate. Give each a test redeemer whose egress returns the box's declared egress (the GitHub host set is fine) through Proxy::with_redeemer. Add unsealed_request_outside_box_egress_is_refused: a proxy with the default Unwired, a CONNECT to api.github.com:443 with a valid upstream chain, then a plain GET; assert 403, an audit record outside_declared_egress, and that the upstream read zero application bytes.

C3 (arch, thread 4341060893). This PR closes #2272, but the NoSigningKey doc in main.rs (lines 196 to 207) and its warn! (line 268) still name #2272 as the lift of the interim. Point all three places, and the forward.rs module doc, at #2483, which wires the real redeemer and each box's declared egress into bep serve.

C4 (arch, thread 4341137501). carried finds a sealed value only in Authorization. A store member can ride a named header (§6.10 line 849). Say on carried's doc that store references injected in a registered header are #2292's (T28), and that until then a sealed value elsewhere than Authorization is not found, so the request is decided as plain.

Low, from the babysitter review (5605287359), take or leave: the UpstreamError::Invalid doc says it covers an upstream that ends the handshake, but an EOF there is an io error and so Unreachable, so make the doc match; say on Redeemer::redeem that the sealed path applies none of decide_plain's checks, so a redeemer carries every BEP-025 check itself; and either refuse a request with two Authorization headers or document that a foreign one rides upstream beside the substituted credential.

Reply on each arch thread with the commit that carries it.


Generated by Claude Code

Unwired gave the admitted set as the egress of every box, so an unsealed
request never met the declared-egress check (Gatehouse §6.10 step 3).
Unwired now declares no egress, and Proxy::new installs it. A proxy with
no redeemer refuses every unsealed request as outside_declared_egress.
Admission::authorities had no other user and is gone.

The passthrough and streaming tests now use a test redeemer that
declares the GitHub host set. A new test proves that the default proxy
refuses an unsealed request and sends nothing upstream.

The docs now point the interim at #2483, and say that carried searches
only Authorization (#2292 owns other headers), that a second foreign
Authorization rides beside the substituted credential, that a redeemer
carries every BEP-025 check, and that a handshake EOF is Unreachable.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 2 of 10 on a /revise note, pushed as fd36c95b66e1.

10000
line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-a2af... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 14.16s ──────────── Nextest run ID 3e81e1a5-d6fc-4bf5-8aa5-18cbb40b0583 with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-cfda... Finished 'test' profile [unoptimized] target(s) in 0.35s ──────────── Nextest run ID 6b3bd5a6-f5b0-49ec-9380-c2ae91889a0d with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-dd2f... Finished 'test' profile [unoptimized] target(s) in 0.37s ──────────── Nextest run ID 6baa01f8-f062-48b7-89c4-0945e0d79772 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-39c2... Finished 'test' profile [unoptimized] target(s) in 0.37s ──────────── Nextest run ID 10e740d4-0c71-4d0c-8812-143565b1922f with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-f3b4... Finished 'test' profile [unoptimized] target(s) in 0.35s ──────────── Nextest run ID 9838c172-435d-4485-b89d-f429246c5bd3 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/forward.rs:213: Unwired::egress now returns an empty Vec. Unwired::new and its field are gone, so the unwired proxy refuses with outside_declared_egress (fd36c95).
  • 👍 valid — crates/bep/src/proxy.rs: Proxy::new installs Arc::new(Unwired) with no egress. The passthrough tests use a test redeemer through with_redeemer, and unsealed_request_outside_box_egress_is_refused is added (fd36c95).
  • 👍 valid — crates/bep/src/forward.rs:23: The forward.rs module doc and the Proxy::new doc now name T41 — Wire the redemption decision and each box's declared egress into bep serve #2483 (fd36c95). The main.rs NoSigningKey doc and warn! still name T11 — Validate the upstream before substituting, pass unsealed requests through, and stream responses #2272: main.rs is outside this task's layers, so that part is left for a follow-up.
  • 👍 valid — crates/bep/src/forward.rs:213: Unwired::egress returns an empty Vec, so decide_plain checks a declared egress and refuses when none is declared (fd36c95).
  • 👍 valid — crates/bep/src/proxy.rs: Proxy::new installs Arc::new(Unwired). Admission::authorities had no other user and is removed (fd36c95).
  • 👍 valid — crates/bep/src/forward.rs: Both tests now pass a Declared redeemer with the GitHub host set as egress. The new unsealed_request_outside_box_egress_is_refused asserts the 403, the outside_declared_egress audit record, and zero upstream bytes (fd36c95).
  • 👍 valid — crates/bep/src/forward.rs:300: The carried doc now says a store reference in a registered header is T28 — Inject a stored key in the registered form, read from the Keychain per request #2292's (T28). Until then a sealed value outside Authorization is not found and the request is decided as plain (fd36c95).

An upstream that fails validation receives no byte and the refusal is audited upstream_tls_invalid. An unsealed request to a host in the box's declared egress passes upstream unchanged and is audited, marked foreign_credential when it carries its own Authorization. An unsealed request outside the declared egress gets a 403, is audited outside_declared_egress, and the upstream reads nothing. Responses stream chunk by chunk to the box.

Notes from the agent: C3 is done only in part. The forward.rs module doc and the Proxy::new doc now name #2483. The NoSigningKey doc (main.rs lines 196-207) and its warn! (line 268) still name #2272. crates/bep/src/main.rs is not in this task's layers, so I did not change it. A follow-up, or a person, must change those three lines. proxy_offers_http11_only_on_both_legs in proxy.rs passed only through the relaxed gate too. Its harness now installs a test redeemer (DeclaresGithub) that declares the GitHub host set. Admission::authorities was added by this PR and had no other user after C1, so I removed it. Low notes: the UpstreamError::Invalid doc now says a handshake EOF is Unreachable. Redeemer::redeem now says that a redeemer carries every BEP-025 check. For two Authorization headers, I documented on carried that a foreign one goes upstream beside the substituted credential. I did not add a refusal for it.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS_WITH_CONDITIONS · 1 condition · the full ruling

The forwarder conforms to Gatehouse §6.10. The upstream validates before any request goes on it, passthrough is egress-checked, and replies stream. The compare fixes the open egress check: Unwired now declares no egress, so the default proxy refuses an unsealed request. One stale interim remains. crates/bep/src/main.rs still says #2272 lifts the bep serve interim, and this PR closes #2272. C1 points that text at #2483.

Outside the diff

  • crates/bep/src/main.rs:203: C1 · This comment says that proxy::relay forwards with no per-request decision, and that #2272 (T11) wires the decision in. This PR is T11 and closes #2272. Say that Unwired refuses every request until #2483 wires the real redeemer and each box's declared egress. Make the same change in the warn! text at line 266.

Ruled at fd36c95 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/forward.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
Every request now goes through decide_plain before the redeemer sees
it. A TLS handshake failure that is not a certificate error no longer
tells the box that the certificate did not validate.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on the runtime's review findings, pushed as 4b810f5fd834.

line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-85cc... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 15.20s ──────────── Nextest run ID 98859771-360e-49bb-975a-9380284854b6 with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-dfe9... Finished 'test' profile [unoptimized] target(s) in 0.37s ──────────── Nextest run ID d1b8d0ce-a6d5-4864-a381-33bcdd4744cf with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-96f7... Finished 'test' profile [unoptimized] target(s) in 0.36s ──────────── Nextest run ID f3194da6-4909-4fbc-a032-c914b550de8a with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-b8e0... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID 46854e2c-5922-4574-9109-e11f36c78029 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-eb9b... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID 3907a7f9-b2e2-467f-99cf-976efa9d6850 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/forward.rs: Flow::decide now runs decide_plain (declared egress and Host pin) on every request before the redeemer sees a sealed value. The Redeemer doc says so. Added sealed_request_outside_egress_or_host_is_not_redeemed.
  • 👍 valid — crates/bep/src/forward.rs:179: Only InvalidCertificate, NoCertificatesPresented and UnsupportedNameType map to Invalid. Other TLS errors are a new Handshake variant: still refused and audited upstream_tls_invalid, but the 502 body says the TLS handshake failed. Added only_certificate_errors_are_invalid.

An upstream that fails validation receives nothing and is audited upstream_tls_invalid. An unsealed request inside the box's declared egress passes unchanged and is audited. Responses stream chunk by chunk. A sealed request outside the declared egress or with a wrong Host now never reaches the redeemer.

Notes from the agent: The five test: lines were run together in one nextest invocation, with the two new tests sealed_request_outside_egress_or_host_is_not_redeemed and only_certificate_errors_are_invalid. The 8 tests passed. Then the whole bep suite passed once. A TLS handshake failure that is not a certificate error (an alert, an incompatible peer) is now UpstreamError::Handshake. It keeps the audit marker upstream_tls_invalid, because the leg did not validate (BEP-055). Its 502 body now says 'the TLS handshake with the server failed', and not that the certificate did not validate. The span records upstream_tls=handshake_failed for it.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The compare puts the egress check and the Host pin before redemption. Gatehouse §6.10 line 823 gives that order. A non-certificate TLS failure now has its own error. It still refuses and audits upstream_tls_invalid, so the upstream leg stays closed. The wiring of the real redeemer stays open in #2483, and Unwired refuses everything until then.

Ruled at 4b810f5 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/proxy.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
Send the refusal before the upstream shutdown, and take an already-sent
refusal when the upstream read ends, so the box sees the 403 and not a
silent close. Refuse a redeemed credential that holds CR, LF or NUL, and
audit it credential_not_header_safe, before it goes into a header.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 4 of 10 on the runtime's review findings, pushed as d8271784033b.

line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-def1... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 13.93s ──────────── Nextest run ID 2cd20fd8-79ba-4489-971d-188202c6c110 with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-e8bb... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID 730faca9-318b-473e-b8c8-44fe9fd2ef44 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-a464... Finished 'test' profile [unoptimized] target(s) in 0.33s ──────────── Nextest run ID 128bf4fe-c1bc-4cfc-8669-643f7e031984 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-e1b4... Finished 'test' profile [unoptimized] target(s) in 0.35s ──────────── Nextest run ID 46decd73-6156-4df2-869b-dde61a9ed016 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-7777... Finished 'test' profile [unoptimized] target(s) in 0.34s ──────────── Nextest run ID abc28f49-f026-4c53-98a6-1826c351c44f with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/proxy.rs: relay now sends the Refusal before upstream.shutdown(), and relay_responses calls try_recv on the oneshot after the upstream read ends. Test: refusal_is_answered_after_upstream_closes.
  • 👍 valid — crates/bep/src/forward.rs:407: substitute now returns Err(credential_not_header_safe) for a credential with CR, LF or NUL and leaves the header unchanged. Flow::decide refuses and audits it. Test: credential_with_line_break_is_not_substituted.

An upstream that fails validation receives nothing, an unsealed request passes unchanged, and responses stream. A refused request now gets its 403 even when the upstream closes first. A redeemed credential with CR, LF or NUL never goes into a header.

Notes from the agent: Revise round 4. These test lines ran in one nextest invocation with the five task tests: credential_with_line_break_is_not_substituted and refusal_is_answered_after_upstream_closes. All seven passed. A final cargo nextest run -p bep passed 120 of 120. This round adds a new audit marker, credential_not_header_safe (forward::CREDENTIAL_NOT_HEADER_SAFE). The spec does not name it. If the spec must list every marker, a spec follow-up must add it. just clippy-strict was not run.

@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: CONDITIONS · 1 condition · the full ruling

The new header-safety refusal in Flow::decide writes an inexact audit record. The redeemer already returned the identifier and the mapping, but the record writes none and module_unmapped. Gatehouse §6.10 line 823 says every refuse records the mapped resource and permission. It uses module_unmapped only when the module cannot map the request. C1 passes Some(identifier) and mapping to the record and adds a test through Flow::decide. The relay change, which answers the 403 after the upstream closes, conforms.

Ruled at d827178 against gominimal/arch@109541a48c31 by claude-opus-5-5.

Comment thread crates/bep/src/forward.rs Resolved
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 2 findings, 2 inline.

Panel: 2 of 3 personas ran (correctness, security); did not complete: supply-chain.

Comment thread crates/bep/src/forward.rs Resolved
Comment thread crates/bep/src/forward.rs Resolved
The upstream can honour either Authorization header, so a request with a
sealed value and a second Authorization header is refused and audited
mixed_authorization. The header-safety refusal now records the mapped
identifier and mapping, and substitute fails when its header is missing.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The compare conforms to Gatehouse §6.10. A sealed value beside a second Authorization header is now refused with mixed_authorization, after the egress check and before a redeem. A refusal after the module admits now records the identifier and the mapping, as line 823 requires. substitute now fails closed when its header is missing. No conditions remain. The interims stay filed as #2483 and #2292.

Ruled at 5fc57d6 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on the runtime's review findings, pushed as 5fc57d665af8.

line exit tail
cargo nextest run -p bep unsealed_request_passes_through_and_is_audited 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-0ac1... Compiling bep v0.0.1 (/workbench/crates/bep) Finished 'test' profile [unoptimized] target(s) in 10.36s ──────────── Nextest run ID d1e2513f-dfb7-424d-940d-b8e15dc1b2ae with nexte
cargo nextest run -p bep prop_upstream_tls_validated_before_substitution 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-87d7... Finished 'test' profile [unoptimized] target(s) in 0.29s ──────────── Nextest run ID e97a292b-4656-439a-b018-318cb6f82b96 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep upstream_tls_failure_is_refused_and_audited 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-4a56... Finished 'test' profile [unoptimized] target(s) in 0.27s ──────────── Nextest run ID a7c15870-110f-4af1-9455-3a63eb55cc5b with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep plaintext_request_to_credentialed_host_is_refused 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-133e... Finished 'test' profile [unoptimized] target(s) in 0.31s ──────────── Nextest run ID 66052240-fa7b-4e85-ba9e-31970eacc6b8 with nextest profile: default Starting 1 test across 2
cargo nextest run -p bep streamed_response_is_not_buffered 0 Applying loadouts: default Running task runtime-implement-test-4 in session task-runtime-implement-test-4-1239... Finished 'test' profile [unoptimized] target(s) in 0.28s ──────────── Nextest run ID 53583813-7d95-4603-98d8-b7ac2e25a6c4 with nextest profile: default Starting 1 test across 2

Verify min task run agent-verify passed.

Review threads the coder judged:

  • 👍 valid — crates/bep/src/forward.rs: The credential_not_header_safe refusal now records Some(identifier) and the mapping from the Admit verdict. unsafe_credential_refusal_records_the_mapping checks these record fields through Flow::decide, with a stub redeemer that returns a credential containing CR LF.
  • 👍 valid — crates/bep/src/forward.rs:353: carried now returns Carried::Mixed when a sealed value sits beside any other Authorization header, in either order. Flow::decide refuses it as mixed_authorization and redeems nothing. sealed_beside_other_authorization_is_refused covers both orders and the case with two sealed headers.
  • 👍 valid — crates/bep/src/forward.rs: substitute now returns Err(SEALED_HEADER_MISSING) when the request has no header at the index, so the caller audits a refusal and not an admit. substitution_without_its_header_fails checks it.

An upstream that fails validation receives nothing, an unsealed request passes unchanged and is audited, and responses stream to the box chunk by chunk. A sealed request that also carries another Authorization header is now refused, and nothing is substituted into it.

Notes from the agent: Round 5. Two new audit markers: mixed_authorization (a sealed value beside another Authorization header, which includes two sealed headers) and sealed_header_missing (substitute finds no header at its index, not reachable today). The spec does not name these markers. A refused mixed request gets a new 403 body: 'The Box Egress Proxy refused the request to , because it has more than one Authorization header. Send the sealed credential in one Authorization header.' New tests: unsafe_credential_refusal_records_the_mapping, sealed_beside_other_authorization_is_refused, substitution_without_its_header_fails. All 14 forward tests pass. The clippy run for bep was clean. just clippy-strict was not run.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — 1 finding, 1 inline.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

Comment thread crates/bep/src/proxy.rs Resolved
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 6 of 10 on the runtime's review findings: the coder changed nothing. Revise round 6: no code change. The one open thread asks for a PR body correction. The correct statement is this: Unwired, which Proxy::new installs, declares an empty egress for every box, so it refuses every passthrough with outside_declared_egress. It does not give the admitted set as egress. The finding's behaviour claim does not hold: bep serve installs NoSigningKey (main.rs), so Proxy::server_config fails and every admitted CONNECT is answered 502 before any request is parsed. bep serve forwarded nothing before this PR and forwards nothing now. Not changed, outside layers: the NoSigningKey doc and the startup warn! in crates/bep/src/main.rs still name #2272 as the issue that lifts the interim. #2483 is the issue that wires the real redeemer and each box's declared egress.

Review threads the coder judged:

@norrietaylor norrietaylor changed the title T11 — Validate the upstream before substituting, pass unsealed requests through, and stream responses feat(bep): validate the upstream, pass unsealed requests through, and stream replies Oct 21, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter approval of T11 at 5fc57d6.

Six rounds, each verified against its diff: the upstream leg validates against the host trust store before any request or substitution, with the interception root taken out and an unparsable root failing closed (BEP-055); the declared egress and the Host pin are checked for every request before a redeemer sees a sealed value; Unwired declares no egress, so the default proxy refuses every passthrough until #2483 wires the real redeemer; an unsealed request inside a declared egress goes upstream byte for byte and is audited (BEP-030); responses stream chunk by chunk (BEP-N01); a refusal reaches the box as a 403 even when the upstream closes first; a credential with a line break, a sealed value beside another Authorization header, and a non-certificate TLS failure are each refused and audited. Arch-review passed at this head, the runtime's review concluded with every thread resolved and each resolution re-verified, every lane aggregator is green, and the squash subject lints. The NoSigningKey text in main.rs that still names #2272 is #2483's.


Generated by Claude Code

@norrietaylor
norrietaylor added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 21, 2026
@norrietaylor
norrietaylor added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit cc56e3b Oct 21, 2026
37 checks passed
@norrietaylor
norrietaylor deleted the agent/2272-validate-the-upstream-before-substituting-pass-u branch October 21, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T11 — Validate the upstream before substituting, pass unsealed requests through, and stream responses

1 participant

0