Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion llp/0232-claude-attaches-by-proxy.decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ environment, because the two-key attach alone breaks Remote Control inbound;
see LLP 0236); LLP 0247 (#attach-writes-https_proxy-not-a-base-url: the
gateway now serves absolute-form request-targets to registered hosts on
forward-proxy listeners, so the case against `HTTP_PROXY` rests on the
no-plaintext-traffic-worth-capturing rationale alone)
no-plaintext-traffic-worth-capturing rationale alone); LLP 0259
(#proxy-attach-preflight: this section says both "a missing CA is a refusal"
and "base-URL mode otherwise"; 0259 settles it in favour of the fallback,
which is what was built, and requires the CLI caller to name the downgrade
whenever config asked for a proxy)
**Superseded-by (in part):** LLP 0262, LLP 0258 (accepted 2026-08-17; attaching the `claude` client writes a telemetry `env` block instead of
`HTTPS_PROXY` and `NODE_EXTRA_CA_CERTS`; #mode-migration and the `prev_env`
undo record are what the third mode reuses unchanged)
Expand Down
5 changes: 4 additions & 1 deletion llp/0244-attach-migrates-to-proxy-mode.decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
**Date:** 2026-08-17
**Related:** LLP 0031, LLP 0174, LLP 0181, LLP 0232, LLP 0233, LLP 0242,
LLP 0243
**Extended-by:** LLP 0262 (accepted 2026-08-17; the migration machinery
**Extended-by:** LLP 0259 (#attach-offers: the gate reads the CA as well as
the config, so `proxy_mode: true` with no CA reaches a repair instead of
returning; #central-managed does not apply to that repair, which writes
nothing); LLP 0262 (accepted 2026-08-17; the migration machinery
here is retargeted, not reverted: `hyp attach claude` migrates a proxy-mode
install to the OTEL mode, unwinding the launchd environment and offering
`detach --purge` for the CA trust)
Expand Down
157 changes: 157 additions & 0 deletions llp/0259-attach-repairs-a-stranded-proxy-install.decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# LLP 0259: attach repairs a stranded proxy install, and never downgrades in silence

**Type:** Decision
**Status:** Accepted
**Systems:** Config, Plugins, Gateway, Daemon
**Author:** Phil / Claude
**Date:** 2026-08-17
**Related:** LLP 0031, LLP 0232, LLP 0235, LLP 0237, LLP 0238, LLP 0242,
LLP 0243, LLP 0244

> `proxy_mode: true` with no local CA on disk is a real state, not an
> impossible one: `hyp detach <client> --purge` produces it by design. Attach
> stops treating it as "nothing to do". It names the base-URL downgrade on
> every attach shape, offers the one repair that fixes it (a daemon restart,
> which re-mints the CA and writes no config), and `hyp status` reports the
> state as a warning instead of rendering nothing at all.

## Context

Three settled behaviours, each right on its own, compose into a dead end
(found on a real macOS install, 2026-08-17):

1. Attach reads the mode off the CA file, never off config (LLP 0232
#proxy-attach-preflight). The adapter cannot see config and must not
promise a transport the gateway is not serving.
2. `--purge` deletes the CA and leaves `proxy_mode` alone. Both halves are
deliberate: purge is the zero-residue exit (LLP 0238 #ca-survives-detach),
and nothing on the detach path has ever written config.
3. The LLP 0244 migration offer returns early on `proxy_mode: true`, because
its own question is about *writing* that key.

So after `hyp detach claude --purge; hyp attach claude` the config asks for a
proxy, no CA exists, the daemon holds its already-loaded CA in memory and
never re-mints, attach writes a base-URL marker, and every surface reports
success. Remote Control inbound is dead and nothing says why.

The repair already exists and already works: `maybeOfferProxyModeMigration`
runs before the endpoint is resolved precisely so an accepted switch can
restart the daemon and let `attach()` find the fresh CA. This is not a missing
capability; it is an existing repair that declines to fire in the one state
that needs it.

### LLP 0232's preflight said two things

`#proxy-attach-preflight` states 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 only `CA_MISSING` refusal in the adapter
sits behind proxy mode having already been selected, so it cannot fire here.

This decision settles the contradiction 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, and LLP 0243/0244 already have the
migration story. What was actually wrong was never the fallback; it was the
silence.

## Decision

### The gate reads config and the CA, not config alone {#gate-reads-both}

`proxy_mode: true` covers two states and only one is settled:

| Config | CA on disk | Meaning |
|---|---|---|
| `proxy_mode: true` | present | genuinely in proxy mode, nothing to do |
| `proxy_mode: true` | absent | config and machine disagree, repair needed |

The attach-time gate returns early only for the first. The second reaches the
branch below. This extends LLP 0244 #attach-offers, whose gate is keyed on the
config alone: that remains correct for the *migration* question (which is about
a config write), and the CA is what decides the second question (which is not).

### The downgrade is named on every attach shape {#never-silent}

Whatever else happens, an attach that is about to write base-URL mode on an
install whose config asks for a proxy says so first, on stderr, before any
question. Non-TTY, `--json` and `hyp attach all` included: those shapes may
not *act*, but LLP 0244 #non-interactive already owes every non-migrating
attach a line naming what was skipped, and this state is worse than the one
that rule was written for. A dry run stays the single silent shape; it changes
nothing and promises nothing.

The line names the repair. A user who reads only that line can fix the machine
by hand (`hyp daemon restart`, then re-attach).

### The repair is a restart, and it writes nothing {#repair-is-a-restart}

An interactive, single-client, wet-run attach offers to restart the daemon,
which makes the gateway re-mint the CA (`prepareInterception` mints whenever
`proxy_mode` is on), after which the attach below takes the proxy branch it
already has. The restart-and-wait steps are LLP 0244's own; only the config
write is skipped, because the key it would set is already there.

Three properties follow from writing nothing:

- **Fleet hosts are repaired too.** LLP 0244 #central-managed declines locally
because a local write would be dropped by the LLP 0031 merge. A restart has
nothing to collide with, so the central branch does not apply here.
- **It fixes the state however it was reached**, not only via `--purge`.
- **It is idempotent and cheap to decline**: nothing is backed up, nothing is
rewritten, and the base-URL attach proceeds as the working fallback.

A repair is not finished until the attach it enables rewrites the client. On a
daemon-managed install (no configured `listen`) attach short-circuits at
"already attached" whenever the client's marker names the live port, and that
port survives a restart: the marker records the *mode* too, and after a
re-mint the recorded one is wrong. A landed repair therefore disqualifies that
no-op, so the run that printed the success line is the run that makes it true.
An offer that changed nothing (declined, or reported with no daemon to
restart) leaves the no-op exactly as it was.

Consent is still asked, and automation is still never restarted mid-run: a
daemon restart is exactly the consequential side effect LLP 0244
#non-interactive keeps out of scripts, and on macOS the attach that follows
raises a keychain dialog that wants a human anyway. Where no daemon service is
installed a restart cannot help, and the flow names the existing
install-and-start ladder instead of claiming a repair.

Declining `--purge` as the fix site is deliberate. Re-minting during purge
contradicts what purge promises (a zero-residue exit), and clearing
`proxy_mode` during purge cannot work where it is most needed, because on a
fleet host that key is the central layer's (LLP 0031).

### `hyp status` names the state {#status-names-it}

`proxy_mode: true` with no certificate rendered as nothing at all: the trust
block is built from the CA, so with no CA the whole block disappears and the
install reports healthy. Status gains a `proxy_mode_ca_missing` warning
diagnostic naming the state and the repair.

This is the missing half of a pair. The inverse (proxy mode off, CA present)
has always been warned about by the gateway itself as
`aigw.proxy_mode_stale_ca`. Unlike the trust block it is platform-independent:
the keychain and launchd halves exist only on macOS, but a config asking for a
transport the machine cannot serve is the same defect everywhere.

Warning, not error, and not degrading: a freshly-configured install whose
daemon has not started yet is legitimately in this state for a few seconds,
and the repair is the same either way.

A gateway entry switched off (`enabled: false`) is read as not asking at all,
the same reading every other plugin lookup in `hyp status` gives that key. A
disabled source never launches and so never mints, which would make the named
repair unable to clear its own warning however often it ran.

## Consequences

- `enableGatewayProxyMode` gains one outcome, `remint`: the config was already
right, no write happened, and only the restart and the two waits ran. Its
`already` outcome now means "config right *and* CA present".
- A machine can no longer report healthy while every attach it accepts lands
in a mode the user did not choose.
- The adapter seam is unchanged. Attach still reads the mode off the CA and
still cannot see config; the comparison between the two lives in the CLI
caller, which can see both.
- The darwin half (keychain trust, the real dialog) is unprovable on Linux, so
a release touching this path wants a `docs/ACCEPTANCE.md` pass on a real Mac.
Loading
Loading