[DO NOT MERGE — awaiting D-1] feat(effects): M1 — request-aware proxy/direct Net transport - #613
Draft
sunholo-voight-kampff wants to merge 1 commit into
Draft
[DO NOT MERGE — awaiting D-1] feat(effects): M1 — request-aware proxy/direct Net transport#613sunholo-voight-kampff wants to merge 1 commit into
sunholo-voight-kampff wants to merge 1 commit into
Conversation
…get-IP pinning on the direct route
Implements M1 of m-net-effect-proxy-boundary to the reviewed design doc's letter.
- New package-private `netProxyRoundTripper` (internal/effects/net_proxy.go) owning
SEPARATE direct and proxy transport creation paths; never mutates one shared
transport between modes.
- no proxy -> resolveAndValidateIP exactly once, dial the returned IP with no
hostname re-resolution (anti-DNS-rebinding pinning preserved)
- proxy -> ordinary proxy dialing, ZERO local target resolution/validation;
the proxy address never enters the target-IP substitution closure
- All three Net client constructors routed through it (net.go:96, :212, :587).
- Removed the three preflight resolveAndValidateIP calls (net.go:85, :201, :565)
and the redirect-validator call (:317). validateRedirect keeps redirect-count,
protocol checks and the caller's cross-origin Authorization stripping.
- Public error categories preserved via a typed internal targetValidationError
unwrapped through *url.Error, at both enumerated sites (net.go:567 preflight,
:631 post-client.Do).
- Unexported test seams on NetContext (lookupIP, dialContext, proxySelector), nil
in production, making resolver-call and dial-call counts falsifiable.
- 781 lines of tests: TestNetProxyBoundary, TestNetProxyDirectPin,
TestNetProxyNoProxy, TestNetProxyRedirectControls (AC-M1.1..M1.4).
AC-M1.1 gate (controller-run): rc=0, `=== RUN` count 17 (>=4), all four top-level
`--- PASS`. Baseline on the pristine tree was rc=0 with `=== RUN` count 0
(`[no tests to run]`) — vacuously green — so the count assertion is load-bearing.
*** DO NOT MERGE — see the PR body. ***
This faithfully implements the reviewed design, and in doing so it makes
`TestNetIPValidation`'s four non-loopback subtests FAIL under the poisoned proxy
that ci.yml sets on its own test legs (ci.yml:89-91, 350-352). Negative control:
the identical command on the pristine tree is rc=0 / 7 PASS. Root cause is D-1 by
construction: SSRF IP-blocking now applies only on the direct route, so a target
that is a LITERAL private IP is handed to the proxy unvalidated. D-1 is still
awaiting human ratification.
Co-Authored-By: DeepSeek V4 Flash 0731 (pi)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sunholo-voight-kampff
pushed a commit
that referenced
this pull request
Aug 7, 2026
…; the outage-era greens were not spendable Gate 4 record + one Gate-5 skill edit. Landed: #606 (squash 49a6af7) and #608 (squash 1d35524) — three iterations of outage backlog cleared; #602 and #603 auto-closed. Held: m-net-effect-proxy-boundary M1 as #613, a DRAFT titled DO-NOT-MERGE. Its own acceptance passes (AC-M1.1 rc=0, `=== RUN` 17, four top-level PASS against a vacuously-green baseline), but implementing the reviewed design faithfully hands a LITERAL private IP to the proxy unvalidated, so TestNetIPValidation goes rc=1 with 4-of-7 failing under the poison ci.yml sets on its own legs — rc=0 with 7 PASS on pristine dev, a real negative control. D-1 is now a demonstrated loss of an existing control rather than an abstract trade, and it has a third option the doc does not carry (validate when net.ParseIP(host) != nil, zero DNS, no TOCTOU), which the independent evaluator endorses. Not applied unattended: rule vii says the reviewed doc wins and Standing rule 2 forbids narrowing a quorum-cleared security boundary without the human. SKILL EDIT (Gate 3b, the workflow_dispatch clause) — two frictions, same gap: 1. iteration 155 fired a dispatch at 21:30Z and banked the mechanism from `total=0 -> total=1`; that run was still queued with jobs=0 seven hours later, and could not be cancelled. 2. iteration 156 measured what a dispatch actually buys: all four required contexts success on the head SHA via commits/<sha>/check-runs (18 rows), while `gh pr checks --required` showed one and the PR stayed BLOCKED — branch protection is gated on the `pull_request` suite the outage wedged. So the clause as written would tell a future iteration a PR is unblocked when it is not. Corrected, with the fix that does work (a tree-identical empty commit via the git API), plus mission-world iter-59's P1 green-direction rule — adopted only after corroborating it first-party here: CI[dev] success 17:32Z, failure 20:03Z, success 21:57Z, with runs from 17:02Z wedged through all three. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sunholo-voight-kampff
added a commit
that referenced
this pull request
Aug 7, 2026
…een, and the charter's own scale warning was a claim nobody had measured PICK: #545, because both items above it are parked on a human (#604 on D-2, #613 on D-1) and iteration 157's own Next names it as the fallback. HEADLINE: the charter has carried #545 since iteration 149 behind a warning reading "47 files ... a surface that moved a lot ... a rebase-and-revalidate job, NOT a merge". Every number in it is true and the conclusion is false. The merge produces THREE conflicted files, one hunk each: a changelog union with zero content overlap, a two-line struct-literal union, and one that looks enormous (427 lines ours vs 1 theirs) and is a code MOVE — dev extracted the agent path into cmd/ailang/eval_benchmark_agent.go, and #545's real delta to that block is four lines. Three iterations deferred the item on that sentence. A "surface moved a lot" warning measures how much OTHER work landed, never how much of it collides. Also recorded: the union resolution justified rather than assumed (dev's new zero-cost fallback resolves through the same models.yml key as #545's task.Pricing, so it does not reintroduce the two-price-table defect); the changelog claims a v0.33.1 release for which NO TAG EXISTS (caught in my own dashboard draft before publishing); Sonar at six consecutive analysed reds, measured on HEAD; and the Gate-0 weekly sweep, which found 7 zero-mention issues, closed #598 as a verified duplicate of #602, and batched the other six into one queue row positioned below every existing pick. STATUS rotation done with the arithmetic asserted before writing (before=after=1864, moved=1) and a post-edit queue-row grep, per the rules iteration 127's mass-deletion bought. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DRAFT — HELD, not ready to merge. Milestone M1 of
m-net-effect-proxy-boundary, implemented to the reviewed design doc's letter. It is opened as a draft because implementing the design faithfully breaks four existing SSRF tests on CI's own poisoned-proxy legs, and the decision that resolves that (D-1) is still awaiting human ratification.What landed
A package-private request-aware
netProxyRoundTripper(internal/effects/net_proxy.go, 150 lines) with separate direct and proxy transport paths:resolveAndValidateIPexactly once, dial the returned IP with no hostname re-resolution (anti-DNS-rebinding pinning preserved)All three Net constructors routed through it (
net.go:96/:212/:587); the three preflightresolveAndValidateIPcalls (:85/:201/:565) and the redirect-validator call (:317) removed; public error categories preserved via a typed internal error unwrapped through*url.Errorat both enumerated sites (:567,:631). 781 lines of tests.Acceptance — AC-M1.1 (controller-run, then independently reproduced by the evaluator)
rc=0,=== RUNcount 17 (≥4), all four top-level--- PASS.The same command on the pristine tree is
rc=0with=== RUNcount 0 /[no tests to run]— vacuously green — so the count assertion is the load-bearing part, not the exit code.⚠ Why this is held — B1
dev(79caa15b3)The only variable is this change.
ci.ymlsets exactly that poison on its own test legs (:89-91,:350-352), so this will red CI.Mechanism, and it is
D-1by construction: IP validation now happens only on the direct route, so a target that is a literal private IP (https://10.0.0.1/…) is handed to the proxy unvalidated. The loopback subtests still pass precisely becauseNO_PROXYroutes them direct — which corroborates the mechanism.The decision, with a third option the design doc does not currently carry
resolveAndValidateIPalready has a// Special case: raw IP address (skip DNS)branch that callsvalidateIPwith zero network I/O. So for a literal-IP target there is no resolution, no TOCTOU and no rebinding risk — the rationale for skipping validation on proxied routes simply does not apply to that case.D-1as written, and updateTestNetIPValidationto expectE_NET_REQUEST_FAILEDfor literal private IPs when a proxy is configured. This narrows the shipped security guarantee.net.ParseIP(host) != nil, even on the proxy route. Restores parity for the unambiguous case and shrinksD-1to hostnames only.The sprint evaluator (independent, sonnet) reviewed this at HOLD 71/100 and recommends (b).
Verification
http.ProxyFromEnvironmentdefault) → gaterc=1, but reds exactly one subtest — the production default has a single point of coverage (NB1). sha256 restored byte-identical.go build,go vet,gofmt,make check-file-sizes/check-boundaries/check-changelog/fmt-check, andgo test ./internal/effects ./internal/executor/... ./internal/testutil(rc=0).http.Transport{}with only a customDialContext; so are the new ones. No goroutine leak measured across 20 sequential / 50 concurrent requests.🤖 Generated with Claude Code