From 39cd8038dcec14a530a336c67502e82703b4fdbf Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 20:35:29 +0000 Subject: [PATCH 1/7] LLP 0245: proxy-mode capture technical design covers RFC 0231 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) --- llp/0245-proxy-mode-capture.design.md | 287 ++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 llp/0245-proxy-mode-capture.design.md diff --git a/llp/0245-proxy-mode-capture.design.md b/llp/0245-proxy-mode-capture.design.md new file mode 100644 index 000000000..c84a7957a --- /dev/null +++ b/llp/0245-proxy-mode-capture.design.md @@ -0,0 +1,287 @@ +# LLP 0245: Proxy-mode capture for Claude Code, technical design + +**Type:** design +**Status:** Active +**Systems:** Gateway, Sources, Config, Plugins, Privacy, Core, Daemon +**Generated-by:** neutral +**Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0234, LLP 0235, LLP 0236, +LLP 0237, LLP 0238, LLP 0239, LLP 0044, LLP 0045, LLP 0114, LLP 0192, +LLP 0206 + +> Technical design for the proxy-mode capture stack the accepted RFC +> LLP 0231 asked for: Claude Code routed through the gateway with +> `HTTPS_PROXY` and a machine-local CA instead of a repointed +> `ANTHROPIC_BASE_URL`, with the aperture narrowed so the rows recorded are +> exactly the rows the reverse proxy already records. Named files, function +> seams, data flow, failure modes, and what the tests prove. + +Coverage anchor: + +`@ref LLP 0231: proxy-mode capture for Claude Code; the CONNECT front door, the routing-table intercept set and path-anchor recording aperture, the in-process name-constrained CA, the macOS keychain-trust and launchd-env delivery, and the proxy-mode Claude attach this document designs are LLP 0231's realization` + +## 0. Scope and code status {#scope} + +LLP 0231 is the accepted RFC; LLP 0232 through 0235 are its spawned +decisions, and LLP 0236 through 0239 are the research and follow-on +decisions that corrected the trust story after live testing. This document +is the implementation design that binds those decisions to the tree. + +The design is realized on `master` by three commits: `fa701a7e` (#782, the +transport, aperture, CA and attach), `d0f7c4ad` (#792, the status and trust +reporting surface), and the rollout work covered separately by LLP 0246. +File paths and function names below are verified against that tree; the +tests named in section 7 exist and gate it. What this document adds to the +corpus is the request-level design of record: the one place the whole +mechanism is laid out end to end, with the request (LLP 0231) `@ref`'d +above. + +This change set deliberately excludes who *turns proxy mode on*. Fresh +install composition and the existing-install migration are LLP 0242's +problem and are designed in LLP 0246, which depends on this change set. + +## 1. Data flow, end to end {#data-flow} + +1. Attach writes `env.HTTPS_PROXY = http://127.0.0.1:` and + `env.NODE_EXTRA_CA_CERTS = /tls/ca.crt` into + `~/.claude/settings.json`. The base URL is untouched, so Claude Code's + first-party predicate stays true on its own terms (LLP 0232). +2. Claude Code opens `CONNECT api.anthropic.com:443` against the gateway's + one listener. The CONNECT front door checks the peer is loopback, + answers `200 Connection Established` on the raw socket, and only then + wraps the socket in TLS with a leaf minted for the target host + (LLP 0233). +3. If no registered upstream names the target host and port, the socket is + a blind tunnel: bytes piped, nothing decrypted, nothing recorded + (LLP 0234). +4. An intercepted socket re-enters the same HTTP server via + `server.emit('connection', tlsSocket)`, stamped with the CONNECT + authority. The ordinary request handler runs; routing proceeds as for + reverse-proxy traffic. +5. An exchange is recorded only when the request path matches the + upstream's `record_prefix`, which is the adapter preset's declared + `path_prefix` (`/v1/messages` for Anthropic). Unmatched paths are + forwarded faithfully and never buffered (LLP 0234). +6. Recorded exchanges take the existing recorder, projector and cache + write path unchanged; capture parity with base-URL mode was measured at + 4 rows / 35 populated columns for the same prompt (LLP 0231). + +Two more macOS-only deliveries make the attach complete: the CA is +installed as a user-domain trusted root in the login keychain (LLP 0237), +and `NODE_USE_SYSTEM_CA=1` is set in the launchd user environment with a +login LaunchAgent to re-apply it (LLP 0239). Both exist because Claude +Code verifies TLS against two different stores (LLP 0236): the main client +honours `NODE_EXTRA_CA_CERTS`; the Remote Control SSE transport honours +only the keychain, merged in at process boot by that variable. + +## 2. Core TLS toolkit: `src/core/tls/` {#core-tls} + +The CA lives in core, not in the gateway plugin, because `hyp detach` and +`hyp daemon uninstall` must be able to remove it with no plugin loadable +(LLP 0235). + +- `src/core/tls/x509.js`: minimal DER emitter signed with `node:crypto`, + scoped to EC P-256 / ECDSA-SHA256 / UTCTime and the fixed extension set + interception needs. Exports `generateKeyPair()`, `mintCertificate()`, + `derToPem()`, and `readNameConstraints()`. Constraints are encoded with + **implicit** tags for `permittedSubtrees [0]` / `excludedSubtrees [1]` + (wrapping instead of replacing the `SEQUENCE OF` tag silently voids the + constraint), all IPv4 and IPv6 space is excluded (a dNSName-only + constraint leaves IP identities unrestricted per RFC 5280 4.2.1.10), and + read-back is a structural DER walk, not a byte scan (LLP 0235). +- `src/core/tls/ca.js`: the CA lifecycle. Exports + `INTERCEPT_PROVIDER_HOSTS` (the reviewed full-provider constant: + `api.anthropic.com`, `api.openai.com`, `chatgpt.com`, per LLP 0238), + `defaultStateRoot()`, `caPaths()` (the `tls/` directory in the state + root), `fingerprint()`, `ensureLocalCa()` (mint or reuse; key written + mode 0600; ten-year validity; regenerates when the stored key does not + match the stored certificate, which concurrent daemon starts can + produce; renewal roll kept for eventual expiry, deliberately longer than + a leaf's lifetime), `createLeafStore()` (per-host leaves minted in + memory, never written to disk), `readLocalCaInfo()`, + `waitForLocalCa()` (polling used by attach-side flows), and + `deleteLocalCa()` (uninstall and explicit purge only; detach keeps the + CA per LLP 0238). +- `src/core/tls/darwin_trust.js`: keychain trust. Exports + `CA_COMMON_NAME` (`HypAware Local CA`), `loginKeychainPath()`, + `isCaTrusted()` (read-only `security verify-cert` probe, making the + install idempotent and the password dialog once-per-machine), + `installCaTrust()` (`security add-trusted-cert -r trustRoot`, user + domain, no sudo; the native macOS dialog is the consent moment), and + `removeCaTrust()` (LLP 0237). +- `src/core/daemon/launchd_env.js`: boot-environment delivery. Exports + `ENV_VAR_NAME` (`NODE_USE_SYSTEM_CA`), `ENV_VAR_VALUE`, + `ENV_AGENT_LABEL` (`com.hyperparam.hypaware.node-system-ca`), + `envAgentPlistPath()`, `buildEnvAgentPlist()`, `installLaunchdEnv()` + (`launchctl setenv` plus the login LaunchAgent), `removeLaunchdEnv()`, + and `isLaunchdEnvSet()` (`launchctl getenv`, feeding status). Unlike the + CA, these follow the attach: they are recreatable for free, so detach + removes them (LLP 0239). + +## 3. Gateway front door: `@hypaware/ai-gateway` {#front-door} + +`hypaware-core/plugins-workspace/ai-gateway/src/connect.js`: + +- `attachConnectFrontDoor(opts)` installs the `connect` handler on the + **existing** HTTP server; there is no second port and every status and + discovery surface keeps meaning one thing (LLP 0233). The two mechanical + constraints it carries are load-bearing: the TLS socket offers + `http/1.1` only in ALPN (an h2 negotiation would hang against the + HTTP/1.1 server), and the `200 Connection Established` plus any early + bytes are written to and pushed back onto the raw socket **before** the + TLS wrap. +- `isLoopbackAddress()` gates the peer, not the bind: a non-loopback + `CONNECT` is refused `403` before the target is parsed, blind tunnels + included, so a `listen = "0.0.0.0"` install is never an open relay + (LLP 0233 #loopback-peers-only). +- `CONNECT_HOST` / `CONNECT_PORT` symbols stamp the terminated socket with + the CONNECT authority, the only new fact the request path needs; + `connectHostOf()` / `connectPortOf()` read them back. +- `openUpstream()` dials the destination directly or through a configured + `upstream_proxy` chain; `parseAuthority()` parses the CONNECT target. + +`hypaware-core/plugins-workspace/ai-gateway/src/proxy.js`: + +- `startProxy(opts)` boots the listener; with `proxy_mode` on and nothing + to route it starts tunnel-only and says so, and with a CA on disk but + interception unavailable it degrades to blind-tunnel-only rather than + refusing CONNECT, because an attached client's whole egress arrives here + (LLP 0233 #degrade-to-blind-tunnels). +- `interceptsHost(upstreams, host, port)` and + `matchUpstreamByHost(upstreams, host, port)`: the intercept set is + derived from the routing table and keyed on host **and** port; nothing + configures it separately (LLP 0234). +- `shouldRecordProxyExchange(upstream, pathname)`: the recording anchor. A + compiled `recordPrefix` of `/` or absent records nothing; failing closed + is the default. The routing matcher is deliberately not reused: the + Anthropic route matcher accepts an `sk-ant-` bearer alone, which under a + proxy is true of every request to the host and measurably reopened the + aperture (LLP 0234 #recording-is-opt-in-per-path). +- `compileUpstreams()` carries `record_prefix` onto the compiled entry; + `createChainedAgent()` chains the intercepted leg through + `upstream_proxy`. + +`hypaware-core/plugins-workspace/ai-gateway/src/source.js` merges each +adapter preset's declared `path_prefix` (and `provider`) onto the merged +upstream entry as `record_prefix`, because operator routing config wins +over presets and a routing `path_prefix` of `/` must never read as +record-everything; without this the default install recorded nothing at +all (LLP 0234). It also owns the status surface: `proxy_mode`, +`ca_fingerprint`, `ca_not_after`, `ca_cert_path`, `ca_permitted_hosts`, +intercepted hosts, and `proxy_mode_error` when CA preparation failed while +the gateway kept reverse-proxying. + +`hypaware-core/plugins-workspace/ai-gateway/src/config.js` reads the +switch: `proxy_mode` is on only when the config field is literally `true` +(LLP 0233 #proxy-mode-is-explicit), and compiles `upstream_proxy`. + +## 4. Claude attach: `@hypaware/claude` {#claude-attach} + +`hypaware-core/plugins-workspace/claude/src/settings.js`: + +- Exports `MODE_PROXY` / `MODE_BASE_URL`; `attach()` takes the mode and + writes, for proxy mode, exactly `HTTPS_PROXY` and `NODE_EXTRA_CA_CERTS` + (`PROXY_MODE_ENV_KEYS`). `HTTP_PROXY` and `NO_PROXY` are never written; + `ENABLE_TOOL_SEARCH` and `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL` are + not needed and not written (LLP 0232). +- A displaced pre-existing `HTTPS_PROXY` is backed up to the marker's + `prev_env`, warned about once with userinfo redacted, and the warning + names `upstream_proxy` as the remedy for a corporate egress proxy. +- The marker (`value[MARKER_KEY]`) records `mode`, `managed.env`, + `managed.hooks`, `prev_env`, and keeps `prev_base_url` as its own field + so markers written by earlier versions still restore. `mode` is what + tells the plugin-agnostic undo that proxy residue exists. +- `releaseUnmanagedKeys()` implements mode migration: switching modes + restores-or-removes keys the new mode no longer manages, so migrating to + proxy never strands a live `ANTHROPIC_BASE_URL` pointing at the gateway + (LLP 0232 #mode-migration). +- The session-start hooks, `cwd` attribution and the settlement sidecar + ride the same `env`-block write and are untouched (LLP 0231). + +`hypaware-core/plugins-workspace/claude/src/index.js`: + +- The proxy-attach preflight refuses (`markActionRefused`) unless + `readLocalCaInfo()` finds a CA on disk: the CA's existence proves the + gateway is actually serving the mode, and a proxy attach against a dead + gateway would break all of Claude Code's HTTPS (LLP 0232 + #proxy-attach-preflight). Mode is read from what the daemon is doing, + never from what config asks for. +- On Darwin the attach then runs the trust and launchd steps: keychain + probe/install (dialog names all permitted hosts, per LLP 0238), and + `installLaunchdEnv()`. A refused dialog degrades politely: attach + completes, prints exactly what will not work (Remote Control inbound), + and re-running attach retries (LLP 0237 #attach-anyway-on-refusal). On + other platforms the attach states Remote Control inbound is unsupported + under proxy mode (LLP 0237 #darwin-only). The final attach line carries + the corrected terminal caveat: only processes launchd starts after the + `setenv` see the variable, so a terminal app must be fully quit and + reopened (LLP 0239 #terminals-predating-attach). + +Detach and uninstall run the plugin-agnostic, disk-driven undo +(`src/core/config/client_detach_disk.js`, `detachClientFromDisk()`): +restore every managed env key from `prev_env` (falling back to +`prev_base_url`), strip managed hooks, remove the launchd env and its +LaunchAgent, and delete the marker. The CA and its keychain trust +*survive* detach so re-attach is silent; `hyp daemon uninstall` and +`hyp detach claude --purge` (`purgeProxyTrustResidue()` in +`src/core/commands/clients.js`) remove them (LLP 0238 +#ca-survives-detach). The state root for the undo is resolved from the +caller's `homeDir`, never the ambient one, so a sandboxed undo cannot +delete a different install's key material. + +## 5. Status surface {#status} + +`src/core/commands/status.js` and `src/core/daemon/types.d.ts` +(`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 in the launchd +environment (`launchctl getenv`). "The dialog was cancelled last month" is +diagnosable without re-running attach (LLP 0237), and the aperture is +readable without grepping a boot log (LLP 0233). + +## 6. Failure modes {#failure-modes} + +- **Dead gateway behind a proxy attach**: the worst mode the feature can + have; prevented by the CA-existence preflight refusal (never a warning). +- **CA preparation fails at boot**: gateway still starts and still reverse + proxies; `proxy_mode_error` reported; degraded blind-tunnel CONNECT + keeps an already-attached client's egress working. +- **Trust dialog refused / non-interactive attach**: capture works, Remote + Control inbound does not; stated, retryable. +- **Squatter on the fixed port**: revisited and accepted; the CA key is + 0600 in the state root, inside the same-user boundary LLP 0114 already + conceded. Mutual auth deliberately not added. +- **Shutdown with live tunnels**: hijacked CONNECT sockets are destroyed + by `stop()` itself; `server.close()` no longer knows about them. +- **Key/cert interleave from concurrent mints**: detected by the explicit + key-matches-certificate check; regenerated instead of failing every + handshake for ten years. +- **Upstream Bun behaviour change** (LLP 0236's canary caveat): first + symptom is inbound Remote Control silently failing; the durable fix is + upstream (claude-code#75050) and out of this design's control. + +## 7. What the tests prove {#tests} + +Traditional (root `test/`): `test/core/tls-x509.test.js` (constraint +encoding, including a minted leaf for another host failing the handshake +with `permitted subtree violation`, and structural read-back), +`test/core/tls-ca.test.js` (0600, reuse, key/cert mismatch regeneration, +wait/delete), `test/core/tls-darwin-trust.test.js` (probe-first +idempotence, refusal path), `test/plugins/ai-gateway-connect-front-door.test.js` +(loopback refusal, blind-tunnel fidelity, ALPN and early-byte ordering), +`test/plugins/ai-gateway-proxy-mode.test.js` and +`ai-gateway-proxy-routing.test.js` (the negatives that define the +aperture: a side-channel path is proxied faithfully and starts no +exchange; an Anthropic bearer on an unmatched path cannot reopen it; a `/` +or absent anchor records nothing; interception keyed on host and port), +`test/plugins/claude-settings-proxy-attach.test.js` (two-key write, marker +`mode`/`prev_env`, displaced-proxy backup and one-time redacted warning, +mode-migration key release), and `test/core/status-proxy-trust.test.js` +(the LLP 0237/0239 reporting). The `security` / `launchctl` seams refuse +under the test runner and are shimmed in hermetic flows, per the LLP 0181 +rule restated in LLP 0244. + +Hermetic smokes: `gateway_claude_capture`, `claude_attach_detach`, +`client_attach_idempotent` exercise the wiring in a temp `HYP_HOME`. +The written acceptance procedure in `docs/ACCEPTANCE.md` remains the +manual gate for real-daemon, real-keychain behaviour; hermetic runs must +never touch the host keychain or launchd table. From 0e7fd98b731a6dc1d7c839acdc94866895d4d005 Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:00:29 +0000 Subject: [PATCH 2/7] LLP 0248: proxy-mode capture plan, audit finds design realized, two doc gaps --- llp/0248-proxy-mode-capture.plan.md | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 llp/0248-proxy-mode-capture.plan.md diff --git a/llp/0248-proxy-mode-capture.plan.md b/llp/0248-proxy-mode-capture.plan.md new file mode 100644 index 000000000..5d35f8400 --- /dev/null +++ b/llp/0248-proxy-mode-capture.plan.md @@ -0,0 +1,101 @@ +# LLP 0248: Proxy-mode capture, implementation plan + +**Type:** plan +**Status:** Active +**Related:** LLP 0245, LLP 0231 +**Generated-by:** neutral + +> Executable plan for the `proxy-mode-capture` change set, refining design +> LLP 0245 (which covers RFC LLP 0231). The plan's headline finding is that +> the design is already realized on `master`; the tasks below close the two +> gaps the design-versus-tree audit actually found, and nothing else. + +## Audit: what is already built on `master` {#audit} + +LLP 0245 section 0 states the design is realized by `fa701a7e` (#782, the +transport, aperture, CA and attach) and `d0f7c4ad` (#792, status and trust +reporting), and this branch's merge base (`04330abb`, #794, the LLP +0242-0244 rollout) already contains both. The audit for this plan verified +every file, symbol and test the design names against that tree: + +- **Core TLS toolkit** (design section 2): `src/core/tls/x509.js` + (`generateKeyPair`, `mintCertificate`, `derToPem`, + `readNameConstraints`), `src/core/tls/ca.js` (`INTERCEPT_PROVIDER_HOSTS` + with exactly the three LLP 0238 hosts, `defaultStateRoot`, `caPaths`, + `fingerprint`, `ensureLocalCa`, `createLeafStore`, `readLocalCaInfo`, + `waitForLocalCa`, `deleteLocalCa`), `src/core/tls/darwin_trust.js` + (`CA_COMMON_NAME`, `loginKeychainPath`, `isCaTrusted`, `installCaTrust`, + `removeCaTrust`), and `src/core/daemon/launchd_env.js` (all eight + exports). All present as designed. +- **Gateway front door** (section 3): + `hypaware-core/plugins-workspace/ai-gateway/src/connect.js` + (`attachConnectFrontDoor`, `isLoopbackAddress`, `CONNECT_HOST` / + `CONNECT_PORT` symbols and readers, `openUpstream`, `parseAuthority`), + `proxy.js` (`startProxy`, `interceptsHost`, `matchUpstreamByHost`, + `shouldRecordProxyExchange`, `compileUpstreams`, `createChainedAgent`), + `source.js` (`record_prefix` merge from the preset `path_prefix`, and the + full status surface: `proxy_mode`, `ca_fingerprint`, `ca_not_after`, + `ca_cert_path`, `ca_permitted_hosts`, `proxy_mode_error`), and + `config.js` (`proxy_mode` strictly-true switch, `upstream_proxy`). All + present. +- **Claude attach** (section 4): + `hypaware-core/plugins-workspace/claude/src/settings.js` (`MODE_PROXY` / + `MODE_BASE_URL`, `PROXY_MODE_ENV_KEYS` of exactly `HTTPS_PROXY` and + `NODE_EXTRA_CA_CERTS`, marker `mode` / `prev_env`, mode-migration key + release), `index.js` (CA-existence preflight via `readLocalCaInfo`, + Darwin trust and launchd steps), the disk-driven undo in + `src/core/config/client_detach_disk.js` (`detachClientFromDisk`), and + `purgeProxyTrustResidue` in `src/core/commands/clients.js`. All present. +- **Status surface** (section 5): `ProxyTrustReport` in + `src/core/daemon/types.d.ts`, populated by `src/core/daemon/status.js` + and rendered by `src/core/commands/status.js`. Present. +- **Tests** (section 7): every named traditional test exists and passes + (120 tests across `test/core/tls-x509.test.js`, `tls-ca.test.js`, + `tls-darwin-trust.test.js`, `status-proxy-trust.test.js`, + `test/plugins/ai-gateway-connect-front-door.test.js`, + `ai-gateway-proxy-mode.test.js`, `ai-gateway-proxy-routing.test.js`, + `claude-settings-proxy-attach.test.js`, plus the rollout-era + `attach-proxy-migration.test.js` and `gateway-proxy-enable.test.js`). + The `gateway_claude_capture` hermetic smoke runs green; + `claude_attach_detach` and `client_attach_idempotent` flows exist. +- **Annotations**: sixteen source files already carry `@ref LLP 0232` + through `@ref LLP 0239` annotations at the seams the design describes. + +**No task below rewrites any of that.** A worker who believes a section 2-5 +mechanism is missing should re-read this audit and the tree before writing +code; the correct output for already-built scope is no diff. + +## Gaps the audit found {#gaps} + +Two, both documentation: + +1. **The manual acceptance gate the design cites does not exist.** Design + section 7 says "the written acceptance procedure in `docs/ACCEPTANCE.md` + remains the manual gate for real-daemon, real-keychain behaviour", but + `docs/ACCEPTANCE.md` contains only `codex_desktop_capture` and + `openclaw_capture`. Nothing written covers proxy-mode attach on a real + Mac: the keychain trust dialog, the launchd environment, Remote Control + surviving attach (the whole point of RFC 0231), or purge. Hermetic + smokes shim the `security` / `launchctl` seams (LLP 0181 rule restated + in LLP 0244), so only a written manual procedure can gate that + behaviour. +2. **The request has no forward-ref to its design of record.** LLP 0231's + header carries `Related:` and `Spawns:` but nothing pointing at LLP + 0245, so the coverage edge is discoverable only from the design side. + The corpus convention is to append a forward-ref to the covered doc; + forward-refs are among the trivial editorial edits an Accepted doc + still admits. + +## Out of scope {#out-of-scope} + +Who turns proxy mode on (fresh-install composition, existing-install +migration) is LLP 0242's problem, was designed in LLP 0246 per the design's +section 0, and in fact already landed on `master` as #794; it belongs to +its own change set either way. Codex stays base-URL (RFC 0231). The +`upstream_proxy` field-testing question and claude-code#75050 are recorded +open items, not tasks. + +## Tasks + +- id: T1 branch: task/proxy-mode-capture/T1 deps: [] complexity: 3 -- Write the missing manual acceptance procedure `claude_proxy_capture` in docs/ACCEPTANCE.md, in the same shape as the existing `codex_desktop_capture` and `openclaw_capture` entries (what it proves, what it does not prove, prerequisites, exact commands, pass condition, an "If it fails" section). Opt-in/manual, needs a real Mac. It must cover, from LLP 0245 sections 1, 4 and 6: real `hyp daemon install`/start, `hyp attach claude` in proxy mode writing only HTTPS_PROXY and NODE_EXTRA_CA_CERTS into ~/.claude/settings.json, the macOS keychain trust dialog naming all INTERCEPT_PROVIDER_HOSTS, NODE_USE_SYSTEM_CA visible via `launchctl getenv` with the fully-quit-and-reopen-terminal caveat (LLP 0239), a Claude Code session producing rows in ai_gateway_messages attributable via entrypoint while Remote Control inbound still works (the RFC's whole point), `hyp status` reporting the ProxyTrustReport fields, detach restoring env keys while the CA and keychain trust survive (LLP 0238), and `hyp detach claude --purge` plus `hyp daemon uninstall` removing CA, trust and launchd residue. Also add `claude_proxy_capture` to the written-procedures list in CLAUDE.md's Smoke Test Model section (currently lists only codex_desktop_capture). Do NOT touch any code: the mechanisms are all built and green on master (see #audit). Verification is `npm test` still green and the two docs reading consistently; no new automated tests. Prose rules apply: no em dashes anywhere. +- id: T2 branch: task/proxy-mode-capture/T2 deps: [] complexity: 1 -- Append the design-of-record forward-ref to the request: in llp/0231-proxy-mode-capture.rfc.md's metadata header add a line `**Design:** LLP 0245` directly after the `**Spawns:**` line. This is a trivial editorial forward-ref, explicitly permitted on an Accepted doc; change nothing else in the file (no body edits, no status change). Check llp/0232 through llp/0239 headers and confirm they need no equivalent edit (0232, 0233 and 0235 already carry Extended-by/Superseded-by lines; the design cites all of them from its own side, and per-decision back-refs from code already exist). Verification: `npm test` untouched and green, and a grep shows exactly one new line added under llp/. From d6dda03ebd60eb1cb54005bcc1264215466329a0 Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:08:53 +0000 Subject: [PATCH 3/7] LLP 0231: forward-ref the design of record (LLP 0245) 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 --- llp/0231-proxy-mode-capture.rfc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/llp/0231-proxy-mode-capture.rfc.md b/llp/0231-proxy-mode-capture.rfc.md index 8fe827060..17e6ca5d9 100644 --- a/llp/0231-proxy-mode-capture.rfc.md +++ b/llp/0231-proxy-mode-capture.rfc.md @@ -7,6 +7,7 @@ **Date:** 2026-08-14 **Related:** LLP 0016, LLP 0044, LLP 0045, LLP 0049, LLP 0066, LLP 0086, LLP 0114, LLP 0116, LLP 0176, LLP 0192, LLP 0206 **Spawns:** LLP 0232, LLP 0233, LLP 0234, LLP 0235 +**Design:** LLP 0245 > Claude Code disables **Remote Control** whenever `ANTHROPIC_BASE_URL` points > anywhere other than `api.anthropic.com`. Attach repoints exactly that key, so From f54331422ba4ae303e5c1298a7eb09159dbd5e35 Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:14:35 +0000 Subject: [PATCH 4/7] Write claude_proxy_capture manual acceptance procedure 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 --- AGENTS.md | 8 ++ docs/ACCEPTANCE.md | 290 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d87b9819c..a4c44682f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,14 @@ Written acceptance procedures: Proves Desktop traffic reaches `ai_gateway_messages` by both the live gateway route and the `~/.codex/sessions` backfill route, and is attributable via `entrypoint`. See `docs/ACCEPTANCE.md`. +- `claude_proxy_capture`: opt-in/manual, needs a real Mac. Proves proxy-mode + attach against a real installed daemon and login keychain: attach writes + only `HTTPS_PROXY` and `NODE_EXTRA_CA_CERTS`, the keychain trust dialog + names every intercepted provider host, `NODE_USE_SYSTEM_CA` reaches the + launchd environment, a Claude Code session lands rows in + `ai_gateway_messages` while Remote Control's inbound channel keeps + working, and detach, purge and uninstall each remove exactly their own + residue. See `docs/ACCEPTANCE.md`. Good acceptance smoke candidates (no written procedure yet): diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 8aa66678f..666ecd5bc 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -510,6 +510,296 @@ procedure checks, R11 in particular), [LLP 0172](../llp/0172-openclaw-two-lane-c --- +## `claude_proxy_capture` + +**What it proves:** that proxy-mode attach ([LLP 0231](../llp/0231-proxy-mode-capture.rfc.md), +design [LLP 0245](../llp/0245-proxy-mode-capture.design.md)) works end to end +against a real, installed daemon and a real macOS keychain: attach writes +only the two proxy-mode environment keys into `~/.claude/settings.json`, the +keychain trust dialog names every intercepted provider host, the launchd +environment carries `NODE_USE_SYSTEM_CA`, a live Claude Code session lands +rows in `ai_gateway_messages` while Remote Control's inbound channel keeps +working (the reason this design exists at all), `hyp status` reports the +trust state, and detach, purge and uninstall each remove exactly what they +are supposed to and nothing more. + +**What it does not prove:** anything about the Codex adapter (Codex stays on +base-URL attach and is out of this design's scope), anything about +`upstream_proxy` field-testing against a corporate egress proxy (an open +item, not covered here), the exact Bun trust-store behaviour underlying +[LLP 0236](../llp/0236-claude-code-split-trust-stores.research.md) (its +canary caveat is a standing risk this procedure can only observe the +symptom of, not the cause), or fleet forwarding. It also proves nothing +about base-URL attach, which is the default and is already exercised by +`hyp smoke gateway_claude_capture` and `hyp smoke client_attach_idempotent`. + +**Requires:** + +- A real Mac (the keychain trust dialog and `launchctl` env delivery are + Darwin-only; see [LLP 0237](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md) + #darwin-only). Do not attempt this on Linux; the design states Remote + Control inbound is unsupported there under proxy mode and there is no + dialog or launchd table to check. +- Claude Code installed and signed in, with Remote Control reachable from a + second device (the mobile app or web) so step 4 has something to pair + against. +- HypAware installed from the package under test, with no prior HypAware CA + trusted in the login keychain (a machine that has run this procedure + before will not see the dialog again in step 2; that is expected, not a + failure, per [LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) + #ca-survives-detach. Run `hyp detach claude --purge` first if you need a + clean first-trust observation). + +**Related:** [LLP 0231](../llp/0231-proxy-mode-capture.rfc.md) (the request), +[LLP 0245](../llp/0245-proxy-mode-capture.design.md) (the design this +procedure gates, sections 1, 4 and 6), +[LLP 0237](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md) +(keychain trust), [LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) +(CA and trust survive detach), [LLP 0239](../llp/0239-node-use-system-ca-via-launchd.decision.md) +(the launchd delivery and its terminal caveat). + +### Steps + +1. Turn proxy mode on and do a **real** daemon install and start (not + `hyp daemon foreground`; this procedure exists specifically to exercise + the real launchd service and the real keychain, which a foreground dev + run never touches): + + ```sh + jq '.plugins |= map(if .name == "@hypaware/ai-gateway" + then .config.proxy_mode = true else . end)' \ + ~/.hyp/hypaware-config.json > /tmp/hypaware-config.json \ + && mv /tmp/hypaware-config.json ~/.hyp/hypaware-config.json + hyp daemon install + hyp daemon start + hyp status + ``` + + Pass condition: `hyp status` shows the daemon running and, under + `sources:`, the gateway source `[running]`. If the daemon is not running, + nothing below can work: the CA-existence preflight in step 2 refuses on + purpose rather than attaching against a dead gateway + ([LLP 0245#claude-attach](../llp/0245-proxy-mode-capture.design.md#claude-attach) + #proxy-attach-preflight). + +2. Attach Claude Code and watch for the keychain dialog: + + ```sh + hyp attach claude + ``` + + A macOS password/consent dialog should appear during this command on a + machine with no prior HypAware trust, naming **HypAware Local CA** as the + certificate and offering to add it as a trusted root. Read the dialog + text: it must name (or the surrounding attach output must state) all + three hosts the CA is constrained to, `api.anthropic.com`, + `api.openai.com`, and `chatgpt.com`, not only the one this attach is for + ([LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) + #full-provider-constraints: one certificate, one dialog, every provider, + and the grant must be informed about all of them). Approve it. + + Confirm the settings write: + + ```sh + cat ~/.claude/settings.json | jq '.env' + ``` + + Pass condition: `env` carries exactly `HTTPS_PROXY` and + `NODE_EXTRA_CA_CERTS` and nothing else HypAware manages for proxy mode. + In particular `ANTHROPIC_BASE_URL`, `ENABLE_TOOL_SEARCH`, and + `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL` must be **absent** (or, if this + machine previously ran base-URL attach, must have been released by the + mode migration, not merely left stale) + ([LLP 0232](../llp/0232-claude-attaches-by-proxy.decision.md) + #mode-migration). `NODE_EXTRA_CA_CERTS` must point at + `~/.hyp/hypaware/tls/ca.crt` (or `$HYP_HOME/hypaware/tls/ca.crt`). + +3. Confirm the launchd environment, respecting the terminal caveat: only + processes launchd starts **after** the `setenv` see the variable, and a + terminal app is single-process, so a window that predates step 2 will not + see it even though it is genuinely set + ([LLP 0239](../llp/0239-node-use-system-ca-via-launchd.decision.md) + #terminals-predating-attach). Read attach's own final output line first: + it states this caveat explicitly. Then, in the **same terminal window** + you ran step 2 in (do not open a new window yet): + + ```sh + launchctl getenv NODE_USE_SYSTEM_CA + ``` + + Pass condition: `1`. This reads launchd's table directly, so it passes + regardless of the terminal caveat above; the caveat only matters for + whether a *shell's own* `$NODE_USE_SYSTEM_CA` is set, which this command + does not test. Now **fully quit and reopen your terminal application** + (closing the window is not enough; the app itself must relaunch) and + confirm the shell environment a freshly-launched Claude Code process + would actually inherit: + + ```sh + echo $NODE_USE_SYSTEM_CA + ``` + + Pass condition: `1`, only after the full quit-and-reopen. A pre-existing + window still printing empty here is the expected, documented gap, not a + failure; do not file it. + +4. Hold a Claude Code session and confirm both halves of the RFC's point: + capture works, and Remote Control still does too. + + ```sh + hyp query sql "select count(*) from ai_gateway_messages" + ``` + + Note the count, then **fully quit and reopen Claude Code** (it reads + `settings.json` at launch) and hold a short conversation: send at least + one message and let it answer. + + ```sh + TODAY=$(date -u +%Y-%m-%d) + hyp query sql " + select entrypoint, client_name, count(*) n, max(message_created_at) last_seen + from ai_gateway_messages + where date >= '$TODAY' + group by 1, 2 + order by last_seen desc" + ``` + + Pass condition: a new row whose `client_name` names Claude, `entrypoint` + is populated, and `last_seen` is inside the last few minutes, with the + total count grown against the value noted above. + + Now, without quitting Claude Code again, pair the second device to + Remote Control (or, if already paired from before this procedure, send + one instruction from it) and confirm it drives this session. Pass + condition: Remote Control connects and controls the session normally, + with no "Remote Control is only available when using Claude via + api.anthropic.com" refusal. This is the one observation the whole design + exists to produce: base-URL attach would have failed this step. + +5. Confirm `hyp status` reports the trust state: + + ```sh + hyp status + hyp status --json | jq '.proxy_trust' + ``` + + Pass condition: the text output has a `proxy trust:` block naming the CA + fingerprint, `login keychain: trusted`, and `launchd env: set`; the JSON + carries the same three facts as `ca_fingerprint`, `ca_trusted: true`, and + `launchd_env_set: true` + ([LLP 0245#status](../llp/0245-proxy-mode-capture.design.md#status), + `ProxyTrustReport`). + +6. Detach and confirm the CA and its trust survive, per + [LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) + #ca-survives-detach: + + ```sh + hyp detach claude + cat ~/.claude/settings.json | jq '.env' + launchctl getenv NODE_USE_SYSTEM_CA + hyp status --json | jq '.proxy_trust' + security find-certificate -c "HypAware Local CA" ~/Library/Keychains/login.keychain-db + ``` + + Pass condition: `env` no longer carries `HTTPS_PROXY` or + `NODE_EXTRA_CA_CERTS` (restored from the marker's `prev_env`, or removed + if none existed); `launchctl getenv` now prints nothing (the launchd + environment and its LaunchAgent are removed, since that half is + recreatable for free and follows the attach per + [LLP 0239#launchctl-setenv](../llp/0239-node-use-system-ca-via-launchd.decision.md#launchctl-setenv)); + but `hyp status --json`'s `.proxy_trust.ca_trusted` is still `true` and + the `security find-certificate` call still finds the CA. Re-attaching + after this point should show no keychain dialog, proving the trust grant + really did survive: + + ```sh + hyp attach claude + ``` + +7. Purge, then uninstall, and confirm every artifact this design created is + gone: + + ```sh + hyp detach claude --purge + security find-certificate -c "HypAware Local CA" ~/Library/Keychains/login.keychain-db + ls ~/.hyp/hypaware/tls/ 2>&1 || echo 'tls dir gone' + ``` + + Pass condition: `security find-certificate` now fails to find the + certificate, and the `tls/` directory under the state root is gone or + empty of key material. + + ```sh + hyp daemon uninstall + ls ~/Library/LaunchAgents/ | grep hyperparam || echo 'no hypaware launchd residue' + ``` + + Pass condition: no HypAware daemon or `node-system-ca` LaunchAgent plist + remains under `~/Library/LaunchAgents`, and `hyp status` (if you still + have the binary) reports no running daemon. + + Then, if this is your working machine, re-install and re-attach so you do + not silently leave it uncaptured: + + ```sh + hyp daemon install + hyp daemon start + hyp attach claude + ``` + +### If it fails + +- `hyp attach claude` refuses outright before any dialog appears: check that + step 1's `proxy_mode: true` edit actually landed and that `hyp daemon + restart` (or a fresh `hyp daemon start`) ran after it; the CA-existence + preflight refuses on purpose when the gateway is not actually running in + proxy mode, and this is correct behaviour, not a bug to route around. +- No dialog in step 2 on a machine that has never run this procedure: + confirm no earlier HypAware install already trusted a CA under this same + login keychain (`security find-certificate -c "HypAware Local CA" ...`); + if one exists, `hyp detach claude --purge` first for a clean observation. + A refused or dismissed dialog is not a failure on its own: attach still + completes and states plainly that Remote Control inbound will not work + ([LLP 0237#attach-anyway-on-refusal](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md#attach-anyway-on-refusal)); + re-running `hyp attach claude` retries the dialog. +- Step 3's `echo $NODE_USE_SYSTEM_CA` is empty even after a full quit and + reopen of the terminal application: confirm you quit the *application*, + not just the window (window managers and some terminal emulators keep the + process alive across "close window"); confirm `launchctl getenv` (which + does not have this caveat) shows `1` first to isolate whether the launchd + side or the terminal side is the gap. +- Step 4 shows new rows but Remote Control still refuses: check + `~/.claude/settings.json` for a lingering `ANTHROPIC_BASE_URL` (mode + migration should have released it; a stale value here means migration + regressed), then check `hyp status --json | jq '.proxy_trust.ca_trusted'` + for `false` (an untrusted CA is exactly the state that leaves capture + working and Remote Control inbound broken, per + [LLP 0245#failure-modes](../llp/0245-proxy-mode-capture.design.md#failure-modes)). + If both are clean, this may be the LLP 0236 canary: an upstream Bun + behaviour change silently breaking Remote Control's trust-store lookup is + out of this design's control; file it against claude-code#75050 rather + than against this procedure. +- Step 4 shows no new rows at all: confirm `proxy_mode_error` is absent from + `hyp status --json | jq '.sources'` (a CA-preparation failure at boot + degrades the gateway to reverse-proxy-only, so an attached proxy-mode + client's traffic never gets terminated); confirm you fully quit and + reopened Claude Code so it re-read `settings.json`. +- Step 6 finds the CA no longer trusted, or the `tls/` directory gone, right + after a plain `hyp detach claude` (no `--purge`): this is the LLP 0238 + regression to watch for specifically, since keeping the CA and trust + across detach is the entire point of that decision; do not treat "detach + cleaned up everything" as a pass here the way it would for the settings + keys. +- Step 7 still finds the certificate after `hyp detach claude --purge`: + confirm `purgeProxyTrustResidue` actually ran (its output names the + keychain and CA removal explicitly); a permission prompt cancelled + mid-purge can leave the keychain entry behind while still removing the CA + files on disk, which is worth filing as its own gap rather than assuming + the whole purge silently no-op'd. + +--- + ## Other candidates `CLAUDE.md` lists further acceptance candidates that have no written From bf28cf122bd0c9c9117d81df71d77408010e99dc Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 22:00:07 +0000 Subject: [PATCH 5/7] Review round 1: correct the CA filename, the LLP 0246 renumber, and the 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. --- docs/ACCEPTANCE.md | 27 ++++++++++++++++++++------- llp/0245-proxy-mode-capture.design.md | 21 ++++++++++++++++----- llp/0248-proxy-mode-capture.plan.md | 5 +++-- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 666ecd5bc..68b006a8e 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -612,7 +612,8 @@ procedure gates, sections 1, 4 and 6), mode migration, not merely left stale) ([LLP 0232](../llp/0232-claude-attaches-by-proxy.decision.md) #mode-migration). `NODE_EXTRA_CA_CERTS` must point at - `~/.hyp/hypaware/tls/ca.crt` (or `$HYP_HOME/hypaware/tls/ca.crt`). + `~/.hyp/hypaware/tls/ca-cert.pem` (or + `$HYP_HOME/hypaware/tls/ca-cert.pem`). 3. Confirm the launchd environment, respecting the terminal caveat: only processes launchd starts **after** the `setenv` see the variable, and a @@ -684,7 +685,8 @@ procedure gates, sections 1, 4 and 6), ``` Pass condition: the text output has a `proxy trust:` block naming the CA - fingerprint, `login keychain: trusted`, and `launchd env: set`; the JSON + fingerprint, `login keychain: trusted`, and + `launchd env: NODE_USE_SYSTEM_CA=1 set`; the JSON carries the same three facts as `ca_fingerprint`, `ca_trusted: true`, and `launchd_env_set: true` ([LLP 0245#status](../llp/0245-proxy-mode-capture.design.md#status), @@ -776,15 +778,26 @@ procedure gates, sections 1, 4 and 6), for `false` (an untrusted CA is exactly the state that leaves capture working and Remote Control inbound broken, per [LLP 0245#failure-modes](../llp/0245-proxy-mode-capture.design.md#failure-modes)). - If both are clean, this may be the LLP 0236 canary: an upstream Bun + If both are clean, and the refusal names the *account* rather than the base + URL ("Remote Control environments are not available for your account"), it + is the absolute-form regression, not the trust story: the bridge client + sends absolute-form requests straight at the proxy port and the gateway + must route them by the host the request line names + ([LLP 0246](../llp/0246-remote-control-absolute-form-requests.issue.md), + [LLP 0247](../llp/0247-absolute-form-third-front-door.decision.md)). + Otherwise this may be the LLP 0236 canary: an upstream Bun behaviour change silently breaking Remote Control's trust-store lookup is out of this design's control; file it against claude-code#75050 rather than against this procedure. - Step 4 shows no new rows at all: confirm `proxy_mode_error` is absent from - `hyp status --json | jq '.sources'` (a CA-preparation failure at boot - degrades the gateway to reverse-proxy-only, so an attached proxy-mode - client's traffic never gets terminated); confirm you fully quit and - reopened Claude Code so it re-read `settings.json`. + the gateway source's details in the daemon status file, which is where the + source publishes it (`hyp daemon status --json | jq '.sources[] | + select(.plugin == "@hypaware/ai-gateway") | .details'`; note `hyp status + --json`'s own `.sources` carries only name, plugin and state, not the + details block). A CA-preparation failure at boot degrades the gateway to + reverse-proxy-only, so an attached proxy-mode client's traffic never gets + terminated. Then confirm you fully quit and reopened Claude Code so it + re-read `settings.json`. - Step 6 finds the CA no longer trusted, or the `tls/` directory gone, right after a plain `hyp detach claude` (no `--purge`): this is the LLP 0238 regression to watch for specifically, since keeping the CA and trust diff --git a/llp/0245-proxy-mode-capture.design.md b/llp/0245-proxy-mode-capture.design.md index c84a7957a..2a239e78f 100644 --- a/llp/0245-proxy-mode-capture.design.md +++ b/llp/0245-proxy-mode-capture.design.md @@ -5,8 +5,8 @@ **Systems:** Gateway, Sources, Config, Plugins, Privacy, Core, Daemon **Generated-by:** neutral **Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0234, LLP 0235, LLP 0236, -LLP 0237, LLP 0238, LLP 0239, LLP 0044, LLP 0045, LLP 0114, LLP 0192, -LLP 0206 +LLP 0237, LLP 0238, LLP 0239, LLP 0246, LLP 0247, LLP 0044, LLP 0045, +LLP 0114, LLP 0192, LLP 0206 > Technical design for the proxy-mode capture stack the accepted RFC > LLP 0231 asked for: Claude Code routed through the gateway with @@ -28,7 +28,7 @@ is the implementation design that binds those decisions to the tree. The design is realized on `master` by three commits: `fa701a7e` (#782, the transport, aperture, CA and attach), `d0f7c4ad` (#792, the status and trust -reporting surface), and the rollout work covered separately by LLP 0246. +reporting surface), and the rollout work covered separately by LLP 0251. File paths and function names below are verified against that tree; the tests named in section 7 exist and gate it. What this document adds to the corpus is the request-level design of record: the one place the whole @@ -37,12 +37,12 @@ above. This change set deliberately excludes who *turns proxy mode on*. Fresh install composition and the existing-install migration are LLP 0242's -problem and are designed in LLP 0246, which depends on this change set. +problem and are designed in LLP 0251, which depends on this change set. ## 1. Data flow, end to end {#data-flow} 1. Attach writes `env.HTTPS_PROXY = http://127.0.0.1:` and - `env.NODE_EXTRA_CA_CERTS = /tls/ca.crt` into + `env.NODE_EXTRA_CA_CERTS = /tls/ca-cert.pem` into `~/.claude/settings.json`. The base URL is untouched, so Claude Code's first-party predicate stays true on its own terms (LLP 0232). 2. Claude Code opens `CONNECT api.anthropic.com:443` against the gateway's @@ -174,6 +174,17 @@ the gateway kept reverse-proxying. switch: `proxy_mode` is on only when the config field is literally `true` (LLP 0233 #proxy-mode-is-explicit), and compiles `upstream_proxy`. +One front door post-dates this design's audit and is described here only so +the map is complete: Claude Code's Remote Control bridge sends absolute-form +plaintext requests straight at the proxy port rather than tunnelling them, +and until LLP 0246/0247 (landed as #797) the gateway routed them by pathname +alone and answered a local 404 that the client misread as an account +limitation. Absolute-form is now a third front door, routed by the host the +request line names through `matchUpstreamByHost` and recorded under the same +per-path anchor. Nothing above changes; it is the reason the acceptance +procedure's Remote Control step passes on current `master`, so a reader +diagnosing that step needs LLP 0247 as well as LLP 0237 and LLP 0239. + ## 4. Claude attach: `@hypaware/claude` {#claude-attach} `hypaware-core/plugins-workspace/claude/src/settings.js`: diff --git a/llp/0248-proxy-mode-capture.plan.md b/llp/0248-proxy-mode-capture.plan.md index 5d35f8400..490698c63 100644 --- a/llp/0248-proxy-mode-capture.plan.md +++ b/llp/0248-proxy-mode-capture.plan.md @@ -55,7 +55,8 @@ every file, symbol and test the design names against that tree: `test/plugins/ai-gateway-connect-front-door.test.js`, `ai-gateway-proxy-mode.test.js`, `ai-gateway-proxy-routing.test.js`, `claude-settings-proxy-attach.test.js`, plus the rollout-era - `attach-proxy-migration.test.js` and `gateway-proxy-enable.test.js`). + `test/core/attach-proxy-migration.test.js` and + `test/core/gateway-proxy-enable.test.js`). The `gateway_claude_capture` hermetic smoke runs green; `claude_attach_detach` and `client_attach_idempotent` flows exist. - **Annotations**: sixteen source files already carry `@ref LLP 0232` @@ -89,7 +90,7 @@ Two, both documentation: ## Out of scope {#out-of-scope} Who turns proxy mode on (fresh-install composition, existing-install -migration) is LLP 0242's problem, was designed in LLP 0246 per the design's +migration) is LLP 0242's problem, was designed in LLP 0251 per the design's section 0, and in fact already landed on `master` as #794; it belongs to its own change set either way. Codex stays base-URL (RFC 0231). The `upstream_proxy` field-testing question and claude-code#75050 are recorded From c7318a587eb3f8c50d5b9e3be0dd7a0abdd5113e Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 22:11:06 +0000 Subject: [PATCH 6/7] Review round 1 (second pass): the CA preflight selects the mode, it does 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. --- docs/ACCEPTANCE.md | 30 +++++++++++++------ llp/0245-proxy-mode-capture.design.md | 43 +++++++++++++++++++-------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 68b006a8e..2ae4bc8b7 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -573,14 +573,20 @@ procedure gates, sections 1, 4 and 6), hyp daemon install hyp daemon start hyp status + ls ~/.hyp/hypaware/tls/ca-cert.pem ``` Pass condition: `hyp status` shows the daemon running and, under - `sources:`, the gateway source `[running]`. If the daemon is not running, - nothing below can work: the CA-existence preflight in step 2 refuses on - purpose rather than attaching against a dead gateway + `sources:`, the gateway source `[started]`, and the CA file exists. Check + all three: the `jq` edit is a silent no-op if the config has no + `@hypaware/ai-gateway` entry, and on a centrally managed host a local + `proxy_mode` write loses the LLP 0031 layer merge outright. The CA is the + only proof that proxy mode is actually being *served*, and step 2 reads + exactly that file to decide which mode to attach in ([LLP 0245#claude-attach](../llp/0245-proxy-mode-capture.design.md#claude-attach) - #proxy-attach-preflight). + #proxy-attach-preflight). With no CA, `hyp attach claude` does not refuse, + it quietly attaches in base-URL mode instead, and every step below would + then be measuring the wrong feature. 2. Attach Claude Code and watch for the keychain dialog: @@ -752,11 +758,17 @@ procedure gates, sections 1, 4 and 6), ### If it fails -- `hyp attach claude` refuses outright before any dialog appears: check that - step 1's `proxy_mode: true` edit actually landed and that `hyp daemon - restart` (or a fresh `hyp daemon start`) ran after it; the CA-existence - preflight refuses on purpose when the gateway is not actually running in - proxy mode, and this is correct behaviour, not a bug to route around. +- `hyp attach claude` reports a base-URL attach, or writes + `ANTHROPIC_BASE_URL` instead of the two proxy keys, and no dialog appears: + the gateway is not serving proxy mode, so there is no CA and attach fell + back to base-URL rather than refusing. Check that step 1's + `proxy_mode: true` edit actually landed and that `hyp daemon restart` (or a + fresh `hyp daemon start`) ran after it, then re-check + `~/.hyp/hypaware/tls/ca-cert.pem`. Do not treat the silent fallback as a + pass: it is the whole reason step 1 checks for the CA file. The one hard + refusal on this path (`CA_MISSING`) fires only when proxy mode was already + selected and the certificate became unreadable between the probe and the + write. - No dialog in step 2 on a machine that has never run this procedure: confirm no earlier HypAware install already trusted a CA under this same login keychain (`security find-certificate -c "HypAware Local CA" ...`); diff --git a/llp/0245-proxy-mode-capture.design.md b/llp/0245-proxy-mode-capture.design.md index 2a239e78f..138a0a520 100644 --- a/llp/0245-proxy-mode-capture.design.md +++ b/llp/0245-proxy-mode-capture.design.md @@ -150,9 +150,12 @@ The CA lives in core, not in the gateway plugin, because `hyp detach` and `matchUpstreamByHost(upstreams, host, port)`: the intercept set is derived from the routing table and keyed on host **and** port; nothing configures it separately (LLP 0234). -- `shouldRecordProxyExchange(upstream, pathname)`: the recording anchor. A - compiled `recordPrefix` of `/` or absent records nothing; failing closed - is the default. The routing matcher is deliberately not reused: the +- `shouldRecordProxyExchange(upstream, pathname)`: the recording anchor is + `recordPrefix ?? prefix`, and an anchor of `/` or empty records nothing; + failing closed is the default. Note the fallback: an upstream with no + `record_prefix` records under its routing prefix, so section 3's + `source.js` merge is what keeps a routing `path_prefix` of `/` from + reading as record-everything. The routing matcher is deliberately not reused: the Anthropic route matcher accepts an `sk-ant-` bearer alone, which under a proxy is true of every request to the host and measurably reopened the aperture (LLP 0234 #recording-is-opt-in-per-path). @@ -210,12 +213,20 @@ diagnosing that step needs LLP 0247 as well as LLP 0237 and LLP 0239. `hypaware-core/plugins-workspace/claude/src/index.js`: -- The proxy-attach preflight refuses (`markActionRefused`) unless - `readLocalCaInfo()` finds a CA on disk: the CA's existence proves the - gateway is actually serving the mode, and a proxy attach against a dead - gateway would break all of Claude Code's HTTPS (LLP 0232 - #proxy-attach-preflight). Mode is read from what the daemon is doing, - never from what config asks for. +- The proxy-attach preflight is `readLocalCaInfo()`, and what it decides is + the *mode*: a CA on disk means `MODE_PROXY`, no CA means the attach falls + back to base-URL mode. The CA's existence proves the gateway is actually + serving the mode, and a proxy attach against a dead gateway would break + all of Claude Code's HTTPS (LLP 0232 #proxy-attach-preflight). Mode is + read from what the daemon is doing, never from what config asks for. + Recorded precisely because LLP 0232 states this twice and the two + statements do not say the same thing: "a missing CA is a refusal + (`markActionRefused`), not a warning" reads absolutely, while the next + paragraph settles "proxy mode when a CA exists and base-URL mode + otherwise". The tree implements the second. The only `markActionRefused` + on this path is `CA_MISSING` in `settings.js`, which fires when proxy + mode was already selected and the certificate has since become + unreadable, i.e. the race between the probe and the write. - On Darwin the attach then runs the trust and launchd steps: keychain probe/install (dialog names all permitted hosts, per LLP 0238), and `installLaunchdEnv()`. A refused dialog degrades politely: attach @@ -252,7 +263,14 @@ readable without grepping a boot log (LLP 0233). ## 6. Failure modes {#failure-modes} - **Dead gateway behind a proxy attach**: the worst mode the feature can - have; prevented by the CA-existence preflight refusal (never a warning). + have. The CA-existence preflight covers the case it was designed for, a + machine that never ran proxy mode: no CA, so attach writes a base-URL + attach instead. It does not cover a machine that ran proxy mode and then + stopped the daemon: the CA outlives the process (LLP 0238), and when a + `listen` is configured `hyp attach` resolves the endpoint from config + rather than from a live bind, so `HTTPS_PROXY` can still be written at a + dead port. Residual, not closed; the acceptance procedure's step 1 + therefore proves the daemon is up before attaching. - **CA preparation fails at boot**: gateway still starts and still reverse proxies; `proxy_mode_error` reported; degraded blind-tunnel CONNECT keeps an already-attached client's egress working. @@ -282,8 +300,9 @@ idempotence, refusal path), `test/plugins/ai-gateway-connect-front-door.test.js` `test/plugins/ai-gateway-proxy-mode.test.js` and `ai-gateway-proxy-routing.test.js` (the negatives that define the aperture: a side-channel path is proxied faithfully and starts no -exchange; an Anthropic bearer on an unmatched path cannot reopen it; a `/` -or absent anchor records nothing; interception keyed on host and port), +exchange; an Anthropic bearer on an unmatched path cannot reopen it; an +anchor of `/` or empty records nothing; interception keyed on host and +port), `test/plugins/claude-settings-proxy-attach.test.js` (two-key write, marker `mode`/`prev_env`, displaced-proxy backup and one-time redacted warning, mode-migration key release), and `test/core/status-proxy-trust.test.js` From b566a6984ed7f19a5482764c0d3c4988f1b60398 Mon Sep 17 00:00:00 2001 From: neutral Date: Tue, 18 Aug 2026 05:30:29 +0000 Subject: [PATCH 7/7] Review round 2: LLP 0251 never existed, and section 5 promised status fields hyp status drops - The rollout is LLP 0242-0244 (04330abb, #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) --- docs/ACCEPTANCE.md | 66 +++++++++++++++++++-------- llp/0231-proxy-mode-capture.rfc.md | 2 +- llp/0245-proxy-mode-capture.design.md | 63 ++++++++++++++++--------- llp/0248-proxy-mode-capture.plan.md | 9 ++-- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 68afdec42..941a94bf2 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -541,8 +541,8 @@ about base-URL attach, which is the default and is already exercised by **Requires:** - A real Mac (the keychain trust dialog and `launchctl` env delivery are - Darwin-only; see [LLP 0237](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md) - #darwin-only). Do not attempt this on Linux; the design states Remote + Darwin-only; see [LLP 0237 #darwin-only](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md#darwin-only)). + Do not attempt this on Linux; the design states Remote Control inbound is unsupported there under proxy mode and there is no dialog or launchd table to check. - Claude Code installed and signed in, with Remote Control reachable from a @@ -551,9 +551,14 @@ about base-URL attach, which is the default and is already exercised by - HypAware installed from the package under test, with no prior HypAware CA trusted in the login keychain (a machine that has run this procedure before will not see the dialog again in step 2; that is expected, not a - failure, per [LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) - #ca-survives-detach. Run `hyp detach claude --purge` first if you need a - clean first-trust observation). + failure, per + [LLP 0238 #ca-survives-detach](../llp/0238-long-lived-ca-full-provider-constraints.decision.md#ca-survives-detach)). + If you need a clean first-trust observation, run `hyp detach claude --purge` + **and then `hyp daemon restart`**, and confirm + `~/.hyp/hypaware/tls/ca-cert.pem` is back before you start: purge deletes + the CA, the running daemon holds its own copy in memory and never re-mints, + and step 2 reads that file to decide which mode to attach in. Attaching + against the gap silently produces a base-URL attach. **Related:** [LLP 0231](../llp/0231-proxy-mode-capture.rfc.md) (the request), [LLP 0245](../llp/0245-proxy-mode-capture.design.md) (the design this @@ -571,16 +576,24 @@ procedure gates, sections 1, 4 and 6), run never touches): ```sh + cp ~/.hyp/hypaware-config.json ~/.hyp/hypaware-config.json.pre-acceptance jq '.plugins |= map(if .name == "@hypaware/ai-gateway" then .config.proxy_mode = true else . end)' \ ~/.hyp/hypaware-config.json > /tmp/hypaware-config.json \ && mv /tmp/hypaware-config.json ~/.hyp/hypaware-config.json hyp daemon install hyp daemon start + hyp daemon restart # reload the config edit above hyp status ls ~/.hyp/hypaware/tls/ca-cert.pem ``` + The backup copy is how you put your machine back afterwards; nothing below + restores the original `proxy_mode` for you. The `restart` is not + redundant: a daemon that was already installed and running does not re-read + the config on `install`/`start`, so without it the CA check at the end of + this step fails for a reason that has nothing to do with the feature. + Pass condition: `hyp status` shows the daemon running and, under `sources:`, the gateway source `[started]`, and the CA file exists. Check all three: the `jq` edit is a silent no-op if the config has no @@ -588,8 +601,9 @@ procedure gates, sections 1, 4 and 6), `proxy_mode` write loses the LLP 0031 layer merge outright. The CA is the only proof that proxy mode is actually being *served*, and step 2 reads exactly that file to decide which mode to attach in - ([LLP 0245#claude-attach](../llp/0245-proxy-mode-capture.design.md#claude-attach) - #proxy-attach-preflight). With no CA, `hyp attach claude` does not refuse, + ([LLP 0245 #claude-attach](../llp/0245-proxy-mode-capture.design.md#claude-attach), + realizing [LLP 0232 #proxy-attach-preflight](../llp/0232-claude-attaches-by-proxy.decision.md#proxy-attach-preflight)). + With no CA, `hyp attach claude` does not refuse, it quietly attaches in base-URL mode instead, and every step below would then be measuring the wrong feature. @@ -605,8 +619,8 @@ procedure gates, sections 1, 4 and 6), text: it must name (or the surrounding attach output must state) all three hosts the CA is constrained to, `api.anthropic.com`, `api.openai.com`, and `chatgpt.com`, not only the one this attach is for - ([LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) - #full-provider-constraints: one certificate, one dialog, every provider, + ([LLP 0238 #full-provider-constraints](../llp/0238-long-lived-ca-full-provider-constraints.decision.md#full-provider-constraints): + one certificate, one dialog, every provider, and the grant must be informed about all of them). Approve it. Confirm the settings write: @@ -621,8 +635,8 @@ procedure gates, sections 1, 4 and 6), `_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL` must be **absent** (or, if this machine previously ran base-URL attach, must have been released by the mode migration, not merely left stale) - ([LLP 0232](../llp/0232-claude-attaches-by-proxy.decision.md) - #mode-migration). `NODE_EXTRA_CA_CERTS` must point at + ([LLP 0232 #mode-migration](../llp/0232-claude-attaches-by-proxy.decision.md#mode-migration)). + `NODE_EXTRA_CA_CERTS` must point at `~/.hyp/hypaware/tls/ca-cert.pem` (or `$HYP_HOME/hypaware/tls/ca-cert.pem`). @@ -630,8 +644,8 @@ procedure gates, sections 1, 4 and 6), processes launchd starts **after** the `setenv` see the variable, and a terminal app is single-process, so a window that predates step 2 will not see it even though it is genuinely set - ([LLP 0239](../llp/0239-node-use-system-ca-via-launchd.decision.md) - #terminals-predating-attach). Read attach's own final output line first: + ([LLP 0239 #terminals-predating-attach](../llp/0239-node-use-system-ca-via-launchd.decision.md#terminals-predating-attach)). + Read attach's own final output line first: it states this caveat explicitly. Then, in the **same terminal window** you ran step 2 in (do not open a new window yet): @@ -704,8 +718,7 @@ procedure gates, sections 1, 4 and 6), `ProxyTrustReport`). 6. Detach and confirm the CA and its trust survive, per - [LLP 0238](../llp/0238-long-lived-ca-full-provider-constraints.decision.md) - #ca-survives-detach: + [LLP 0238 #ca-survives-detach](../llp/0238-long-lived-ca-full-provider-constraints.decision.md#ca-survives-detach): ```sh hyp detach claude @@ -758,9 +771,18 @@ procedure gates, sections 1, 4 and 6), ```sh hyp daemon install hyp daemon start + ls ~/.hyp/hypaware/tls/ca-cert.pem # wait for this before attaching hyp attach claude + cp ~/.hyp/hypaware-config.json.pre-acceptance ~/.hyp/hypaware-config.json ``` + Do not skip the `ls`. The gateway mints the CA after it boots and + `hyp attach claude` does not wait for it, so an attach fired immediately + after `start` can find no CA and silently write a base-URL attach, + leaving your working machine with Remote Control broken. Restore the + config backup last, and `hyp daemon restart` after it if you want the + machine back on its original mode. + ### If it fails - `hyp attach claude` reports a base-URL attach, or writes @@ -777,10 +799,16 @@ procedure gates, sections 1, 4 and 6), - No dialog in step 2 on a machine that has never run this procedure: confirm no earlier HypAware install already trusted a CA under this same login keychain (`security find-certificate -c "HypAware Local CA" ...`); - if one exists, `hyp detach claude --purge` first for a clean observation. - A refused or dismissed dialog is not a failure on its own: attach still - completes and states plainly that Remote Control inbound will not work - ([LLP 0237#attach-anyway-on-refusal](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md#attach-anyway-on-refusal)); + if one exists, `hyp detach claude --purge` first for a clean observation, + then `hyp daemon restart` and confirm `~/.hyp/hypaware/tls/ca-cert.pem` is + back before re-attaching. Purge deletes the CA and never touches config, + and the running daemon keeps serving the copy it loaded at boot rather than + re-minting, so a `hyp attach claude` in that window finds no CA and quietly + writes a base-URL attach instead of raising the dialog again. That reads + identically to "still no dialog", so check the CA file rather than the + symptom. A refused or dismissed dialog is not a failure on its own: + attach still completes and states plainly that Remote Control inbound will not work + ([LLP 0237 #attach-anyway-on-refusal](../llp/0237-attach-trusts-ca-in-login-keychain.decision.md#attach-anyway-on-refusal)); re-running `hyp attach claude` retries the dialog. - Step 3's `echo $NODE_USE_SYSTEM_CA` is empty even after a full quit and reopen of the terminal application: confirm you quit the *application*, diff --git a/llp/0231-proxy-mode-capture.rfc.md b/llp/0231-proxy-mode-capture.rfc.md index 17e6ca5d9..21a8ca75a 100644 --- a/llp/0231-proxy-mode-capture.rfc.md +++ b/llp/0231-proxy-mode-capture.rfc.md @@ -7,7 +7,7 @@ **Date:** 2026-08-14 **Related:** LLP 0016, LLP 0044, LLP 0045, LLP 0049, LLP 0066, LLP 0086, LLP 0114, LLP 0116, LLP 0176, LLP 0192, LLP 0206 **Spawns:** LLP 0232, LLP 0233, LLP 0234, LLP 0235 -**Design:** LLP 0245 +**Designed-by:** LLP 0245, proxy-mode capture technical design > Claude Code disables **Remote Control** whenever `ANTHROPIC_BASE_URL` points > anywhere other than `api.anthropic.com`. Attach repoints exactly that key, so diff --git a/llp/0245-proxy-mode-capture.design.md b/llp/0245-proxy-mode-capture.design.md index 138a0a520..7d16d2822 100644 --- a/llp/0245-proxy-mode-capture.design.md +++ b/llp/0245-proxy-mode-capture.design.md @@ -5,8 +5,8 @@ **Systems:** Gateway, Sources, Config, Plugins, Privacy, Core, Daemon **Generated-by:** neutral **Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0234, LLP 0235, LLP 0236, -LLP 0237, LLP 0238, LLP 0239, LLP 0246, LLP 0247, LLP 0044, LLP 0045, -LLP 0114, LLP 0192, LLP 0206 +LLP 0237, LLP 0238, LLP 0239, LLP 0242, LLP 0243, LLP 0244, LLP 0246, +LLP 0247, LLP 0044, LLP 0045, LLP 0114, LLP 0192, LLP 0206 > Technical design for the proxy-mode capture stack the accepted RFC > LLP 0231 asked for: Claude Code routed through the gateway with @@ -28,7 +28,8 @@ is the implementation design that binds those decisions to the tree. The design is realized on `master` by three commits: `fa701a7e` (#782, the transport, aperture, CA and attach), `d0f7c4ad` (#792, the status and trust -reporting surface), and the rollout work covered separately by LLP 0251. +reporting surface), and `04330abb` (#794, the LLP 0242-0244 rollout that +turns proxy mode on). File paths and function names below are verified against that tree; the tests named in section 7 exist and gate it. What this document adds to the corpus is the request-level design of record: the one place the whole @@ -37,7 +38,9 @@ above. This change set deliberately excludes who *turns proxy mode on*. Fresh install composition and the existing-install migration are LLP 0242's -problem and are designed in LLP 0251, which depends on this change set. +problem, settled by LLP 0243 (the picker row composes `proxy_mode`) and +LLP 0244 (attach offers the migration), and already landed on `master`. +Their own design of record belongs to that change set, not this one. ## 1. Data flow, end to end {#data-flow} @@ -152,10 +155,13 @@ The CA lives in core, not in the gateway plugin, because `hyp detach` and configures it separately (LLP 0234). - `shouldRecordProxyExchange(upstream, pathname)`: the recording anchor is `recordPrefix ?? prefix`, and an anchor of `/` or empty records nothing; - failing closed is the default. Note the fallback: an upstream with no - `record_prefix` records under its routing prefix, so section 3's - `source.js` merge is what keeps a routing `path_prefix` of `/` from - reading as record-everything. The routing matcher is deliberately not reused: the + failing closed is the default, so a routing prefix of `/` can never read + as record-everything. Note the fallback: an upstream with no + `record_prefix` records under its routing prefix, which is why the + `source.js` merge below matters. The `hyp init` preset writes + `path_prefix = "/"`, so without the merge the anchor was `/`, the + fail-closed guard suppressed every request, and the default install + recorded nothing at all. The routing matcher is deliberately not reused: the Anthropic route matcher accepts an `sk-ant-` bearer alone, which under a proxy is true of every request to the host and measurably reopened the aperture (LLP 0234 #recording-is-opt-in-per-path). @@ -165,13 +171,15 @@ The CA lives in core, not in the gateway plugin, because `hyp detach` and `hypaware-core/plugins-workspace/ai-gateway/src/source.js` merges each adapter preset's declared `path_prefix` (and `provider`) onto the merged -upstream entry as `record_prefix`, because operator routing config wins -over presets and a routing `path_prefix` of `/` must never read as -record-everything; without this the default install recorded nothing at -all (LLP 0234). It also owns the status surface: `proxy_mode`, -`ca_fingerprint`, `ca_not_after`, `ca_cert_path`, `ca_permitted_hosts`, -intercepted hosts, and `proxy_mode_error` when CA preparation failed while -the gateway kept reverse-proxying. +upstream entry as `record_prefix`. Operator config still wins the *routing* +question, but the record anchor belongs to the adapter that registered the +preset: an operator writing `path_prefix = "/"` is saying "route everything +on this host here", not "record everything on this host", and the +fail-closed guard turns that into recording nothing at all on a default +install (LLP 0234). It also owns the gateway's own status details: +`proxy_mode`, `ca_fingerprint`, `ca_not_after`, `ca_cert_path`, +`ca_permitted_hosts`, `intercept_hosts`, and `proxy_mode_error` when CA +preparation failed while the gateway kept reverse-proxying. `hypaware-core/plugins-workspace/ai-gateway/src/config.js` reads the switch: `proxy_mode` is on only when the config field is literally `true` @@ -252,13 +260,24 @@ delete a different install's key material. ## 5. Status surface {#status} -`src/core/commands/status.js` and `src/core/daemon/types.d.ts` -(`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 in the launchd -environment (`launchctl getenv`). "The dialog was cancelled last month" is -diagnosable without re-running attach (LLP 0237), and the aperture is -readable without grepping a boot log (LLP 0233). +Two surfaces, and they carry different things. + +`hyp status` reports the trust half: `src/core/commands/status.js` renders +the `proxy trust:` block from `ProxyTrustReport` +(`src/core/daemon/types.d.ts`), which is exactly three facts: the CA +fingerprint, keychain trust state, and whether `NODE_USE_SYSTEM_CA` is live +in the launchd environment (`launchctl getenv`). Trust and launchd state are +tri-state, because "the probe could not run" is not the claim "not trusted". +"The dialog was cancelled last month" is diagnosable without re-running +attach (LLP 0237). + +The aperture half lives in the gateway source's own status details +(section 3): `proxy_mode`, `ca_not_after`, `ca_cert_path`, +`ca_permitted_hosts`, `intercept_hosts` and `proxy_mode_error`, readable +without grepping a boot log (LLP 0233). Note that `hyp status --json` maps +each source to name, plugin and state only and drops the details block, so +those fields are read from the daemon status file +(`hyp daemon status --json`), not from `hyp status`. ## 6. Failure modes {#failure-modes} diff --git a/llp/0248-proxy-mode-capture.plan.md b/llp/0248-proxy-mode-capture.plan.md index 490698c63..0d6c16f2c 100644 --- a/llp/0248-proxy-mode-capture.plan.md +++ b/llp/0248-proxy-mode-capture.plan.md @@ -90,13 +90,14 @@ Two, both documentation: ## Out of scope {#out-of-scope} Who turns proxy mode on (fresh-install composition, existing-install -migration) is LLP 0242's problem, was designed in LLP 0251 per the design's -section 0, and in fact already landed on `master` as #794; it belongs to -its own change set either way. Codex stays base-URL (RFC 0231). The +migration) is LLP 0242's problem, settled by LLP 0243 and LLP 0244 and +already landed on `master` as #794 (`04330abb`, this branch's merge base); +it belongs to its own change set either way. Codex stays base-URL +(RFC 0231). The `upstream_proxy` field-testing question and claude-code#75050 are recorded open items, not tasks. ## Tasks - id: T1 branch: task/proxy-mode-capture/T1 deps: [] complexity: 3 -- Write the missing manual acceptance procedure `claude_proxy_capture` in docs/ACCEPTANCE.md, in the same shape as the existing `codex_desktop_capture` and `openclaw_capture` entries (what it proves, what it does not prove, prerequisites, exact commands, pass condition, an "If it fails" section). Opt-in/manual, needs a real Mac. It must cover, from LLP 0245 sections 1, 4 and 6: real `hyp daemon install`/start, `hyp attach claude` in proxy mode writing only HTTPS_PROXY and NODE_EXTRA_CA_CERTS into ~/.claude/settings.json, the macOS keychain trust dialog naming all INTERCEPT_PROVIDER_HOSTS, NODE_USE_SYSTEM_CA visible via `launchctl getenv` with the fully-quit-and-reopen-terminal caveat (LLP 0239), a Claude Code session producing rows in ai_gateway_messages attributable via entrypoint while Remote Control inbound still works (the RFC's whole point), `hyp status` reporting the ProxyTrustReport fields, detach restoring env keys while the CA and keychain trust survive (LLP 0238), and `hyp detach claude --purge` plus `hyp daemon uninstall` removing CA, trust and launchd residue. Also add `claude_proxy_capture` to the written-procedures list in CLAUDE.md's Smoke Test Model section (currently lists only codex_desktop_capture). Do NOT touch any code: the mechanisms are all built and green on master (see #audit). Verification is `npm test` still green and the two docs reading consistently; no new automated tests. Prose rules apply: no em dashes anywhere. -- id: T2 branch: task/proxy-mode-capture/T2 deps: [] complexity: 1 -- Append the design-of-record forward-ref to the request: in llp/0231-proxy-mode-capture.rfc.md's metadata header add a line `**Design:** LLP 0245` directly after the `**Spawns:**` line. This is a trivial editorial forward-ref, explicitly permitted on an Accepted doc; change nothing else in the file (no body edits, no status change). Check llp/0232 through llp/0239 headers and confirm they need no equivalent edit (0232, 0233 and 0235 already carry Extended-by/Superseded-by lines; the design cites all of them from its own side, and per-decision back-refs from code already exist). Verification: `npm test` untouched and green, and a grep shows exactly one new line added under llp/. +- id: T2 branch: task/proxy-mode-capture/T2 deps: [] complexity: 1 -- Append the design-of-record forward-ref to the request: in llp/0231-proxy-mode-capture.rfc.md's metadata header add a line `**Designed-by:** LLP 0245` directly after the `**Spawns:**` line (`Designed-by` is the corpus's existing name for this forward-ref relation; see llp/0044). This is a trivial editorial forward-ref, explicitly permitted on an Accepted doc; change nothing else in the file (no body edits, no status change). Check llp/0232 through llp/0239 headers and confirm they need no equivalent edit (0232, 0233 and 0235 already carry Extended-by/Superseded-by lines; the design cites all of them from its own side, and per-decision back-refs from code already exist). Verification: `npm test` untouched and green, and a grep shows exactly one new line added under llp/.