Uh oh!
There was an error while loading. Please reload this page.
The gateway serves absolute-form requests as a third front door (LLP 0246/0247) - #797
Conversation
…0246/0247) Claude Code's Remote Control bridge registers via POST https://api.anthropic.com/v1/environments/bridge sent as absolute-form plaintext to the proxy port instead of a CONNECT tunnel. The gateway routed non-CONNECT requests by pathname alone, discarded the host the request line named, and answered a local 404 that Claude Code misread as "Remote Control environments are not available for your account". An absolute-form request-target now routes by its named authority through matchUpstreamByHost, with the same per-path recording opt-in as proxy mode and the recorded path normalized to origin-form. Hosts no upstream names are refused with 403, as are non-loopback peers (the CONNECT rule), and the /_hypaware/ control surface never answers an absolute-form target. Issue LLP 0246, decision LLP 0247 extending LLP 0233; regression tests replay the exact on-the-wire request shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-review round 1 findings on PR #797: - The absolute-form branch was live on every listener flavor, including pure reverse-proxy installs whose behavior LLP 0233 #proxy-mode-is-explicit promises stays unchanged. The door now opens only where the CONNECT front door does (interception or tunnelOnly), which still fixes the Remote Control bridge: it only sends absolute-form when HTTPS_PROXY is set. LLP 0247 gains the #only-forward-proxy-listeners-serve-it section settling this. - The loopback-peer 403 (the containment that stops the listener relaying for the network) had no test. Added one that injects a socket with a LAN peer address into the real listener, plus a test that a reverse-proxy-only listener leaves absolute-form to path routing. StartedProxy exposes the server as the test seam. - Comment honesty: matchUpstreamByHost's "a miss is impossible" JSDoc now covers the absolute-form caller where a miss means refusal; the connect.js "two front doors" docblocks now describe the three-shape split; LLP 0232 carries an Extended-by forward-ref to 0247 since the gateway now serves a shape 0232's rationale said it does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both round-2 reviewers converged on the same gap from different sides: tunnelOnly marks the degraded states (stale CA with proxy mode off, CA prep failure, nothing to intercept), and two of those three keep a populated routing table, so the gate let a degraded listener record absolute-form traffic inside a path anchor while its CONNECT door was blind, and 0247's consequence bullet wrongly claimed tunnel-only listeners have empty routing tables and refuse the shape. Recording now additionally requires live interception: a degraded listener forwards absolute-form to registered hosts (the stranded client's Remote Control keeps working) but records nothing, matching the unrecorded-but-working degrade contract and keeping capture behind the explicit proxy-mode consent. LLP 0247 gains the #degraded-listeners-forward-it-blind section and a corrected consequence bullet; the test rig now boots the three listener shapes (intercepting, degraded, reverse-proxy) and a new test proves the degraded listener forwards an anchored path unrecorded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #797 (`fix/absolute-form-third-front-door`) claimed 0247 for `llp/0247-absolute-form-third-front-door.decision.md` about a minute before this branch's commit, so both PRs introduce an LLP 0247 and both merge cleanly (different filenames). The result on master would be two documents claiming one number, an ambiguous `@ref LLP 0247#the-bit`, and a red `test/core/llp-ref-hygiene.test.js` ('no LLP number is claimed by two documents'). LLP 0156#renumber settles this: the later claimant moves above the highest number claimed anywhere. 0245-0249 are all claimed on unmerged branches, so this doc takes 0250. Mechanical rename plus the reference sweep in the same commit (the doc title, the two `Extended-by:` forward-refs on LLP 0138/0186, four `@ref`s and two prose mentions in `action_reconciler.js`, two in `types.d.ts`, and the test file's `@ref` and two test names). Nothing the document decided changes.
Round-3 review nit: the consequence bullet tied the empty-routing-table tunnel-only state to "proxy mode on, no upstream naming a host", but the same state is reachable with proxy mode off (zero compiled upstreams plus a CA on disk). Describe the state, not the branch that produced it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
philcunliffe
commented
Aug 17, 2026
Dual-agent review — |
| Source | Finding (severity, evidence) | Intersects |
|---|---|---|
| Claude | 0247 consequence bullet mislabeled the empty-table degrade state (nit, llp/0247:110; fixed in b63235f) | Targets (LLP doc set) |
Codex review (round 3, head 25208e0/b63235f)
Fix Validations
Absolute-form Remote Control requests were handled as local path-routed requests
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:310, hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:357, hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:454, test/plugins/ai-gateway-absolute-form.test.js:139
- Assessment: The new path classifies absolute-form requests only on forward-proxy listeners, resolves by request-line host/port, and forwards the origin-form path upstream. The existing CONNECT handler would not have fixed this because it only runs on
connectevents, while this traffic entershandleRequestas a normal HTTP request.
Absolute-form recording follows proxy-mode path opt-in
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:405, hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:424, test/plugins/ai-gateway-absolute-form.test.js:160, test/plugins/ai-gateway-absolute-form.test.js:310
- Assessment: Anchored paths record with normalized origin-form paths, while Remote Control and degraded tunnel-only traffic stay unrecorded as described.
Absolute-form containment prevents open relay behavior
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:318, hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:381, hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:342, test/plugins/ai-gateway-absolute-form.test.js:179, test/plugins/ai-gateway-absolute-form.test.js:237
- Assessment: Non-loopback peers and unregistered hosts are refused, and
/_hypaware/is not served locally for absolute-form targets.
Findings
No new issues found.
No Finding
- Behavioral Correctness
- Contract & Interface Fidelity
- Change Impact / Blast Radius
- Concurrency, Ordering & State Safety
- Error Handling & Resilience
- Security Surface
- Resource Lifecycle & Cleanup
- Release Safety
- Test Evidence Quality
- Architectural Consistency
- Debuggability & Operability
Evidence Bundle
- Changed hot paths:
startProxyreturn shape,handleRequestfront-door classification, host/port upstream matching, recording decision, local control-path guard. - Impacted callers: hypaware-core/plugins-workspace/ai-gateway/src/source.js:370, hypaware-core/plugins-workspace/ai-gateway/src/source.js:397, hypaware-core/plugins-workspace/ai-gateway/src/source.js:538, test/plugins/ai-gateway-absolute-form.test.js:54, test/plugins/ai-gateway-proxy-mode.test.js:62, test/plugins/ai-gateway-proxy-routing.test.js:201.
- Impacted tests: test/plugins/ai-gateway-absolute-form.test.js:139, test/plugins/ai-gateway-absolute-form.test.js:160, test/plugins/ai-gateway-absolute-form.test.js:179, test/plugins/ai-gateway-absolute-form.test.js:198, test/plugins/ai-gateway-absolute-form.test.js:237, test/plugins/ai-gateway-absolute-form.test.js:287, test/plugins/ai-gateway-absolute-form.test.js:310.
- Unresolved uncertainty: Full
npm testwas not rerun here. I rannode --test test/plugins/ai-gateway-absolute-form.test.js; it passed after escalating past sandbox loopback-bind restrictions.
Claude review (round 3, head 25208e0/b63235f)
Claude review
Round 3 of 3 (two earlier rounds' findings were fixed on the PR head; see the
review-history section of the summary comment). The round-2 fix was verified
sound: the recording conditional traces correctly for all four front-door and
listener combinations, opts.interception and opts.tunnelOnly are mutually
exclusive in production, all @ref anchors resolve, and no guidance violations
(em dashes, semicolons) exist in the changed files.
0247 consequence bullet mislabeled the empty-table degrade state
- Severity: nit
- Confidence: 85
- Evidence: llp/0247-absolute-form-third-front-door.decision.md:110
- Why it matters: The bullet tied the empty-routing-table tunnel-only state to "proxy mode on, no upstream naming a host", but the same state is reachable with proxy mode off (zero compiled upstreams plus a CA on disk falls through the idle check into the stale-CA branch), and the doc is headed for Accepted.
- Suggested fix: Describe the state, not one arrival route. (Fixed on the PR head in b63235f.)
Reports: .git/dual-review/pr-797
philcunliffe
commented
Aug 17, 2026
🧭 Decision map — where to spend your attentionCompanion to the dual-review verdict. This casts no verdict — it points at the 6 forks where the author made a real choice, so you can skim the rest. Scanned: 18 hunks across 8 files. Most is mechanical or explanatory: the two new LLP docs (0246/0247) restating the settled rationale, comment and JSDoc honesty updates in connect.js/proxy.js, the types.d.ts field addition, and raw-socket test plumbing. The decisions worth your eyes, in order: 1. Degraded listeners forward absolute-form blind · unhappy-path policy
constabsoluteFormBlind=absoluteForm&&!opts.interceptionconstrecording=!absoluteFormBlind&&((!proxyMode&&!absoluteForm)||shouldRecordProxyExchange(upstream,parsedUrl.pathname))
2. The door opens only beside the CONNECT front door · contract
constforwardProxyDoor=Boolean(opts.interception)||Boolean(opts.tunnelOnly)constabsoluteForm=!proxyMode&&forwardProxyDoor&&/^https?:\/\//i.test(requestUrl)
3. Unregistered hosts are refused, never relayed · contract / security
sendJson(res,403,{error: 'no upstream matches absolute-form host',
4. The control surface never answers absolute-form · security
if(!proxyMode&&!absoluteForm&&isControlPath(parsedUrl.pathname)){
5. Absolute-form routes by the named authority · contract
: absoluteForm
? matchUpstreamByHost(upstreams,parsedUrl.hostname,absoluteFormPort)
: matchUpstream(upstreams,req.method??'GET',parsedUrl.pathname,req.headers)
6. The recorded path is origin-form · contract / shape
path: absoluteForm ? parsedUrl.pathname+parsedUrl.search : requestUrl,
Honorable mentions (real but lower-stakes): Generated by |
Uh oh!
There was an error while loading. Please reload this page.
…he proxy_mode_error read path - The CA on disk is `tls/ca-cert.pem`, not `tls/ca.crt`: LLP 0245 section 1 and the new claude_proxy_capture step 2 both named a path that does not exist, so the procedure's settings check could only fail. - LLP 0246 now names the Remote Control absolute-form issue on master; the proxy-mode-default design renumbered itself to LLP 0251. Both references updated. - proxy_mode_error is published in the gateway source's status details, which `hyp status --json`'s sources array does not carry; the failure step now reads it from `hyp daemon status --json`. - Note the absolute-form third front door (LLP 0246/0247, #797) in the design and in step 4's diagnosis, since it is what makes Remote Control pass. - Correct the launchd env status line and two test paths in the audit.
… 2) (#799) * A terminal marker rewrite records the effect it overwrites (LLP 0247) The reconciler's reverse gap dropped an assetless failed/refused marker for a request key the config stops naming, reading "no installed_assets" as "this key never applied anything". That is only true for the half of an attach that copies files. An attach that reached `done`, wrote the client's settings and copied nothing (openclaw, routinely), then re-performed into failed/refused, produced a marker indistinguishable from one whose attach never touched the disk - and the drop stranded the settings write with nothing naming it. Record a `prior_done` bit on the rewrite and read it in the drop condition, so such a marker is handed to reverse() instead. Item 2 of #780, deferred from PR #630's review. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Review fix: renumber the new decision to LLP 0250, the number is free PR #797 (`fix/absolute-form-third-front-door`) claimed 0247 for `llp/0247-absolute-form-third-front-door.decision.md` about a minute before this branch's commit, so both PRs introduce an LLP 0247 and both merge cleanly (different filenames). The result on master would be two documents claiming one number, an ambiguous `@ref LLP 0247#the-bit`, and a red `test/core/llp-ref-hygiene.test.js` ('no LLP number is claimed by two documents'). LLP 0156#renumber settles this: the later claimant moves above the highest number claimed anywhere. 0245-0249 are all claimed on unmerged branches, so this doc takes 0250. Mechanical rename plus the reference sweep in the same commit (the doc title, the two `Extended-by:` forward-refs on LLP 0138/0186, four `@ref`s and two prose mentions in `action_reconciler.js`, two in `types.d.ts`, and the test file's `@ref` and two test names). Nothing the document decided changes. * Review fix: the re-arm's assetless drop states why it is safe, not a premise LLP 0250 retired --------- Co-authored-by: test <test@example.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: test <test@test.com>
Problem
On proxy-mode installs (post #782/#794),
claudeRemote Control fails with "Remote Control environments are not available for your account". The account message is false: withNO_PROXY=api.anthropic.comit works immediately.The RC bridge's first call is
POST https://api.anthropic.com/v1/environments/bridge. Unlike Claude Code's main API client, the bridge does not open a CONNECT tunnel throughHTTPS_PROXY: it sends the request to the gateway port as absolute-form plaintext (POST https://api.anthropic.com/... HTTP/1.1, verified on the wire). The gateway served only two request shapes (LLP 0233), routed the request by pathname alone (the host on the request line was parsed but never read), answered its own local 404no upstream matches path, and Claude Code misread that 404 as a missing account feature. The same request through a proper CONNECT tunnel returns 200, so forwarding, auth, and TLS trust were all fine.Fix
A third front door (decision LLP 0247, extending LLP 0233; issue LLP 0246):
matchUpstreamByHostforwarding path, exactly like a terminated tunnel./v1/environments/bridgesits outside the Claude anchor, so it forwards unrecorded./_hypaware/control surface never answers an absolute-form target (it is addressed to a third party).Tests
test/plugins/ai-gateway-absolute-form.test.jsreplays the exact on-the-wire shape (raw absolute-form plaintext POST to the listener port):/_hypaware/path is not answered locallyVerified: 89 gateway tests green,
npm run typecheckclean, smokesgateway_claude_capture,gateway_codex_capture,hypignore_capture_drop,otel_loopback_capture,client_attach_idempotentall green. The fullnpm testhas 21 failures that are pre-existing on master (parquet pushdown and gateway-source reload areas, confirmed identical with this change stashed).Upstream
Separately worth filing with Anthropic: the RC bridge client ignores standard
HTTPS_PROXYCONNECT semantics for anhttps://target and sends its bearer token in plaintext to the proxy port. Noted in LLP 0246/0247.🤖 Generated with Claude Code