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

@philcunliffephilcunliffe 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

testand 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
ContributorAuthor

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 listenhyp 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
ContributorAuthor

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

claimaddedcommit
0245-proxy-mode-capture.design.md (this PR)2026-08-17 20:35:29 UTC39cd8038
0245-otel-attach-replaces-proxy.rfc.md (#818)2026-08-17 21:16:34 UTC84cfbcb2

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
ContributorAuthor

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
ContributorAuthor

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
@philcunliffephilcunliffe 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
philcunliffe added a commit that referenced this pull request Aug 18, 2026
…2) (#818)
* RFC 0245: OTEL telemetry replaces proxy attach for Claude Code
The deliberation record for the change set tracked by #798.
Committed unchanged as the branch's design record; status stays Draft because
acceptance is the author's call, not a pipeline step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* LLP 0251-0256: the decisions RFC 0245 promises on acceptance
One narrow decision per settled choice, so implementation code can cite the
choice it realizes instead of restating the reasoning: the settings env block
as the injection surface, the events-first plus bodies-for-gaps split, the
spool's cap and sweep duties, ingest-time settlement in place of the flush-time
pass, the claude_telemetry_events dataset, and the session-ignore control route
on the listener.
All six are Draft. RFC 0245 gains a Spawns line naming them; nothing here flips
a status, because acceptance is the author's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* LLP 0257: spec for the claude telemetry listener source
The requirements the listener implementation and its tests answer to: what the
source registers, what its endpoint accepts, how events and body files combine
at ingest, the two datasets it writes, its status and capture-health duties,
its failure modes, and the seams its tests use. Composes decisions 0251-0256
and cites them rather than repeating why.
Draft, like the RFC that spawns it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Forward-ref LLP 0245 from the proxy-attach docs it displaces
Mechanical forward refs only, on the parts the claude client's mode switch
displaces: the RFC (0231), the proxy attach keys (0232), the CA lifecycle and
trust (0235, 0237, 0238), the launchd environment delivery (0239), and the
fresh-install and migration path (0242, 0243, 0244). Each is phrased "on
acceptance of 0245" so it stays honest while 0245 is Draft, and each says what
survives for the clients still routed through the proxy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* RFC 0245 names all six decisions it spawns
The header Spawns line already listed LLP 0251 to 0256 plus 0257, but the
prose "On acceptance" section still enumerated only five choices and left out
session-ignore transport (LLP 0256), the choice open question 4 in the same
file resolves. Name each decision next to its number so the two lists agree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Carry the 0245 back-reference in Related, not a new header field
The seven new docs each carried a Spawned-by header field that appears nowhere
else in the corpus. The corpus already has a way to say this: LLP 0168, spawned
by RFC 0167, glosses its parent inside Related ("LLP 0167 (the accepted RFC
this decision realizes)"), and the parent RFC carries the one-way Spawns line.
Fold the same gloss into Related here and drop the field. LLP 0256 keeps the
"settled in #798" fact on a Tracker line, which LLP 0245 and LLP 0257 already
use.
No information is lost: every doc already listed LLP 0245 in Related, and the
Spawns line on 0245 records that all seven stay Draft until it is accepted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* LLP 0251 renumbered to 0258: open PR #817 claimed 0251
The number was taken on origin/integration/proxy-mode-default-attach
(llp/0251-proxy-mode-default-attach.design.md, Status Active), so whichever
branch merged second would have gone red on the uniqueness check. 0258 is the
next number free across master and every remote branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The OTLP http/json listener becomes shared core machinery (LLP 0257)
A second plugin is about to host an OTLP listener of its own, so the
transport half of @hypaware/otel's receiver moves to src/core/otlp:
signal routing, json-only content-type enforcement, gzip and deflate
decoding, the per-signal partialSuccess envelopes, and bind-and-resolve.
The shared server never looks inside `data`, so payload interpretation
stays with whichever plugin hosts the listener.
Pure prefactor. @hypaware/otel keeps its banner text, its bind error
prefix, its config keys, its wire responses and its self-telemetry loop
guard, which never left the collector. The one new option, a signal
subset, defaults to all three signals, so today's listener is unchanged.
The transport contract is now pinned by test/core/otlp-json-server.test.js
rather than only by the two otel smokes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A live producer that is not the proxy records through the gateway (LLP 0252)
`ai_gateway_messages` is about to have a third producer, so the gateway
capability grows the one call such a producer needs:
`recordProjectedExchange`. It takes a finished
`AiGatewayProjectedExchange` and owns everything after it - the shared
row expansion, `part_id` identity, the schema strip, the table path -
so an OTEL-projected row cannot drift from the proxy's row for the same
content.
The write is preceded by a pre-write `part_id` dedupe seeded from both
committed partitions and the spool, restricted to the batch in hand.
That is what makes producer overlap harmless: a part the proxy or a
backfill run already stored is skipped, not appended a second time. The
spool scan is the same one backfill does and stays forbidden on the
flush path, where the rows being tested ARE the spool.
Purely additive on the capability surface, so every adapter's `^2.0.0`
requirement still resolves and the proxy recorder's path is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The claude plugin hosts a listener for Claude Code's own telemetry (LLP 0257)
`@hypaware/claude` contributes a `claude-telemetry` source through the
kernel source registry: an OTLP http/json listener on loopback, on its
own port (default 4319, config `claude.telemetry.listen_port`, `0` for
dynamic), built on the shared core server so the transport is not
copied. Only the logs and metrics routes are served; metrics are
accepted and dropped so the exporter is not left retrying a 404, and a
non-json content type is refused exactly as the otel receiver refuses
it.
Payload interpretation is claude-owned. `user_prompt` and
`assistant_response` each carry their own `message.uuid`, so they
project once, with native identity, and no settlement enricher has
anything to repair. `api_request` carries no content and no uuid: it is
the usage record for the `request_id` an assistant response names, and
is folded onto that message's `attributes.usage` in the same shape the
proxy path writes. The index that holds it spans batches, because the
exporter flushes on a timer, and evicts oldest-first at a cap.
Rows go out through the gateway's `recordProjectedExchange`, so OTEL is
a third producer of `ai_gateway_messages`, not a new table. cwd and git
identity still come from the SessionStart hook record; `query_source`
and `agent.name` carry attribution, and parent_uuid,
logical_parent_uuid, user_type and permission_mode read null by design.
The daemon's own exports are dropped by resource marker, and another
exporter that finds the port is ignored by scope rather than
half-parsed. Registration is feature-detected against the capability, so
an older gateway degrades to "no listener" instead of failing boot, and
the source is registered rather than auto-started, so a CLI activation
never binds a port.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A hermetic smoke drives the telemetry listener end to end (LLP 0257)
Content in at the HTTP endpoint, rows out of `hyp query sql`: the smoke
activates the gateway and the claude plugin in a temp HypAware home,
drives the SessionStart hook, starts the listener on a dynamic port, and
POSTs one real-shaped Claude Code batch (the three content events plus
two behavioral ones the listener does not model yet, so skipping them is
proven rather than assumed).
It then asserts the rows: native uuid identity, prompt and response
text, model, the usage the `api_request` event carried, the cwd the hook
recorded, the OTEL producer marker, and the transcript-only columns
reading null. A replayed batch adds nothing. `hyp backfill claude` over
a transcript carrying the same uuids also adds nothing, which is the
proxy-to-OTEL overlap window in miniature. A `text/plain` POST is
refused with 415. The capture spans and the batch log carry the counts
that prove the intended path ran.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The claude settings writer gains an otel mode behind a version floor (LLP 0258)
A third attach mode beside base_url and proxy: `otel` merges the LLP 0258
#env-keys telemetry block into the settings env block and writes nothing
that routes traffic, so the endpoint stays first party and Remote Control
needs no override keys. The mode reuses the whole marker machinery
unchanged (managed env, prev_env backups, atomic mtime-gated writes, JSONC
refusal, releaseUnmanagedKeys for mode switches), records the spool
directory on the marker for detach and purge to sweep (LLP 0258
#marker-and-spool, LLP 0253), and refuses below Claude Code 2.1.193 with a
`claude update` hint, leaving any existing attach byte-identical (LLP 0258
#version-floor, no fallback to any other mode).
claude_version.js owns the floor: numeric triple parse and compare
("2.1.193" must not sort below "2.1.9"), unknown-is-not-old, a best-effort
`claude --version` probe, and the HYP_CLAUDE_CODE_VERSION override that
keeps hermetic smokes independent of whatever binary the machine carries.
telemetry/spool.js owns the spool path contract: <hyp-home>/spool/
claude-bodies, created 0700 (LLP 0253 #spool-location).
Adopted from the crash-debris-806 stash after review; the malformed mode
doc block in types.d.ts is repaired.
Part of #806 (spec #798, RFC LLP 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp attach claude writes the telemetry env block in otel mode (LLP 0258)
The adapter's attach handler switches its non-proxy branch from base_url
to otel: with no local CA (no proxy-mode gateway) it probes the Claude
Code version, resolves the listener port, and hands the writer the otel
mode. The spool directory is created owner-only right after the settings
write. A CA-bearing install still attaches by proxy; migrating it to otel
is ticket #807.
The endpoint's port resolves in trust order: the live daemon's bound port
from status.json (pid-gated, via the new core
resolveLiveSourceListenPortFromStatus, the generic sibling of the gateway
endpoint resolver), then a configured fixed telemetry.listen_port, then
the well-known default. That is the promise that makes the listener's
default-port bind fallback safe. The marker keeps recording the gateway
port, so the attach-drift check is unchanged.
Smokes pin the new surface: claude_attach_detach golden-compares the
exact nine-key env block, the absence of every routing key (the Remote
Control predicate as absences), the marker's mode and spool_dir, and the
0700 spool; client_attach_idempotent adds a below-floor refusal leg
(exit 1, `claude update` hint, settings byte-identical) beside the
attach/detach cycles; client_attach_on_join proves the daemon's org-driven
attach delivers the same block and that drift now shows in the marker
port; the walkthrough asserts init's real attach lands otel. All four pin
HYP_CLAUDE_CODE_VERSION so no flow depends on the machine's own claude.
Closes#806 (spec #798, RFC LLP 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Spooled body files fill the projection gaps events never carry (LLP 0252)
Body events (api_request_body / api_response_body) join the projection
through their body_ref: the request body supplies system_text, the tools
list, and canonical message ordering; gap blocks (tool_use with
untruncated args, tool_result, thinking with its signature) become their
own projected messages with a minimized pointer-only raw_frame, matching
the proxy path's excerpt policy. Text blocks are never re-projected: the
events already delivered them once under native uuid identity. A ref
outside the spool is refused (the listener deletes what it reads, so an
uncontained ref would be a read-and-delete primitive over the whole
filesystem), a missing body counts as evicted, and an unparseable one is
deleted rather than retried forever. Session-level body facts carry
across exporter batches, bounded oldest-first.
Ticket: #804 (spec #798, RFC 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The daemon caps the body spool and the listener consumes it (LLP 0253)
The listener repairs the spool to owner-only on every start, enforces
the byte cap (telemetry.spool_max_bytes, default 512 MB) at start and on
a timer, and evicts strictly oldest-first (mtime, then name) when the
cap is exceeded: the sweep exists exactly for the window where Claude
Code writes bodies and nothing consumes them. At ingest the referenced
bodies are read, projected, and deleted only after the dataset write
succeeded, so a failed write becomes an HTTP error the exporter retries
against the same files. Status details gain the spool byte size and the
projected / evicted / missing body counts, and eviction is logged with a
count so a machine routinely losing detail to backfill is visible.
Ticket: #804 (spec #798, RFC 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The telemetry smoke joins body fixtures and recovers an evicted session (LLP 0257)
The hermetic flow now drops real-shaped request and response body files
into the spool and proves the join end to end: system_text and the tools
list on every row, the full 600-char tool args the event-side clip would
have truncated, the tool result, the thinking signature, a pointer-only
raw_frame, and both files DELETED after projection. A pre-staged
over-cap body is evicted by the startup sweep (config-driven cap,
oldest-first, visible in status and logs) and its session still
completes: the events land at ingest and transcript backfill recovers
exactly the tool rows the evicted body held, under native identity. The
spool directory a client created loose is tightened to 0700.
Ticket: #804 (spec #798, RFC 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Behavioral events land in their own claude_telemetry_events dataset (LLP 0255)
The first dataset @hypaware/claude owns: a manifest contributes.datasets
entry plus a registration at activation, with the claude_telemetry
source signal so central forwarding never falls back to the dataset
name. One row per event, hot fields typed (event name, timestamp,
session id, tool name, decision, source, cost), every remaining
attribute preserved in a JSON column; a hot key whose value does not
fit its typed column stays in the JSON rather than vanishing, and an
event name we do not model still lands (LLP 0257 S20).
The split is behavioral-vs-conversation: user_prompt and
assistant_response stay in ai_gateway_messages, the body-pointer events
are transport, and everything else becomes a row - including the
metrics half of the exporter config, which the listener previously
dropped: claude-scope metric data points (cost, lines of code, active
time) flatten into the same event shape, one row per data point.
The listener writes the behavioral rows only after the message write
succeeded, so an exporter retry after a failure re-attempts a write
that never happened; there is deliberately no pre-write dedupe (single
producer, one POST per batch), and the lost-success-response window
produces byte-identical rows compaction's content-hash layer collapses.
No localOnlyContentColumns declaration: the LLP 0105 wrapper would null
the attributes column for every ordinary caller (no row here carries a
cwd to prove itself with); this dataset's privacy seam is the inline
ingest drop of LLP 0254 #policy-inline, ticketed as #808.
The activation-context test stubs gain the query registry the real
kernel always wires in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The telemetry smoke reads behavioral events back from claude_telemetry_events (LLP 0255)
The batch grows tool_decision (reject, with its source) and the hook
execution pair beside the existing permission_mode_changed and
tool_result, then hyp query sql asserts the behavioral half: one row
per event in timestamp order, content and body events absent, hot
fields typed and lifted out of the attributes JSON, the unpromoted
attributes (from_mode/to_mode, hook identity and outcome, token counts)
readable through JSON_VALUE. A /v1/metrics POST lands its two data
points as rows named by their metrics, value and unit joined.
The registration surfaces are asserted too: hyp query status enumerates
the dataset beside ai_gateway_messages, and the registration carries
the claude_telemetry source signal. The replayed batch's behavioral
rows double by design (no pre-write dedupe; compaction's content-hash
layer owns the collapse), asserted explicitly so a future dedupe is a
conscious change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The session-ignore control handler becomes shared core machinery (LLP 0256)
A second recorder is about to host the same /_hypaware/ignore/session
route the gateway proxy hosts, and LLP 0256 wants one route shape with
one set of tests rather than a copy per plugin. So the handler (and the
reserved-prefix recognizer) moves from ai-gateway/src/control.js to
src/core/control/session_ignore.js, the same way LLP 0257 S2 moved the
OTLP http/json server into core. The mutation log's event name and
identity fields become caller-supplied so each host keeps its own signal
shape; the defaults keep the gateway's exactly, and no behavior changes.
The route test moves to test/core with it, per LLP 0256's consequence
that the route's tests live at the shared shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The claude listener hosts session ignore and drops the session at ingest (LLP 0256)
The shared OTLP http/json server gains the same reserved-prefix seam the
gateway proxy has: an optional control handler owns /_hypaware/* before
any OTLP routing. The claude telemetry listener registers the shared
session-ignore handler over its own in-memory set (nothing on disk, dies
with the process), so /_hypaware/ignore/session answers identically on
both recorders.
Ingest enforces the set on both signals: events whose session.id is in
the set are partitioned out before the spool is read, so nothing of the
session reaches ai_gateway_messages or claude_telemetry_events, and the
dropped session's spooled bodies are DELETED unread under the same
spool-containment rule as the read path (LLP 0253 delete-on-drop). Each
drop emits the usage_policy_drop signal with policy_source
session_opt_out, and the source status reports the live set size plus
the drop counters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp session ignore posts to every recorder that offers the route (LLP 0256)
A recorder that hosts the session-ignore route now says so itself: the
claude listener advertises control_routes in its status details, and a
new core resolver reads every such advertisement out of a LIVE daemon
snapshot. The mutations address the gateway (its own two-rung
resolution, unchanged) plus every advertised recorder, so the
client-agnostic verb never carries a list of client plugins, and a
listener that is not running is simply not addressed - it records
nothing, so its absence is not a failure.
Each outcome is reported: the receipt keeps its legacy top-level fields
(the gateway's answer) and gains a recorders array naming every write;
the human output prints one line and one responder-trust disclosure per
addressed endpoint. An addressed recorder that refuses makes the verb
report partial and exit unknown - the refusing recorder is the one still
recording, so the write must not read as done. Single-recorder installs
keep byte-identical receipts apart from the new array.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A privacy smoke drives session ignore through both recorders (LLP 0257 S25)
claude_telemetry_session_ignore boots the real daemon, ignores a session
through hyp session ignore (asserting the receipt names the gateway and
the claude listener, and that each control route confirms membership
directly, so the gateway's own route is proven undisturbed), then posts
the ignored session's events, bodies, and metrics: zero rows land in
either dataset, the spooled body is deleted unread, and the drop signal
fires with policy_source session_opt_out. unignore restores capture for
the same session, whose resumed body is projected then deleted normally.
A clean session lands beside the drop throughout, isolating the effect.
PRIVACY.md's pausing-a-session passage now names both recorders and the
listener's delete-not-skip duty for spooled bodies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp status renders a capture-health line for otel-attached clients (LLP 0257 S17)
On the otel path capture is best-effort, so a stale endpoint, a down
daemon, and upstream event drift all fail into the same silence while
every other status line stays healthy. This is RFC 0245 open question
1's duty: the gap becomes a line, and past a threshold a diagnostic.
The comparison has two sides. The listener source now publishes
last_event_at unconditionally (null before the first event), so its
presence marks the snapshot as the telemetry listener's, the same
self-advertisement pattern as control_routes; status reads it from
status.json without a liveness gate (the LLP 0164 argument: last seen
at T survives its daemon, and the dead-daemon window is precisely the
gap to surface). The client's own side is a new manifest-declared
activity_probe (dir + file suffix, the settings_file home-relative
contract) that core stats fresh for the newest transcript mtime, so a
daemon that was down while the user worked cannot hide the loss.
assessCaptureHealth is pure: the baseline is the newer of the last
event and the marker's attached_at, so months of pre-attach
transcripts prove nothing right after a migration, and a listener
that never saw an event is measured from the attach. Fifteen minutes
past baseline is a warning; two hours escalates to an error that
degrades overall, because an attached machine silently losing
sessions is an outage, not an unfinished setup. The attach probe also
reads the marker's mode and attached_at, gating the section to
otel-attached configured clients and carrying mode into --json.
Part of #810 (spec #798, RFC LLP 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A capture-health smoke drives hyp status through lockstep, gap, and detached (LLP 0257 S17)
Modeled on status_diagnostics: one otel-attached claude install (marker
plus transcript trail under a fake $HOME, listener detail under
status.json, no daemon alive - the comparison must survive its daemon)
driven through three states. Lockstep renders the line and stays
healthy; transcripts hours past the last event turn --json's
capture_health entry to gap, fire the capture_gap diagnostic at error
severity with the attach repair hint, and degrade overall while the
text surface tags the line; removing the marker empties the array and
silences the section. Span assertions pin the healthy and degraded
status.render runs.
Part of #810 (spec #798, RFC LLP 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp attach claude migrates a proxy attach to otel in one command (LLP 0245)
The adapter stops choosing proxy mode off the CA on disk: otel is the
claude client's only attach mode now, so an attach on a proxy-attached
machine IS the migration. The settings write releases the proxy keys
through the ordinary mode-switch rule and reports the prior marker mode;
on 'proxy' the adapter unwinds the launchd environment (darwin,
best-effort, mirroring the detach undo's release) and prints the
migration story, ending with the one residue that stays the user's call:
the CA trust, offered as 'hyp detach claude --purge' and never run for
them. Below the version floor the refusal still fires before any I/O, so
a too-old client keeps its working proxy attach byte for byte.
The proxy-only attach machinery this retires from the adapter
(ensureDarwinProxyTrust, the CA preflight read, the Cmd-Q relaunch note)
goes with it; detach's own launchd release and the purge path are
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp status names the attach mode on the text surface (LLP 0245)
--json has carried client_attach[].mode since the marker grew one; the
text line a human reads still said only 'attached', so a machine the
migration just moved from proxy to otel looked unchanged from the surface
that matters. The mode now rides the attached state ('attached (otel)');
markers that predate modes keep the bare word, and the not-attached and
probe-error shapes are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The migration overlap window collapses to one row set (LLP 0245)
Sessions started before the mode flip keep proxying while new events
arrive over OTEL, so for a while both producers capture the same session.
These tests pin what makes that harmless: the proxy projector's native
transcript identity and the telemetry projection's message.uuid identity
yield the same part_ids for the same content, and the OTEL producer's
pre-write dedupe therefore drops every part the proxy already stored,
whole or half. Either arrival order collapses, because the proxy's
flush-time dedupe asks the same committed-part_id membership question.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The migration's CA offer stops claiming a trust it never verified (LLP 0245)
A proxy attach whose keychain dialog was refused still ran and still left
the CA on disk, so the migration telling that user their CA "is still
trusted in your login keychain" is the one false line in an otherwise
honest story. The offer now names the CA and any trust it was granted,
which is exactly what 'hyp detach claude --purge' removes either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp status proves the migrated mode through the real collector (LLP 0245)
The migration tests probed the marker directly and the renderer tests fed
themselves fabricated client rows, so nothing joined the two: the claim
that 'hyp status reflects the new attach mode' held only if the collector
in between happened to carry `mode` through. The rig now writes the
install config a real machine has, and one test drives
collectHypAwareStatus plus renderStatusText over the same temp home before
and after the one command: 'attached (proxy)' becomes 'attached (otel)'.
The three non-migrating attach shapes (floor refusal, re-attach, base-URL
switch) also assert no launchd line, which is what the residue unwind
prints on darwin when it runs at all - the negative half of "the unwind
happens on a proxy migration and nowhere else".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* hyp purge and detach empty the raw-body spool (LLP 0253)
The spool holds raw request and response bodies no row has been made from yet,
so the two verbs that exist to get rid of captured data have to reach it: a
purge that leaves it lets the next received batch write back rows the user just
deleted, and a detach that leaves it strands raw prompts in our own directory.
Core learns the root (`<hyp-home>/spool`) and the claude plugin's directory
becomes a child of it, so `hyp purge` empties every client's spool without
naming a plugin. Detach takes the path off the attach marker instead, because
the config that produced it is gone by then - which makes it settings-file
input, so it is honored only when it is a direct child of this install's spool
root. Without that gate, "empty the directory the marker names" would be a
recursive delete pointed anywhere a hand edit chose.
Every purge target sweeps, targeted ones included: a spooled body has not been
read, so nothing about it says which directory or session it belongs to.
Neither sweep can fail its caller - the destructive work has already landed by
the time it runs, so an unreadable entry is a reported count, not a thrown
error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The folder usage policy decides at ingest on the OTEL path (LLP 0254)
.hypignore and the machine-local list are resolved per session from the cwd the
SessionStart hook recorded, before the spool is read and before either dataset
is written. An ignored session's events project nothing and its spooled bodies
are deleted unread, so the transport keeps working and the content goes.
The machine-local list is threaded from the SHARED state root, the same path
the export and query seams read. The per-plugin state directory has no list
file, so a resolver built from it silently sees `.hypignore` dotfiles only, and
a `--private` directory would have recorded here after being dropped everywhere
else.
A session with no hook record has no cwd and so no verdict. It is withheld
rather than recorded: writing first and resolving later is the fail-open window
LLP 0085 exists to patch, and this path has no flush-time late drop to patch it
with. The content is still in the Claude Code transcript, which is what the
signal names as the recovery path, and the withhold is counted in status so a
machine whose hook is missing reads as blind rather than idle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A privacy smoke drives .hypignore through the telemetry listener (LLP 0257 S25)
The OTEL-path analog of hypignore_capture_drop, with four sessions posted at a
live listener: one clean, one under a `.hypignore`, one on the machine-local
list with no dotfile anywhere near it, and one with no SessionStart record at
all. Only the clean session's rows land, in either dataset, every suppressed
session's staged body is gone from the spool, and each drop signal names what
governed it.
The machine-local case is the one that catches a wiring regression nothing else
would: its verdict is only reachable if the listener reads the list from the
shared state root. The hookless case pins the fail-open window closed.
PRIVACY.md gains the spool as a named surface, since it is a directory of raw
prompts under the user's own home: what it is for, that a dropped session's
bodies are deleted unread, that it is capped, and that purge and detach empty
it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The glossary stops saying attach rewrites a base URL (LLP 0258)
The client-source gloss described one attach mechanism as if it were the
only one; it has been wrong since proxy mode shipped and is wrong twice
over now that the claude client attaches by telemetry. The gloss now says
what attach means and defers the how to a new Attach entry that names the
three modes and what each one costs the user.
Also records the distinction the OTEL path invites a reader to get wrong:
`@hypaware/claude` running an OTLP listener does not make claude an `otel`
source. It stays a client source, autodetected and configured for the
user; the `otel` picker source is what someone turns on for their own app.
Discharges the glossary duty in the RFC's implementation spec
(#798, #811).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* A release gate checks the OTEL shapes against the installed Claude Code
LLP 0245 open question 5 leaves flag stability detected two ways: the
`hyp status` capture-health line in production, and a shape assertion
against the real client at release time (LLP 0257 S21). The second one
had no written form until now.
`claude_otel_shape_check` walks a human through it: assert the nine-key
env block on disk, take a raw body sample with the daemon deliberately
stopped (the only way to read a file the listener otherwise deletes on
sight), assert the body fields the projector fills its column gaps from,
then hold a real conversation and read the event names, the event
attributes, the message columns, and the capture-health line back out.
The event-name query doubles as the drift detector, because an event the
listener does not model is recorded rather than dropped: the step asks
for the whole list in the release notes, not a verdict, so the next
release has something to diff against.
The release checklist now names it for any release touching the claude
adapter, and says why the hermetic smoke cannot stand in for it: a smoke
POSTs a fixture we wrote, so it agrees with itself no matter what
upstream did. Also lists `openclaw_capture`, which has had a written
procedure for a while without appearing here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The README describes the attach Claude Code actually gets (LLP 0245)
The proxy-mode section opened by telling the reader that `hyp attach
claude` points ANTHROPIC_BASE_URL at the gateway and that proxy mode is
the opt-in that saves Remote Control. Neither is true on this branch:
claude has one attach mode, `otel`, and Remote Control survives it for
free because no base URL and no proxy are written at all.
Claude Code gets its own section covering what attach writes, where the
raw bodies go and when they are deleted, the version floor and its
refusal, and the proxy-to-OTEL migration. The proxy-mode section keeps
everything true of a client that still proxies (the CA, the decryption
aperture, the corporate-proxy chain) with the claude-specific framing
taken out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The shape check waits out an export interval before calling it broken
Claude Code batches its OTLP exports, and the metrics exporter runs on a
longer interval than the logs one. Checking the spool or the event list
immediately reads "not yet" as "the listener is not consuming", which is
the one wrong answer a release gate must not give. The two steps now wait
and say why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* RFC 0245 renumbered to 0262: open PR #815 claimed 0245
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* RFC 0262 and the docs it spawns are Accepted
Phil accepted the RFC and its spawned decisions and spec on 2026-08-17; the
conditional forward-ref phrasing in the displaced proxy-attach docs is firmed
up to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* The 0245 renumber reaches the test tree's @refs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review round 1: the daemon stops minting the body spool, and status stops printing a raw marker mode
Three review findings on the OTEL attach path, all narrow:
- The telemetry listener called `ensureClaudeBodySpool` on every source
start, so a daemon created `<hyp-home>/spool/claude-bodies` on machines
that never attached the claude client, against whatever HYP_HOME the
activation context resolved. `activatePlugins` does not thread the
daemon's env, so ctx.env is `process.env`: running `npm test` without
`HYP_HOME` demonstrably created the directory in the developer's real
`~/.hyp`. Attach is what mints the spool (the same write tells Claude
Code where to put bodies), so the daemon now repairs a directory it
finds (`tightenClaudeBodySpool`) and creates none.
- `hyp attach claude` ran the spool mkdir inside the same try as the
settings write, so an unwritable spool root reported a failed attach
after the settings file had already been rewritten, and swallowed the
migration notes with it - including the `hyp detach claude --purge`
line a migrated machine needs. It is now its own try; the failure rides
the existing warnings list, counted apart from
`malformed_blocks_repaired` so that count keeps meaning one thing.
- `hyp status` interpolated the attach marker's `mode` into the clients
row raw. The mode is read back off the client's own settings file, so
it is a captured label, not an in-process constant; it now goes through
the same `printable` every other disk-derived label on that surface
uses (LLP 0225).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review round 2: hyp status compares where claude exports against where the listener listens
An `otel` attach writes one `OTEL_EXPORTER_OTLP_ENDPOINT` into the client's
settings and nothing ever rewrites it. Two ordinary sequences move the
listener out from under it:
- attach ran with no live daemon, so `resolveAttachTelemetryPort` could only
write the well-known default, and the daemon later found that port taken
and fell back to an ephemeral bind (LLP 0114 #ephemeral-fallback);
- `telemetry.listen_port: 0` plus a daemon-down attach, where the default is
written for a port the config guarantees will never be bound.
Either way Claude Code keeps POSTing at the port it was told about, with
`OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_ASSISTANT_RESPONSES` on, and every
other line in `hyp status` stays healthy. `client_attach_stale` does not
cover it: that comparison watches the gateway, which this mode never talks
to. The capture-health gap line eventually notices the silence, but only
after fifteen minutes of transcript activity and without naming the cause.
Both ports are already on disk. The probe now lifts the marker's managed
`OTEL_EXPORTER_OTLP_ENDPOINT` port (the live value the client is using, not
a parallel field that could disagree with it), and the capture-health block
compares it against the listener's live `listen_port`, raising a
non-degrading `client_telemetry_stale` naming both ports and the repair.
Liveness-gated, unlike `last_event_at` beside it: a dead daemon's snapshot
cannot say where anything is bound now, and a restart is what moves the port
back. A marker with no endpoint, or one that does not parse to an in-range
port, makes no claim at all.
This is LLP 0114 #fallback-is-visible applied to the new listener - the half
of that decision the listener had adopted the fallback without - and LLP 0257
gains S17b to record it.
Does not prevent the exposure, only makes it loud: whether attach should
refuse to write an endpoint no daemon has confirmed is a design call, left
for a human.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review round 2: a restarted daemon stops reporting a month-long capture gap, and a failed listener start owns nothing
Two defects in the new listener, both found by re-reading the start path.
The capture-health baseline could not survive a daemon restart.
`state.lastEventAt` lives only in the listener's process, so every restart
republishes `last_event_at: null` however long capture has been healthy.
`assessCaptureHealth` then fell back to the attach timestamp, which on a
machine attached a month ago and used an hour ago yields a month-long gap:
severity `error`, degrading `overall`, on an install that is capturing
perfectly. The first repair the diagnostic prints is `hyp daemon restart`,
so the advice reproduced the finding. The listener now publishes
`listener_started_at`, and the gap is measured from the newest of the last
event, the attach, and a LIVE listener's start - live only, because on a
dead daemon the last one's start bounds nothing and the growing gap is
exactly what the line exists to surface. The null-events message stops
saying "after the attach", which is no longer always the baseline it used.
`listener_started_at` rides the capture-health report and `--json` so the
reason a gap is or is not claimed is readable.
The spool sweep timer was armed before the bind. `stop()` is the only thing
that clears it, and a `start()` that throws never returns a handle to call
`stop()` on, so an explicit-port bind failure (LLP 0114
#explicit-listen-fails-loudly) left an interval scanning the spool every
minute for the life of the daemon on behalf of a source that does not
exist. `unref()` kept it from holding the process open, which is why it
would never have been noticed. The one-shot sweep still runs before the
bind - bodies already on disk are over the cap either way - and only the
repeating one waits for a listener to be behind it.
LLP 0257 gains S17c for the restart baseline. Tests: five in
test/core/status-capture-health.test.js covering the restart, a listener up
long enough for the gap to be real, an event outranking the listener start,
the live-daemon and dead-daemon halves end to end; and a new
test/plugins/claude-telemetry-start-failure.test.js that counts `setInterval`
arming across a failed start, because an unref'd timer does not appear in
process.getActiveResourcesInfo() - verified to fail against the old order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review round 2: otel attach names the OTLP keys that outrank the endpoint it wrote
In the OTLP environment-variable contract a per-signal key beats the generic
one, so `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` decides where log records go
whatever `OTEL_EXPORTER_OTLP_ENDPOINT` says. Nothing in the tree mentioned
those keys.
That leaves one shape worth saying out loud: a machine already exporting to
its own collector through a per-signal key gets `OTEL_LOG_USER_PROMPTS`,
`OTEL_LOG_ASSISTANT_RESPONSES` and `OTEL_LOG_TOOL_DETAILS` switched on by
this attach, and its prompts and assistant responses start flowing THERE,
while `hyp status` reports `attached (otel)` and the listener sees nothing.
`OTEL_EXPORTER_OTLP_HEADERS` is the same hazard from the other side: it
carries a collector's credentials and would now ride requests aimed at our
loopback listener.
The managed set is untouched - LLP 0258 #env-keys is "exactly these keys,
and only these", and it is Accepted. Attach now warns per offending key
instead, on the same warnings list every other attach notice rides. A
warning rather than a refusal because attach cannot see a key exported from
the user's shell, so refusing on the half it can see would buy a false sense
of completeness. Values are never echoed: an endpoint or a headers value is
exactly where a collector token lives, and this string is printed, logged,
and serialised into `--json`.
Two tests in test/plugins/claude-settings-otel-attach.test.js: the warning
fires per key, echoes neither the collector nor the credential, and leaves
the keys themselves untouched and unmanaged; and an ordinary attach still
warns about nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* The client hook enforces the body spool cap too, so a down daemon is bounded (LLP 0263)
LLP 0253 #byte-cap named the daemon-down window as the reason the spool's
byte cap exists, then shipped every enforcement inside the listener source,
so the window it named was the one window nothing swept. Claude Code keeps
writing raw request and response bodies whether or not the daemon reads them,
at roughly 145 KB per request, and the daemon is legitimately absent for a
crashed service, a machine where one was never started, an uninstall that
skipped detach, and the attach-before-first-start path the port resolver
deliberately supports. With OTEL_LOG_USER_PROMPTS and
OTEL_LOG_ASSISTANT_RESPONSES on, that is unbounded retention of raw prompts,
not just a disk nit.
`hyp claude-hook session-context` now enforces the same cap on its way out.
It is the right second enforcer because attach already installs it on
SessionStart, CwdChanged, UserPromptSubmit and PostToolUse/Bash (LLP 0085),
so it runs at exactly the cadence bodies are written and the spool cannot
outrun it, and because it needs nothing the daemon owns. It deletes only what
the daemon's sweep would have deleted: the same enforceClaudeBodySpoolCap,
same directory, same oldest-first order, same operator cap read from the same
telemetry.spool_max_bytes key. The hook never widens the rule, it runs the
existing one while the daemon cannot.
The sweep goes last so it can never delay the session-context records the
projector waits on (LLP 0085), and runs on every invocation including the
ones that record nothing, because a malformed event says nothing about
whether the spool is filling. A sweep failure is swallowed, as is the
recording half, so the hook still exits 0.
LLP 0263 carries the decision and 0253 gains the Extended-by forward-ref.
Nine tests drive the hook with no daemon anywhere: four of them fail if the
sweep call is removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LLP 0263 is Accepted
The last Draft in this change set joins LLP 0252-0258 and 0262 at Accepted,
so every doc PR #818 lands is approved for implementation and the code
already on the branch cites settled decisions rather than a draft.
Accepted, not Active: the lifecycle reserves Active for built AND merged,
and #818 is still an unmerged draft. All nine flip together at merge.
Status only. Nothing 0263 settles was edited, and it carries no [inferred]
claims to retire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: test <test@test.com>
@philcunliffephilcunliffe added neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human and removed neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) labels Aug 18, 2026
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

neutral is stuck on #815: the textual merge is trivial, the design underneath it is not

What neutral was doing.neutral prs reported action=resolve-conflict (CONFLICTING/DIRTY) after #818 and #785 landed on master. neutral merged origin/master into integration/proxy-mode-capture in a throwaway worktree, resolved all three conflicts, and got a green local run. It is not pushing, because resolving the text would ship a design record and a release gate that merged work has already contradicted, and that is your call, not neutral's.

The three conflicts and how they resolve textually

FileOurs (#815)Theirs (master, from #818)Resolution
llp/0231-proxy-mode-capture.rfc.mdadds **Designed-by:** LLP 0245adds **Extended-by:** LLP 0262Both. Two independent additive forward-refs on adjacent lines; neither touches what 0231 settled.
AGENTS.mdadds the claude_proxy_capture bullet to the written-procedures listadds openclaw_capture and claude_otel_shape_check bulletsBoth, same list.
docs/ACCEPTANCE.mdappends the ## claude_proxy_capture section (~340 lines)appends ## claude_otel_shape_check (~300 lines)Both sections, --- separated, at the same append point. No duplicate entry.

With those resolutions: npm test is 4482 pass / 1 fail, the one failure being the inherited-from-mastertest/core/repo-scratch-hygiene.test.js "no tool transcript is tracked in the repo" (tracked x/npm-test.log, x/typecheck.log). npm run typecheck is clean. So nothing mechanical is blocking.

The actual blocker: #818 superseded LLP 0245's Claude-attach story

You already deconflicted the number - #818 renumbered its RFC 0245 -> 0262 explicitly to cede 0245 to this PR (2221a698). That resolved the collision on the label. It did not resolve the collision on the content, and the content collision is head-on:

On master today, hyp attach claude cannot attach in proxy mode at all.hypaware-core/plugins-workspace/claude/src/index.js hardcodes mode: MODE_OTEL with the comment "otel is the claude client's only attach mode: a machine still carrying a proxy attach is migrated by this same write [...] never re-attached by proxy", carrying @ref LLP 0258#version-floor [constrained-by]: one attach mode per client, with no proxy fallback. llp/0232-claude-attaches-by-proxy.decision.md on master now reads Superseded-by (in part): LLP 0262, LLP 0258.

What that breaks in this PR's diff:

  1. llp/0245 section 1, step 1 - "Attach writes env.HTTPS_PROXY and env.NODE_EXTRA_CA_CERTS into ~/.claude/settings.json". False for the claude client on master.
  2. llp/0245 section 4 #claude-attach - the whole section, and specifically "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". That predicate no longer runs on this path. The section also predates MODE_OTEL and the settings-env telemetry block entirely.
  3. docs/ACCEPTANCE.mdclaude_proxy_capture - unrunnable as written, not merely stale:
    • step 2's pass condition is "env carries exactly HTTPS_PROXY and NODE_EXTRA_CA_CERTS". A real hyp attach claude now writes the nine-key OTEL block. Step 2 fails on a correct tree.
    • step 3 reads launchctl getenv NODE_USE_SYSTEM_CA. The OTEL attach path does no launchctl setenv and installs no LaunchAgent (@ref LLP 0258#nothing-else).
    • step 6 re-runs hyp attach claude expecting the proxy attach to come back.
  4. AGENTS.md - listing claude_proxy_capture as a written procedure makes it a release gate "before a release that touched the relevant adapter". For the claude adapter that gate is now claude_otel_shape_check, which OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) #818 already added two lines below it.

What is not superseded, and is worth keeping: LLP 0245 sections 2 (src/core/tls/), 3 (the CONNECT front door, blind tunnel, and record_prefix aperture) and 6 (failure modes). LLP 0262 narrows the proxy's client list, it does not retire the gateway - codex, claude-desktop, openclaw, hermes and raw SDK traffic still ride it, and llp/0231 on master says exactly that in its own Extended-by: line. There is a real, still-true design of record in this doc; it is the Claude-attach third of it that died.

The call neutral needs from you

Per the repo's own rule, neutral will not silently rewrite an authored design doc so it agrees with newly merged code. Options, roughly in order of how much of #815 survives:

One dependency to weigh: #817 (Proxy mode by default: design of record for LLP 0242) is open and builds on this change set; it is being resolved by another worker and neutral has not touched it. LLP 0243 and LLP 0244 both carry Extended-by: LLP 0262 on master, so #817 likely faces a smaller version of the same question.

Reply with a comment on this PR (or push to the branch); neutral monitors this thread and will re-engage with your guidance on its next tick.

philcunliffe added a commit that referenced this pull request Aug 19, 2026
…mits openclaw_capture (#831) (#833)
Two independently reachable findings from the #815 triage, both pre-existing
on master and outside that PR's diff.
`aigw.proxy_mode_stale_ca` fires when `proxy_mode` is off but an
interception CA is still on disk, and told the operator to "run `hyp attach
claude` to move it back to base-URL mode". Attach derives its mode from that
same CA (LLP 0232 #proxy-attach-preflight), so in exactly this state a
re-attach picks proxy mode again: the named remedy is a no-op. The warning
now names the two that land, removing the CA with `hyp detach claude
--purge` and re-attaching, or turning `proxy_mode` back on. The existing
blind-tunnel test pins the reason string so the wrong remedy cannot come
back.
AGENTS.md's "Written acceptance procedures" list has named only
`codex_desktop_capture` since #570 added `openclaw_capture` to
docs/ACCEPTANCE.md, so the second written procedure was invisible to anyone
reading the repo guidance rather than the doc. Listed it.
Co-authored-by: neutral <neutral@hyparam.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: test <test@test.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:stuckneutral attempted this but cannot complete it autonomously — needs a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@philcunliffe