Type: Decision
Status: Accepted
Systems: Config, Daemon, Onboarding, Sources
Author: Phil / Claude
Date: 2026-06-26
Related: LLP 0011, LLP 0016, LLP 0025, LLP 0031, LLP 0036, LLP 0037, LLP 0041
Designed-by: LLP 0045, client attach implementation design
Extended-by: LLP 0086, attach tracks the gateway's ephemeral port (the "attach once, done forever" model becomes endpoint-aware: re-attach on a daemon rebind, and manual hyp attach discovers the live port); LLP 0229 (#status-derives-by-the-same-gate: the §Status surface n/a case is widened to a client desired() would never name, because pending is a claim the reconciler will act and must be derived by desired()'s own gate)
Extended-by: LLP 0234, decryption follows the routing table and recording follows the path anchor (#context's "the gateway records only traffic a client actually routes to it" is narrowed to "only traffic a registered upstream's path anchor claims" once proxy-mode attach means the transport no longer enforces the aperture; the rows produced are unchanged)
When a machine joins a fleet the central config pulls and the gateway binds, but nothing is captured until someone runs
hyp attach claude/hyp attach codexby hand. Daemon healthy, gateway bound, nothing recorded: the silent-gap failure #126 names. This document specifies the reversible instance of the LLP 0036 action seam: when a joined machine confirms a central config that enables a client adapter, the daemon performs that client's attach machine-effect (editing~/.claude/settings.json/~/.codex/config.tomlto route through the local gateway) and reverses it on leave/detach. It resolves the attach-specific open questions LLP 0036 and LLP 0041 deferred (consent, conflict, opt-out) and is the sibling of LLP 0037 (backfill on join, the run-once instance).
hyp attach does two things (LLP 0016): the
config-layer half (the @hypaware/claude plugin entry) is already
fleet-expressible and applied by the engine; the machine-effect half,
editing a user-owned client file so the agent routes to the gateway, the apply
engine deliberately never touches (LLP 0025 §apply engine is kernel surface).
That second half is exactly what the LLP 0036
action reconciler exists to carry.
Attach is the reconciled / reversible flavour the seam was built for but had
not yet exercised: the marker records the currently-applied state, perform()
attaches when the config names a client, and reverse() detaches when it stops
naming it (config drop or hyp leave). It reuses the same client.attach()
machine-effect the manual command calls: auto-attach is a new caller of the
adapter, not a new effect.
This decision settles the three attach-specific questions LLP 0036/0041 left
open. All three follow the grain already set by backfill, so attach rides the
existing plugins[] surface and locking with no new merge rule.
- Live capture is opt-in per client. The gateway records only traffic a
client actually routes to it. Routing is set by the client's native settings
file (
ANTHROPIC_BASE_URLfor Claude,[model_providers.hypaware]for Codex), which onlyhyp attachwrites (LLP 0012 Sources). - Join wires config, not client files. The non-interactive
joinpath (LLP 0011 §Non-interactive entry) applies the central config and starts the gateway, but the apply engine never edits a user-owned file. Result: a healthy, forwarding gateway that captures nothing because no client points at it. - The seam already supports a reversible handler. The
LLP 0041 reconciler calls
reverse(requestKey)for any marker whose keydesired()no longer names, and the marker file already reserves a per-kindnamespace (LLP 0041 §Idempotency). Attach is a secondActionHandler(kind: 'attach'), registered besidebackfillHandler; the reconciler core is unchanged.
Add an attach action handler: the reversible instance of the LLP 0036 seam.
Attach rides the client adapter's own plugin entry (the entry the config
already names, #126), so the central-vs-local locking
(LLP 0031 §Merge model) falls
out: a central-named @hypaware/claude entry is authoritative; a colliding
local entry is dropped at the boot merge. There is no generic actions[]
schema and no top-level attach section: consistent with
LLP 0036 §Where actions are declared.
attach.on_join (default true) is validated by the owning client
plugin's config-section validator (LLP 0005),
the same path that validates backfill. Core validates nothing new.
The attach handler enumerates every registered gateway client whose owning
plugin is enabled in the effective config, minus those whose plugin entry set
attach.on_join: false. The marker request key is the client name
(claude, codex). On a non-joined host there is no central layer, the
reconciler never runs, and attach stays the manual hyp attach command.
Attach is in-process (a bounded settings-file edit, unlike backfill's unbounded subprocess import: LLP 0041 §Execution isolation):
perform()calls the gateway client'sattach({ endpoint, … }): the exact effecthyp attachinvokes, in JSON mode, recording the adapter's reportedsettings_pathand prior base URL (prev_value) into the marker.reverse(client)calls the client'sdetach(…)whendesired()stops naming it (the config dropped the client, orhyp leavecleared the central layer). The adapter restores the backed-up prior base URL (below).
A done marker short-circuits re-attach on every subsequent pass: the effect
is already in place; the reconciler does not re-edit the file each tick.
A joined machine self-attaches with no prompt when the central config names a client. Rationale:
- It is the issue's acceptance criterion, "becomes attached with no manual
step" (#126), and reaches
parity with the interactive
initfinale, which already attaches every picked client (LLP 0011). - The blast radius is narrow and fully reversible: the effect edits the
user's own client file to route to the user's own local gateway on the
machine that already joined, and
reverse()restores the prior state on leave. This is the same consent footing backfill stands on (LLP 0036 §Consent). - The operator retains an off switch,
attach.on_join: falsein the locked central plugin entry, so a fleet that wants the gateway up but clients attached by hand can have it. Consent is therefore operator-scoped, not a per-invocation prompt.
When a machine already has a base URL pointing somewhere that is not the
gateway (a deliberate third-party proxy), auto-attach records the prior value
and overrides, then restores the original on detach/leave. The backup
lives in the adapter's own marker (_hypaware.prev_base_url for Claude; the
Codex marked-block metadata), so restoration is the adapter's job: the
reconciler stays client-agnostic. This makes both the auto path and the
manual hyp detach fully round-trip; previously detach removed the gateway
URL only when it still matched and never restored a pre-existing one. Chosen over
skip-and-warn because the feature's whole point is zero-touch capture, and over
override-without-backup because clobbering a deliberate setting with no undo is
not reversible: the seam's core promise.
A machine cannot locally suppress an attach the central config mandates.
Attach policy lives in the plugins[] entry, which is locked under
LLP 0031 §Merge model; a
colliding local entry is dropped, exactly as for backfill
(LLP 0037 §No local opt-out)
and the central sink. If a particular machine should not attach, that is an
operator scoping decision (attach.on_join: false, or a different
config/token), not a local flag. This rides the existing locking with no new
merge rule.
Identical to backfill's reconcile timing
(LLP 0041 §When the reconciler runs):
fired on the confirmPoll probation active→cleared edge and on the
after-activation already-confirmed pass, never mid-apply. A failed attach
(file not writable, malformed settings) writes a failed marker (reason +
attempt count), is retried on the next pass, and does not roll back the
central config or flip overall to degraded
(LLP 0041 §Failure is surfaced, not fatal).
hyp status reports attach under the
LLP 0041
client_action section, per client: done (attached, with when) / failed
(reason + last attempt) / pending (named, not yet attached) / n/a
(on_join: false, non-joined, or a client desired() would never name: see
LLP 0229 #status-derives-by-the-same-gate).
A failed or pending attach does not make overall degraded.
pending is a claim that the reconciler will act, so status must derive it
by desired()'s own rule, not a looser one. Where the two can drift, they
share a reader: readAttachPolicy for the on_join tri-state, and the
attachProbe presence check for attach-eligibility.
hyp attach / hyp detach are untouched as commands. Auto-attach is a new
caller of the same adapter attach()/detach(), parameterised by the central
config instead of argv. The one change to the adapters, recording and restoring
the prior base URL, is a strict improvement that the manual hyp detach
inherits too: a manual round-trip now restores a pre-existing base URL it
previously left in place with a warning.
- LLP 0031's deferred attach open question is fully resolved (not just "in principle" as LLP 0036 left it): consent, conflict, and opt-out are settled here; the implementation is designed in LLP 0045.
- The reconciler's
reverse()path is exercised for the first time. Backfill omits it (imported data stays); attach is the reversible handler the seam was designed around (LLP 0041 §Undo on leave). - The reconcile context gains a client seam. The handler needs the gateway's
client registry and endpoint, which only the daemon has live: keeping attach
daemon-only by construction (a plain CLI boot has no gateway capability and no
configControl, sohyp statusperforms no machine effect). - Join does more. A joined machine self-attaches its clients (subject to the operator off switch): an onboarding-surface change from LLP 0011's manual-attach finale, reached without editing that record.
- Marker vs actual-file drift. v1 keys idempotency on the marker, not the
live settings file: if a user manually strips the gateway from their settings,
the
donemarker means the reconciler will not re-attach until the config changes. A later refinement could havedesired()re-detect actual attach state (theattach_probedescriptor already powershyp status) and re-apply on drift. v1 accepts the simpler marker-only model, matching backfill. - Ordering vs first ingest. Attach (start live routing) and backfill (import history) run in the same post-confirm pass. The cache→forward path is order-insensitive (both just land rows the forward sink drains), so no ordering is imposed; revisit only if a dependency surfaces (LLP 0036 open questions).
- Codex prior-provider nuance. Codex routing is a
model_providerpointer plus a marked provider block; "restore the prior value" means restoring the priormodel_provider, not a URL. The adapter owns this asymmetry; confirm the round-trip in the adapter tests.
- LLP 0036: the action seam (the decision this instantiates)
- LLP 0037: backfill on join (the run-once sibling instance)
- LLP 0041: client-actions implementation design (the reconciler this extends)
- LLP 0045: client attach implementation design (the design realizing this decision)
- LLP 0011: setup and onboarding (the attach finale this reaches parity with)
- LLP 0016: AI gateway / client adapters (
registerClient,attach/detach) - LLP 0025: join flow, apply, probation (the confirmation trigger)
- LLP 0031: layered config / merge model (plugin-entry locking; the deferred attach question)
- LLP 0005: plugin manifest / config_sections (per-plugin
attachvalidation) - #126, config-driven client attach
{ "name": "@hypaware/claude", "config": { "proxy": "@hypaware/ai-gateway", "attach": { "on_join": true } } }