Skip to content

Proxy-mode capture: design of record for RFC 0231, plus the missing claude_proxy_capture acceptance procedure - #815

Open
philcunliffe wants to merge 10 commits into
masterfrom
integration/proxy-mode-capture
Open

Proxy-mode capture: design of record for RFC 0231, plus the missing claude_proxy_capture acceptance procedure#815
philcunliffe wants to merge 10 commits into
masterfrom
integration/proxy-mode-capture

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes the coverage gap on RFC LLP 0231 (proxy-mode capture for Claude Code) and writes the acceptance procedure that its design cites but that never existed.

The headline: this change set adds no code

The design audit found LLP 0231 is already implemented on master, by human PRs #782 (transport, aperture, CA, attach), #792 (status and trust reporting) and #794 (the LLP 0242-0244 rollout). Verified symbol by symbol before planning: every export in src/core/tls/x509.js, ca.js, darwin_trust.js and src/core/daemon/launchd_env.js exists as designed; the gateway front door (connect.js, proxy.js, source.js, config.js) is complete including the record_prefix merge; Claude attach, disk-driven detach undo, purgeProxyTrustResidue and ProxyTrustReport are all present; sixteen source files already carry @ref LLP 0232-0239; all eight design-named test files exist and pass (120/120), and the gateway_claude_capture smoke is green.

So the plan deliberately schedules no rebuild of working code. It closes only the two gaps the design-versus-tree audit actually found, both documentation.

What is in the diff

  • LLP 0245 (design, covers RFC 0231) and LLP 0248 (plan) - the design of record for what shipped, plus the audit that establishes it shipped.
  • T1 - docs/ACCEPTANCE.md gains the claude_proxy_capture manual procedure. LLP 0245 section 7 cites this procedure; only the codex and openclaw ones had been written. It covers a real hyp daemon install/start, proxy-mode attach writing only HTTPS_PROXY and NODE_EXTRA_CA_CERTS, the keychain trust dialog naming every INTERCEPT_PROVIDER_HOSTS entry, NODE_USE_SYSTEM_CA visible via launchctl getenv with the quit-and-reopen caveat (LLP 0239), a Claude Code session producing ai_gateway_messages rows attributable via entrypoint while Remote Control inbound still works, hyp status reporting the ProxyTrustReport fields, detach restoring env keys while CA and keychain trust survive (LLP 0238), and hyp detach claude --purge plus hyp daemon uninstall removing CA, trust and launchd residue. Also listed in AGENTS.md's written-procedures list (CLAUDE.md is a symlink to it).
  • T2 - llp/0231-proxy-mode-capture.rfc.md gains a **Design:** LLP 0245 forward-ref. A trivial editorial forward-ref, which the repo's conventions permit on an Accepted doc; nothing it settled is touched.

Why the acceptance procedure is the point

Proxy mode is the one part of this subsystem that hermetic smokes structurally cannot prove: it needs a real Mac, a real login keychain and its GUI trust dialog, a real launchd environment, and a real Claude Code session. The written procedure is what a human runs before a release that touched this adapter. Its absence meant the design's own gate was cited but unrunnable.

Review notes

Docs only, no behaviour change. npm test is unaffected. Nothing here needs to be exercised on CI beyond the existing suite; the procedure it adds is by construction a manual gate.

Change-Set: proxy-mode-capture

test and others added 6 commits August 17, 2026 20:35
Design of record for the proxy-mode capture stack (LLP 0231-0239): the
CONNECT front door, routing-table intercept set and path-anchor recording
aperture, the in-process name-constrained CA, macOS keychain trust and
launchd env delivery, and the proxy-mode Claude attach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trivial editorial addition on an Accepted doc: add a Design: LLP 0245
line after Spawns: so the RFC points at its own technical design,
matching the corpus convention of appending discoverable forward-refs.
No body edits, no status change. LLP 0232, 0233 and 0235 already carry
Extended-by/Superseded-by lines and need no equivalent edit.

Task-Id: T2
Design LLP 0245 is realized on master, but the manual acceptance gate
it cites (docs/ACCEPTANCE.md) never got a proxy-mode procedure. Add
claude_proxy_capture in the same shape as codex_desktop_capture and
openclaw_capture: real daemon install/start, attach writing exactly
HTTPS_PROXY and NODE_EXTRA_CA_CERTS, the keychain trust dialog naming
every INTERCEPT_PROVIDER_HOSTS entry, NODE_USE_SYSTEM_CA via
launchctl getenv with the terminal-quit caveat, a live session
landing rows while Remote Control inbound keeps working, hyp status
reporting ProxyTrustReport, detach restoring env keys while the CA
and trust survive, and purge/uninstall removing all of it. List it in
AGENTS.md's (CLAUDE.md) Smoke Test Model written-procedures section.

No code touched; the mechanisms are already built and tested per LLP
0248's audit.

Task-Id: T1
test added 2 commits August 17, 2026 22:00
…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.
…oes not refuse

Findings from the code-review pass, verified against the tree:

- `hyp attach claude` with no CA on disk does not refuse; `index.js:200-207`
  omits `mode` and `attach()` writes a base-URL attach. The only
  `markActionRefused` is settings.js's `CA_MISSING`, which needs proxy mode
  already selected. LLP 0245 sections 4 and 6 and the procedure's first
  "If it fails" bullet all claimed a refusal that never fires; the silent
  base-URL fallback is the failure a release gate has to catch.
- Section 6's dead-gateway claim narrowed to what is actually closed: the CA
  outlives the daemon and a configured `listen` resolves the endpoint from
  config, so a stopped daemon can still take a proxy attach.
- `shouldRecordProxyExchange` uses `recordPrefix ?? prefix`, so an absent
  `record_prefix` records under the routing prefix rather than nothing.
- `SourceSnapshot.state` is started|failed|stopped: `hyp status` prints
  `[started]`, never `[running]`.
- Step 1 now proves the CA file exists before attaching, and says why the jq
  edit can silently not take.
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review, round 1

Verdict: approve after the ten corrections pushed as bf28cf12 and c7318a58. The audit's central claim holds up under an independent check of the tree, and the plan is right to schedule no code. But the two documents this change set adds are the design of record and a manual release gate, and six of the ten findings would have made one of them lie to the person relying on it. That is the whole risk surface of a doc-only PR, and it was live here.

The "already implemented on master" claim: it holds

Checked symbol by symbol against the branch tree rather than taking the audit's word for it.

  • src/core/tls/x509.js: generateKeyPair, mintCertificate, derToPem, readNameConstraints all exported. The implicit-tag claim is real (x509.js:356 emits tlv(0xa0, ...) / tlv(0xa1, ...); x509.js:490-492 reads them back structurally).
  • src/core/tls/ca.js: all nine named exports present; INTERCEPT_PROVIDER_HOSTS is exactly the three LLP 0238 hosts (ca.js:75-79); CA_VALID_DAYS = 3650 matches "ten-year validity".
  • src/core/tls/darwin_trust.js: all five exports; installCaTrust really is security add-trusted-cert -r trustRoot -k <login keychain> with no -d, so "user domain, no sudo" is accurate.
  • src/core/daemon/launchd_env.js: all eight exports, label com.hyperparam.hypaware.node-system-ca.
  • Gateway: connect.js (attachConnectFrontDoor, isLoopbackAddress, CONNECT_HOST/CONNECT_PORT + readers, openUpstream, parseAuthority), proxy.js (startProxy, interceptsHost, matchUpstreamByHost, shouldRecordProxyExchange, compileUpstreams, createChainedAgent), source.js:598-605 (the record_prefix merge), config.js:39 (cfg.proxy_mode === true). The mechanical claims check out too: connect.js:216 is ALPNProtocols: ['http/1.1'], connect.js:244 is server.emit('connection', tlsSocket), proxy.js:128-129 destroys hijacked tunnels inside stop().
  • Claude attach: MODE_PROXY/MODE_BASE_URL, PROXY_MODE_ENV_KEYS = ['HTTPS_PROXY', 'NODE_EXTRA_CA_CERTS'] (settings.js:105), releaseUnmanagedKeys (settings.js:479), detachClientFromDisk (client_detach_disk.js:122), purgeProxyTrustResidue (clients.js:1586), ProxyTrustReport (daemon/types.d.ts:251).
  • The @ref LLP 0232-0239 count is 16 files, as claimed. The ten named test files run 120/120 green, also exactly as claimed. npm test is 4216 pass / 1 skip / 0 fail and npm run smoke -- gateway_claude_capture is green.

So "the correct output for already-built scope is no diff" is the right instruction to leave behind, and I did not write any code.

Findings

1. High - the CA file this design and procedure name does not exist. llp/0245:45 and docs/ACCEPTANCE.md:615 said NODE_EXTRA_CA_CERTS points at <state root>/tls/ca.crt. The real filename is ca-cert.pem (src/core/tls/ca.js:41-43); the string ca.crt appeared nowhere in the tree outside these two new lines. Step 2's pass condition is a literal path comparison, so a correct install would have failed the gate. Fixed in both.

2. High - the "preflight refuses" story is not what the code does. llp/0245:213 said "the proxy-attach preflight refuses (markActionRefused) unless readLocalCaInfo() finds a CA on disk", llp/0245:266 built the section 6 dead-gateway argument on it, and docs/ACCEPTANCE.md:761 opened "If it fails" with it. In fact claude/src/index.js:200-207 simply omits mode when there is no CA, so attach() writes a base-URL attach and says nothing about proxy mode. The only markActionRefused on this path is CA_MISSING (claude/src/settings.js:185), which requires proxy mode to have been selected already and the certificate to have become unreadable in between. Note this is LLP 0232's own tension, not an invention of LLP 0245: LLP 0232 #proxy-attach-preflight says "a missing CA is a refusal (markActionRefused), not a warning, and nothing is written" and then, two sentences later, "proxy mode when a CA exists and base-URL mode otherwise". The tree implements the second. Fixed by describing the implemented behaviour in LLP 0245 and recording the discrepancy explicitly, rather than by touching Accepted LLP 0232. The practical stake for the gate is large: a silent base-URL fallback would have let a human run all seven steps and "pass" a procedure that never exercised proxy mode.

3. Medium - section 6's dead-gateway claim overstated what is closed. The CA survives a stopped daemon by design (LLP 0238), and with a configured listen hyp attach resolves the endpoint from config rather than a live bind (src/core/commands/clients.js:329-333), so HTTPS_PROXY can still be written at a dead port. Fixed: the failure mode is now recorded as residual, with the preflight credited only for the case it actually covers.

4. Medium - LLP 0246 now names a different document. llp/0245:31, llp/0245:40 and llp/0248:92 used "LLP 0246" for the proxy-mode-default / attach-migration design. On master, llp/0246-remote-control-absolute-form-requests.issue.md and llp/0247-absolute-form-third-front-door.decision.md took those numbers when #797 merged, and the sibling change set has already renumbered itself (3efab8c2 on integration/proxy-mode-default-attach: "LLP 0246 renumbered to 0251"). Fixed: all three now read LLP 0251.

5. Medium - the design's front-door map is one door short of master. Section 3 describes the two front doors LLP 0233 settled, but master has carried a third since #797: Remote Control's bridge sends absolute-form plaintext requests straight at the proxy port, and routing them by the host the request line names is what makes step 4's Remote Control observation pass at all. A design claiming to be "the one place the whole mechanism is laid out end to end" should not omit the mechanism carrying its headline result. Fixed additively (a paragraph at the end of section 3, LLP 0246/0247 added to Related:, and a diagnostic bullet in "If it fails" distinguishing the account-message symptom from the base-URL one). Nothing already settled was rewritten.

6. Medium - proxy_mode_error cannot be read the way the procedure said. docs/ACCEPTANCE.md:785 told the operator to confirm it is absent from hyp status --json | jq '.sources'. It is never there: src/core/commands/status.js:159-167 maps each source to name/plugin/state/optional provenance/optional error and drops the details block where the field lives (ai-gateway/src/source.js:148-150). A jq that always returns null reads as "no error", so the check silently passes on a broken install. Fixed to hyp daemon status --json (src/core/commands/daemon.js:83-85 writes the whole status file), with a note about why hyp status --json cannot answer it.

7. Medium - [running] is not a source state. docs/ACCEPTANCE.md:579 expected the gateway source rendered [running]; SourceSnapshot.state is 'started' | 'failed' | 'stopped' (src/core/daemon/types.d.ts:35, set at src/core/daemon/status.js:758, rendered [${s.state}] at src/core/commands/status.js:345). Fixed to [started].

8. Medium - step 1 never verified that proxy mode took. The jq map is a silent no-op when no @hypaware/ai-gateway entry matches, and on a centrally managed host a local proxy_mode write loses the LLP 0031 layer merge outright. Combined with finding 2 this is how the whole procedure passes while measuring base-URL attach. Fixed: step 1 now also checks ~/.hyp/hypaware/tls/ca-cert.pem exists, and says why all three checks matter.

9. Low-medium - "an absent anchor records nothing" is wrong. llp/0245:154 and :303 both said so; shouldRecordProxyExchange is recordPrefix ?? prefix (ai-gateway/src/proxy.js:230-234), so an upstream with no record_prefix records under its routing prefix, and only / or empty records nothing. This is the aperture, so an inaccurate statement of it in the design of record is worth correcting. Fixed, with the source.js merge credited as what actually keeps a routing path_prefix of / from reading as record-everything.

10. Low - step 5's expected status line is not what the code prints. docs/ACCEPTANCE.md:687 expected launchd env: set; the actual line is launchd env: NODE_USE_SYSTEM_CA=1 set (status.js:430, describeLaunchdEnv at :577). Also low, same commit: llp/0248:58 listed attach-proxy-migration.test.js and gateway-proxy-enable.test.js at the tail of a run of test/plugins/ paths; both live in test/core/. Both fixed.

Checked and clean

  • LLP immutability: the llp/0231 diff is exactly one added line, **Design:** LLP 0245, directly after **Spawns:**. No body edit, no status change, nothing the RFC settled is rewritten. Squarely the trivial-editorial-forward-ref carve-out. My own edits stayed inside the two documents this PR introduces; LLP 0232's contradiction (finding 2) was recorded, not edited.
  • @ref and anchor honesty: the coverage anchor @ref LLP 0231: resolves, and every prose anchor resolves to a real heading slug: 0233#loopback-peers-only, #degrade-to-blind-tunnels, #proxy-mode-is-explicit, 0234#recording-is-opt-in-per-path, 0232#mode-migration, #proxy-attach-preflight, 0237#attach-anyway-on-refusal, #darwin-only, 0238#ca-survives-detach, #full-provider-constraints, 0239#terminals-predating-attach, #launchctl-setenv, plus the 0245#claude-attach / #status / #failure-modes self-links.
  • The rest of the procedure's commands. hyp status [--json], hyp daemon install|start|status|uninstall, hyp attach claude, hyp detach claude [--purge], hyp query sql are all real verbs with the flags as spelled. The step-1 jq shape matches the real config (<HYP_HOME>/hypaware-config.json, plugins[].name, .config.proxy_mode; cross-checked against test/core/gateway-proxy-enable.test.js:109). ~/Library/Keychains/login.keychain-db matches loginKeychainPath. The step-4 columns (entrypoint, client_name, message_created_at, date) all exist in message_projector.js. The Remote Control refusal string quoted in step 4 is verbatim from LLP 0231:26-27. The JSON keys ca_fingerprint / ca_trusted / launchd_env_set and the text login keychain: trusted were already exact. purgeProxyTrustResidue really does all three removals, so step 7's expectations are right.
  • Prose conventions: zero U+2014 across all five touched files, before and after my edits. The AGENTS.md entry is well-formed and in the right list (CLAUDE.md is a symlink to it).

Left unfixed, deliberately

CI note

The pull_request CI run on my push is red, and so is master's own CI (70b9c1c7, dfe3e0e4, 51a011ac all fail). The four failures are union-source absent-column tests from #740 and have nothing to do with this diff. The push run against the branch tree is green, as is npm test locally on c7318a58 (4216 pass / 1 skip / 0 fail) plus npm run smoke -- gateway_claude_capture. Worth someone unbreaking master independently of this PR.

Pushed as bf28cf12 then c7318a58.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Heads up before merging: LLP 0245 is claimed twice

This PR's llp/0245-proxy-mode-capture.design.md collides with llp/0245-otel-attach-replaces-proxy.rfc.md on otel-attach-0245 (PR #818, "OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0245)").

Neither branch's CI can see this. Each is internally consistent, duplicate-numbers passes on both, and the failure only appears on master after the second one merges: an ambiguous @ref LLP 0245, and a red ref-hygiene check.

Claim order, measured from the commit that added each file

claim added commit
0245-proxy-mode-capture.design.md (this PR) 2026-08-17 20:35:29 UTC 39cd8038
0245-otel-attach-replaces-proxy.rfc.md (#818) 2026-08-17 21:16:34 UTC 84cfbcb2

This PR claimed it first, by about 41 minutes. Neither number is on master, so there is no merged claim to defer to. By the repo's renumber rule (later claimant moves above the highest number claimed anywhere, currently 0259), the document that should move is #818's, to 0260.

Why neutral has not fixed it

otel-attach-0245 is not a branch neutral owns (it is neither integration/* nor fix/issue-*) and carries no neutral:adopt or neutral:review label, so neutral will not push to it.

Nor has neutral renumbered its own document, for two reasons. It holds the earlier claim, so moving it would invert the rule. And it would cascade: llp/0251-proxy-mode-default-attach.design.md on integration/proxy-mode-default-attach (PR #817) cites LLP 0245 twice, as does llp/0248-proxy-mode-capture.plan.md here, so the sweep would span two change sets to resolve a collision this side did not create.

What resolves it

  1. OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) #818 renumbers to 0260 (what the rule prescribes). Note its branch name and PR title also carry 0245, so there is a cosmetic cost there even though only the filename and its references matter for correctness.
  2. Or tell neutral to move its own to 0260 by replying here. It will sweep this branch and the two references on integration/proxy-mode-default-attach in one pass and re-verify no number collides anywhere.
  3. Or merge one and let the other rebase. Whichever merges second has to renumber regardless, so deciding now is cheaper than discovering it at the second merge.

Either way this is a maintainer's call between two people's work, which is why it is a note rather than a change. Reply here and neutral will act on its next tick.

For reference, the full claimed range across every branch right now is 0245-0259, with 0245 the only duplicate.

… fields hyp status drops

- The rollout is LLP 0242-0244 (04330ab, #794), on master; LLP 0251 is an
  unmerged number on a sibling branch, so three references dangled.
- Section 5 listed proxy_mode, expiry, cert path and permitted/intercepted
  hosts as hyp status output; ProxyTrustReport carries three fields and
  hyp status --json drops the source details block those live in.
- Section 3's / anchor rationale was inverted: the fail-closed guard is what
  stops record-everything, and the source.js merge is what stops the default
  install recording nothing.
- 0231's forward-ref uses the corpus's Designed-by name.
- Acceptance: purge deletes the CA while the running daemon keeps its own, so
  a purge-then-attach silently downgrades to base URL; the step-7 restore
  raced the CA mint the same way. Config backed up and restored, anchors
  folded into their links (#proxy-attach-preflight is LLP 0232's, not 0245's).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review, round 2

Verdict: approve after the seven corrections pushed as b566a698. Round 1's ten fixes hold, with one exception it created: the LLP 0246 renumber it applied pointed the design at LLP 0251, a number that exists only on an unmerged sibling branch. Beyond that, the master merge invalidated nothing, but re-reading the design against the refreshed tree surfaced two more places where the document of record describes behaviour the code does not have.

Did the master merge invalidate anything? No.

The branch's merge base is now a14246d9 (master's tip), so this is a real re-check against current master, not the round-1 tree.

  • "RFC 0231 is already implemented on master" still holds. LLP 0241's padding change and Union absent-column tests pin the pre-LLP-0241 contract, so master is red (#820) #821's test correction touch the union/scan-column path, which this design does not describe. Every symbol the design and plan name is still present and still exported: src/core/tls/{x509,ca,darwin_trust}.js, src/core/daemon/launchd_env.js, the gateway's connect.js / proxy.js / source.js / config.js, claude/src/{index,settings}.js, client_detach_disk.js, purgeProxyTrustResidue, ProxyTrustReport.
  • Paths, env vars and verbs still match. ~/.claude/settings.json, <stateRoot>/tls/ca-cert.pem (ca.js), PROXY_MODE_ENV_KEYS = ['HTTPS_PROXY', 'NODE_EXTRA_CA_CERTS'], NODE_USE_SYSTEM_CA via launchd_env.js, and hyp status [--json] / hyp daemon install|start|restart|status|uninstall / hyp attach claude / hyp detach claude [--purge] / hyp query sql all still resolve as spelled.
  • The dangling links are fixed by the merge. llp/0246-remote-control-absolute-form-requests.issue.md and llp/0247-absolute-form-third-front-door.decision.md are now in the branch tree, so the two ../llp/0246-* / 0247-* links in the "If it fails" section resolve. That leftover is closed.
  • Numbering is clean. OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) #818 renumbered off 0245 (its title now reads LLP 0262). Sweeping llp/ on every remote branch, 0245- and 0248- appear on integration/proxy-mode-capture and nowhere else. duplicate-numbers passes.

Round 1's own fixes: all landed, one wrong

Verified each against the current tree.

  • ca-cert.pem (not ca.crt) in both llp/0245 and docs/ACCEPTANCE.md: correct, matches src/core/tls/ca.js. ca.crt appears nowhere in the tree.
  • The refusal-versus-fallback correction: correct, and consistent with the resolution in flight. claude/src/index.js:201-208 reads readLocalCaInfo() and simply omits mode when there is no CA, so attach writes a base-URL attach. PR detach --purge then attach no longer downgrades claude to base URL in silence #822 mints LLP 0259 settling LLP 0232's self-contradiction in favour of the fallback ("0259 settles it in favour of the fallback ... The fallback was never the bug. The silence was"). This PR's prose says the same thing, records the same LLP 0232 tension, and treats the silence as the hazard (step 1's CA check, the first "If it fails" bullet). Nothing here is contradicted by detach --purge then attach no longer downgrades claude to base URL in silence #822.
  • [started], hyp daemon status --json for proxy_mode_error, the launchd env: NODE_USE_SYSTEM_CA=1 set line, the test/core/ paths, step 1's CA check, and the additive absolute-form paragraph: all present and all check out.
  • The one that did not hold: finding 4 swapped LLP 0246 for LLP 0251. See finding 1.

Findings

1. Medium - LLP 0251 is not in the corpus. llp/0245:31 (§0, "the rollout work covered separately by LLP 0251"), llp/0245:40 ("designed in LLP 0251, which depends on this change set") and llp/0248:93 ("was designed in LLP 0251 per the design's section 0, and in fact already landed on master as #794"). Round 1 renumbered off 0246 because a sibling change set had moved to 0251, but PR #817 is still open, so 0251 exists on no merged tree; an anchor sweep over the whole diff flags exactly these three and nothing else. The rollout it means is real and merged: LLP 0242 (issue), LLP 0243 and LLP 0244 (both Accepted), landed as 04330abb (#794) - which is this branch's own merge base, as llp/0248:17 already says. The plan's sentence was also self-contradictory in one clause ("designed in LLP 0251 ... and in fact already landed as #794"). Fixed: all three now cite LLP 0242-0244 and 04330abb, no unmerged number is referenced, and Related: gains 0242/0243/0244.

2. Medium - section 5 promises status output that does not exist. llp/0245:255-261 said src/core/commands/status.js and ProxyTrustReport report "gateway proxy_mode, CA fingerprint, expiry, cert path, permitted and intercepted hosts, proxy_mode_error, keychain trust state, and whether NODE_USE_SYSTEM_CA is live". ProxyTrustReport is three fields (caFingerprint, trusted, launchdEnvSet, src/core/daemon/types.d.ts:261-268) and the renderer prints exactly three lines (status.js:471-475). proxy_mode, ca_not_after, ca_cert_path, ca_permitted_hosts, intercept_hosts and proxy_mode_error live in the gateway source's status details (ai-gateway/src/source.js:136-150), which hyp status --json deliberately drops - sources maps to name/plugin/state only (status.js:160-168). This is the same fact round 1 fixed in docs/ACCEPTANCE.md:790, so the design of record contradicted the procedure this PR ships alongside it. Fixed: §5 is now two paragraphs, the trust half on hyp status and the aperture half in the daemon status file, with the drop stated.

3. Medium - section 3's / rationale is inverted. llp/0245:155-158 said the source.js merge "is what keeps a routing path_prefix of / from reading as record-everything", and :167-170 repeated it. shouldRecordProxyExchange returns false for an anchor of / or empty (proxy.js:236-241), so / can never read as record-everything with or without the merge. The merge exists for the opposite failure, which the code comment states outright (source.js:598-605): hyp init writes path_prefix = "/", so reading the routing prefix as the record anchor "made proxy mode record nothing at all on a default install". The second clause also had the direction backwards - the merge is precisely where the preset wins the record anchor while operator config keeps the routing question (source.js:594-596). Round 1's finding 9 corrected the first half of this bullet and left the causal claim standing. Fixed in both places.

4. Medium - the procedure's own purge advice walks into issue #819. Two places tell the operator to run hyp detach claude --purge mid-procedure for a clean dialog observation: the Requires bullet (docs/ACCEPTANCE.md:555) and the second "If it fails" bullet (:780), the latter concluding "re-running hyp attach claude retries the dialog". It does not. --purge deletes tls/ca-cert.pem and never touches config; the running daemon holds the CA it loaded at boot and never re-mints; attach reads the mode off that file. So the follow-up attach finds no CA and silently writes a base-URL attach - exactly the defect #819 reports and #822 fixes - and the symptom is "still no dialog", indistinguishable from the failure being diagnosed. Fixed: both places now require hyp daemon restart and a CA re-check before re-attaching, and say why the symptom is ambiguous.

5. Medium - step 7's restore block races the CA mint. docs/ACCEPTANCE.md:758-762 ran hyp daemon install / start / hyp attach claude back to back to put the tester's working machine back. hyp attach claude preflights with readLocalCaInfo() and does not wait; waitForLocalCa() exists for this race but is used only by walkthrough.js:1558 and gateway_proxy_enable.js:251. The gateway mints asynchronously after boot, so the procedure's own last step can leave the tester's machine base-URL-attached with Remote Control broken. Fixed: an ls gate before the attach, with the reason stated.

6. Low - step 1 rewrote the operator's real config with no backup and no restore, and on an already-running daemon install/start do not reload it, so the step-1 CA gate fails for an unrelated reason (hyp daemon restart was mentioned only down in "If it fails"). Fixed: a cp backup before the jq, an explicit hyp daemon restart in the block, and a restore in step 7.

7. Low - seven anchors sat outside their links, and one was misattributed. docs/ACCEPTANCE.md:545, 555, 592, 609, 625, 634, 708 wrote [LLP 0237](path.md) followed by a bare #darwin-only on the next line, so none were navigable; the rest of the file uses the in-link form (:204, :228, :452). Worse, :592 read [LLP 0245#claude-attach](...) #proxy-attach-preflight, but #proxy-attach-preflight is LLP 0232's anchor, not 0245's (0245 has scope/data-flow/core-tls/front-door/claude-attach/status/failure-modes/tests). Fixed: all seven folded into their links, and :592 now links LLP 0232 explicitly.

8. Low - **Design:** is a one-off field name. llp/0231:10. The corpus's forward-ref idiom is -by: Extended-by (45), Planned-by (3), Superseded-by, Decided-by, and Designed-by for exactly this relation (llp/0044-client-attach-on-join.decision.md:9, "Designed-by: LLP 0045, client attach implementation design"). A one-off name makes the edge invisible to anything grepping the conventional ones. Fixed to **Designed-by:** LLP 0245, proxy-mode capture technical design, and LLP 0248's T2 task text updated to match.

Checked and clean

  • LLP immutability. git diff origin/master -- llp/0231-...rfc.md is still exactly one added header line after **Spawns:**. No body edit, no status change, nothing the RFC settled is rewritten. My own edits stayed inside the two documents this PR introduces plus that one line.
  • Anchor honesty. A sweep of every LLP NNNN#anchor and every ../llp/*.md#anchor across all five touched files: all resolve, and every referenced LLP number is now in the corpus. Two broken anchors remain in the tree but are pre-existing and outside this diff: docs/ACCEPTANCE.md:228 cites LLP 0229#status-derives-by-the-same-gate (0229's anchors are context/decision/consequences/open-questions/references), and llp/0231:80,142 cite LLP 0114#interception-accepted (0114 carries no {#...} anchors at all). Both predate this PR; 0231's body is Accepted and not this change set's to edit.
  • The @ref coverage anchor @ref LLP 0231: resolves, and the design's own self-links (#claude-attach, #status, #failure-modes) resolve.
  • Prose conventions. Zero U+2014 across all five files, before and after my edits.
  • Numbering as above: 0245 and 0248 uniquely claimed across every remote branch.
  • The rest of the procedure re-verified against current master: the step-1 jq shape, INTERCEPT_PROVIDER_HOSTS (the three hosts named in step 2), the ProxyTrustReport JSON keys and the proxy trust: text block, loginKeychainPath, the ai_gateway_messages columns in step 4, deleteLocalCa's directory behaviour, both com.hyperparam.* launchd labels, and every named test file and smoke flow.

Left unfixed, deliberately

No code changed, so no test run was warranted; the diff remains documentation-only and duplicate-numbers passes.

Pushed as b566a698.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Triage at head b566a698: every residual finding is non-blocking. All 18 review-round fixes are verified landed and correct against the tree, the claude_proxy_capture procedure is safe for an operator to execute (backup/restore, the #819 purge trap, and the step 7 attach/mint race are all handled), no reference points at an LLP that exists only on an unmerged branch, every anchor in the diff resolves, and LLP 0245/0248 are uniquely claimed across all remote branches. Six deferred, non-blocking findings (doc completeness, two pre-existing broken anchors, a step 7 restore edge case for pre-rollout configs, a step 1 mint-race caveat, a wrong remedy string in master's stale-CA warning, and the PR #791 overlap decision) are recorded in follow-up issue #831.

@philcunliffe
philcunliffe marked this pull request as ready for review August 18, 2026 05:45
@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant