detach --purge then attach no longer downgrades claude to base URL in silence - #822
detach --purge then attach no longer downgrades claude to base URL in silence#822philcunliffe wants to merge 6 commits into
Conversation
… silence `hyp detach claude --purge` deletes the local CA by design and leaves `proxy_mode: true` in config, and the running daemon has no reason to re-mint. The next `hyp attach claude` therefore read the CA-derived preflight (LLP 0232), found nothing, and wrote a base-URL marker while reporting success. `hyp status` said nothing either: its proxy-trust block is built from the CA, so with no CA the whole block vanished and the install reported healthy with Remote Control inbound dead. Root cause is the attach-time gate: `proxy_mode: true` returned early as "nothing to do", which is true only when a CA also exists. - The gate now reads config AND the CA. With both, unchanged. With the key and no CA, the attach names the downgrade on every shape (non-TTY, --json and `attach all` included) and, interactively, offers the repair. - The repair is `enableGatewayProxyMode` with the write skipped: a daemon restart makes the gateway re-mint, then the existing attach takes the proxy branch. Because it writes nothing it also repairs a fleet-managed host, where LLP 0244's migration must decline. New outcome, `remint`. - `hyp status` gains a `proxy_mode_ca_missing` warning naming the state and the repair, on the text and --json surfaces, on every platform. LLP 0259 records the decision: it extends LLP 0244 (whose gate is keyed on config alone, correct for the migration question) and settles LLP 0232 #proxy-attach-preflight, which said both "a missing CA is a refusal" and "base-URL mode otherwise". The fallback is what was built and stays; the silence is what was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e LLP 0259 paths - maybeOfferProxyModeMigration's new remint branch claimed 'daemon restarted, local CA re-minted' without checking daemonInstalled, so a host with no service was told a repair ran that could not have. - repairStrandedProxyMode had no branch for outcome 'enabled', reachable when enableGatewayProxyMode's layered read throws and it falls back to the local file, so a repair that succeeded was reported as a failure. - The proxy_mode_ca_missing status diagnostic ignored 'enabled: false' on the gateway entry, unlike every other plugin lookup in status.js. A disabled gateway never mints, so the named repair could never clear the warning. Each fix has a regression test that fails on 3b4776c.
VerdictApprove with fixes applied. The diagnosis is right, the root-cause fix is the right shape, and LLP 0259 settles a real contradiction rather than inventing one. Three findings, all in the reporting half of the change (the half this PR exists to get right), all fixed on the branch with regression tests that fail on Reviewed at Independently verifiedThe LLP 0232 contradiction is real.
and in the next:
Those cannot both hold. And the code claim checks out too: the adapter's only LLP 0259's call is the right one. The refusal sentence, read at the seam it governs, means the adapter refuses whenever no CA exists - and the adapter cannot see config, so that is every ordinary base-URL install, not just the stranded one. Settling in favour of the fallback and fixing the silence is correct. Two things I checked because they are what would make the call wrong:
LLP immutability holds. The gate genuinely distinguishes the two states. The user-visible surface is right.
The status diagnostic is non-degrading. Findings1.
|
…top at the machine On a daemon-managed install (no configured `listen`) attach short-circuits at "already attached" whenever the client's marker names the live port. A daemon restart keeps that port, so after an accepted re-mint the marker still matched and the adapter never ran: the CLI printed the repair's success line while the client stayed in the mode the repair was meant to leave behind, and the darwin trust grant (which only runs inside attach()) never re-ran either. That is the silent wrong outcome LLP 0259 exists to remove, reached by declining the offer once and accepting it on the next run. maybeOfferProxyModeMigration / repairStrandedProxyMode now report whether the proxy state actually moved, and a move disqualifies the no-op when there is a live endpoint to re-attach at. An offer that changed nothing (declined, or reported with no daemon to restart) leaves the LLP 0086 no-op untouched. Also corrects two JSDoc sentences that overstated readLocalCaInfo's contract: it returns undefined for a missing or malformed cert but throws on any other read failure, which the adapters do not catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VerdictApprove with one fix applied. Round 1's three fixes hold under scrutiny and introduced no misreporting of their own. The round-2 sweep ("can this line be printed when the thing it describes did not happen?") turned up one more instance of the PR's own bug shape, in the half nobody had looked at: the repair reaches the machine and can stop short of the client. Fixed on the branch with a regression test that fails at Reviewed at Round 1's three fixes: verified, and completeEach re-derived from the code rather than from the round-1 record.
None of the three introduced a new claim that outruns what was checked. The "can this message lie?" sweepI walked every string these paths can print. Results: Honest, verified: all four Not honest in one shape - finding 1 below. Every one of those I also checked Findings1. An accepted repair can print its success line over a client it never reached - medium - FIXED
The client stays on Same short-circuit also swallows the LLP 0244 migration's Fix. Tests in One sentence added to LLP 0259 2.
|
|
Triage at head Every deferred finding is tracked in #832, including:
|
Three conflicts, all additive: LLP 0232 keeps 0259's Extended-by clause beside master's Superseded-by (in part) for 0262/0258; LLP 0244 carries both Extended-by clauses (0259's CA-reading gate, 0262's OTEL retarget); StatusDiagnosticKind takes both new kinds, proxy_mode_ca_missing and capture_gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neutral triage: cannot ship at this head, needs a human decisionWhat neutral was doing. Triage after 2 review rounds on PR #822. Both rounds' findings were verified fixed and the residue was deferred to #832 at head Why it cannot proceed. The merge brought in master's The blocker, concretelyAt head, the claude adapter attaches only via OTEL and never by proxy:
Yet this PR's
The LLP corpus also lands in contradiction: LLP 0259 (Accepted, minted by this PR) settles that attach repairs a stranded install back into a proxy attach for claude, while LLP 0258 (Accepted, on master) settles one attach mode per client with no proxy fallback. Per repo rules two Accepted docs must not contradict; this PR would create that state. Why no gate caught it. Both review rounds (at Non-blocking residue (unchanged, for the whole picture)All already tracked in #832: the status-diagnostic gap after an informed decline, platform wording shared with the LLP 0244 prompt, the Decision neededHow to reconcile this PR with LLP 0262/0258. The plausible shapes:
Neutral will not choose among these: each discards reviewed, working code or re-scopes an Accepted doc, and the sequencing question (this PR versus the LLP 0262 rollout) is a maintainer call. To unstick: 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. |
The defect
On a real macOS install,
hyp detach claude --purgefollowed byhyp attach claudereports success and writes a base-URL attach. No CA is minted, no keychain dialog appears, Remote Control inbound stays dead, and no surface says a thing:hyp statusreports healthy.Three settled behaviours compose into it, each right on its own:
#proxy-attach-preflight).--purgedeletes the CA (LLP 0238#ca-survives-detach) and never touches config, soproxy_mode: truesurvives with no CA behind it; the running daemon holds its loaded CA in memory and never re-mints.proxy_mode: true, because its own question is about writing that key.Root cause
The attach-time gate at
src/core/commands/clients.js:proxy_mode: truecovers two states and only one is settled:proxy_mode: trueproxy_mode: trueThe repair already exists and already works.
maybeOfferProxyModeMigrationruns before the endpoint is resolved precisely so an accepted switch can restart the daemon and letattach()find the fresh CA. It just declines to fire in the one state that needs it.What changed
The gate reads config and the CA. With both, unchanged and silent. With the key and no CA, the attach reaches a new branch.
The downgrade is named on every attach shape (non-TTY,
--json,hyp attach allincluded;--dry-runstays the one silent shape), on stderr, before any question, with the manual repair in the line. A user who reads only that line can fix the machine by hand.The repair is a daemon restart, and it writes nothing. An interactive, single-client, wet-run attach offers it; accepting reuses
enableGatewayProxyMode's restart/bind-wait/CA-wait steps with the config write skipped, and the attach below then takes the proxy branch it already has. Because nothing is written it has nothing for the LLP 0031 merge to drop, so it repairs a fleet-managed host too, where LLP 0244's migration has to decline. New outcome on that function:remint.alreadynow means "config right and CA present". Where no daemon service is installed, the existing install-and-start ladder is named instead of a repair being claimed.hyp statusnames the state. Aproxy_mode_ca_missingwarning diagnostic on both the text and--jsonsurfaces, on every platform (the keychain/launchd trust block is darwin-only by design; a config asking for a transport the machine cannot serve is not). This is the missing half of a pair whose inverse (proxy mode off, CA present) the gateway already warns about asaigw.proxy_mode_stale_ca. Warning and non-degrading: a freshly-configured install whose daemon has not started yet is legitimately here for a few seconds.--purgeitself is unchanged. Re-minting on the way out contradicts what purge promises, and clearingproxy_modecannot work on a fleet host, where that key belongs to the central layer.The new LLP
LLP 0259 (Decision, Accepted) records this. It was needed for two reasons:
#attach-offerssettles that "the offer is keyed on the config, not the CA". This adds a CA-derived trigger to that gate, so it changes what an Accepted doc settled. 0244 gains anExtended-by:forward-ref; nothing in it was rewritten.#proxy-attach-preflightcontradicts itself, saying both "a missing CA is a refusal (markActionRefused), not a warning, and nothing is written" and, a paragraph later, "attach uses proxy mode when a CA exists and base-URL mode otherwise". Only the second was ever built (the adapter's oneCA_MISSINGrefusal sits behind proxy mode having already been selected, so it cannot fire here). 0259 settles it in favour of the fallback, and repairs what made the fallback unsafe: refusing outright would break the ordinary base-URL install, which is most installs. The fallback was never the bug. The silence was.Verification
Regression tests fail on
origin/masterand pass here (11 failing before, 0 after):test/core/attach-proxy-migration.test.js: the purge-then-attach sequence, with the fake adapter now recording the mode the way LLP 0232's preflight decides it, so the downgrade is visible to the test. Covers decline, accept, non-TTY,attach all,--dry-run, and the CA-present case that must stay silent.test/core/gateway-proxy-enable.test.js: theremintoutcome, the untouched config and absent backup, the fleet-managed host, the no-daemon case, and the CA timeout.test/core/status-proxy-mode-ca-missing.test.js(new): the diagnostic on both surfaces, on darwin and linux, and its absence in the two states that are not faults.npm test: 4267 pass, 4 fail. Those 4 are the pre-existing union-source absent-column failures onmaster(#820), identical to the pristineorigin/masterbaseline captured in the same worktree.npm run typecheckclean. Smokesclient_attach_idempotent,claude_attach_detach,status_diagnostics,walkthrough_picker_to_first_queryall green.The darwin half (keychain trust, the real dialog) cannot be proven on Linux; a release touching this path wants a
docs/ACCEPTANCE.mdpass on a real Mac.Fixes #819