@haven_ai/connect 0.2.1-alpha.0 → 0.4.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,13 +87,39 @@ every credential directory on the machine and classifies each one:
87
87
  | `retired` | Tombstoned (see below); key material removed. |
88
88
  | `orphaned` | No usable identity and no tombstone. |
89
89
 
90
- The exit code is non-zero if **any** wired agent fails **any** check — not
90
+ The exit code is non-zero if **any** wired agent fails **any** check
91
+ (`level: failed` — see the next section; an advisory does not count) — not
91
92
  only the one the report's main section describes. In `--json`, the same
92
93
  information is on `agents[]`, each entry carrying `slug`, `agentId`,
93
94
  `directory`, `classification` and its own `checks[]`; the flat `checks` array
94
95
  is retained and still describes one agent, so a single-agent install reads as
95
96
  it always did.
96
97
 
98
+ ### Three verdicts, not two (#3121)
99
+
100
+ Every check carries a `level` — `ok`, `advisory` or `failed` — and the report
101
+ carries the rolled-up `level` over the flat checks and every wired agent's
102
+ checks. Only `failed` reaches the exit code; an advisory is printed with a `!`
103
+ marker, gets its own summary line ("No failures. N advisory finding(s)") and
104
+ exits 0. `ok` on a check and on the report is kept for `--json` consumers and
105
+ means "nothing is broken": it is `true` for `ok` and `advisory`, `false` only
106
+ for `failed`, so `report.ok` is always the exit code's predicate. The doctor
107
+ report stays `version: 1`; `level` is additive.
108
+
109
+ | Level | Meaning | Example |
110
+ | --- | --- | --- |
111
+ | `ok` | Nothing to say. | The installed signer matches the connector's pin. |
112
+ | `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered); `signer_runtime_unused` (#3123): runtime directories nothing references; `mcp_server_name_rebound` (#3122): two local binding records claim one server name. `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
113
+ | `failed` | A real failure with one repair action. Exit 1. | A stale or empty runtime directory; a live key in a directory the runtime's config demonstrably does not use. |
114
+
115
+ What stays blocking is live spend authority: a directory whose stored key
116
+ still authenticates and whose classification is not `wired` fails the run, for
117
+ every non-wired classification, whenever the runtime config could be read. The
118
+ classification itself does not change on Claude Code — only the severity of
119
+ the check that reads it — so `agents[]` reads as before. A runtime string the
120
+ connector does not recognise (`--runtime codex-clii`) is not "no config": it
121
+ fails `runtime_config` naming the allowed values, and demotes nothing.
122
+
97
123
  One check is worth calling out: **`identity_match`** compares the agent the
98
124
  stored API key actually authenticates as against the `delegate_address` in
99
125
  that directory's `signer.json`. A mismatch means the runtime would quote as
@@ -165,13 +191,80 @@ npx @haven_ai/connect@<channel> --unwire --name research [--reason "..."]
165
191
  touches local files only, so any published connector does the same job.
166
192
 
167
193
  It tombstone-first (so a stale long-lived host still hears `HAVEN-TOMBSTONE`,
168
- never a masked `ENOENT`), then removes THAT agent's hosted + signer pair from
194
+ never a masked `ENOENT`), releases the directory's local server-name binding
195
+ record (#3122, whatever the key-material decision below), then removes THAT
196
+ agent's hosted + signer pair from
169
197
  every runtime config it appears in (Hermes YAML, Codex TOML, the Cursor / VS
170
198
  Code / Insiders / Claude Desktop JSON configs), plus the Hermes dotenv API-key
171
- line — bare `MCP_HAVEN_API_KEY` or named `MCP_HAVEN_<SLUG>_API_KEY`. Finally it
172
- tears down the target directory's local key material (signer key, any abandoned
173
- re-key, the stored API key) so `--doctor` reports `retired`, not the
174
- still-spend-capable `superseded`; the #2155 tombstone mirror keeps the record.
199
+ line — bare `MCP_HAVEN_API_KEY` or named `MCP_HAVEN_<SLUG>_API_KEY`. Then it
200
+ decides about the target directory's local key material (signer key, any
201
+ abandoned re-key, the stored API key) — and since #3123 it **asks before it
202
+ destroys**.
203
+
204
+ #### Teardown refuses before destroying the recovery credential (#3123)
205
+
206
+ A revoked agent's API key and delegate signature are exactly what the
207
+ sweep-recovery routes still accept — they are the only local means of
208
+ recovering a stranded delegate balance. So after the wiring is removed,
209
+ `--unwire` runs the one read it has (the identity probe, `GET
210
+ /machine-payments/agent` with the stored key — no new network call, no
211
+ backend change) and **refuses to destroy the key material on every answer**:
212
+
213
+ | Probe | What it means | What `--unwire` does |
214
+ | --- | --- | --- |
215
+ | `ok` | The agent is still active: its key still spends. | Refuses; tells you to revoke on the Haven agent page (connect never revokes), then re-run. |
216
+ | `unauthorized` | The key no longer authenticates on normal routes (revoked, archived, paused, pending approval, rotated, or not a key the backend knows — it does not say which). | Refuses; says plainly that a stranded balance **may** exist and the connector **cannot check**; recover first (`haven_sweep_delegate`, or the agent page). |
217
+ | `network_error` / `bad_response` | Could not verify. | Refuses: unknown is not "safe to delete". Retry. |
218
+ | *(no stored API key + URL)* | Nothing the recovery routes would accept. | Proceeds, unprobed — the pre-#3123 shape. |
219
+
220
+ The refusal is exit 1 with the wiring already gone; the key stays in the
221
+ 0o600 credential file only (the config and Hermes-env copies are scrubbed
222
+ first, so a refusal leaves the key in the credential file and in any config
223
+ this run could not clean — those are reported `✗` above, never silently),
224
+ and `--doctor` keeps reporting the directory as `superseded` until the key is
225
+ revoked or destroyed — that is the honest state. **`--destroy-key-material`**
226
+ proceeds on every answer, states what it destroyed and that local recovery
227
+ of a stranded balance ends with it. In `--json` the record carries an
228
+ additive `teardown: { status: destroyed | retained | forced, probe, detail,
229
+ remedy? }` and, since #3122, `binding_released` (whether a local
230
+ `mcp-server-binding.json` was removed).
231
+
232
+ The `claude-code` copy of the key (written by `claude mcp add`, into a config
233
+ the connector does not own) is out of scope for `--unwire`, as it always was:
234
+ remove it with `claude mcp remove haven` / `haven-signer` (or the named
235
+ pair) yourself; `--doctor --runtime claude-code` cannot see it either.
236
+
237
+ ### Pruning signer-runtime directories (`--prune-signer-runtimes`, #3123)
238
+
239
+ Every pinned version — and, since #2424, every `HAVEN_*_SPEC` override — gets
240
+ its own `~/.haven/signer-runtime/<key>/`, and nothing reclaimed them:
241
+
242
+ ```
243
+ npx @haven_ai/connect@<channel> --prune-signer-runtimes --dry-run
244
+ npx @haven_ai/connect@<channel> --prune-signer-runtimes
245
+ ```
246
+
247
+ It walks the root (so override-keyed `override-<hash>` directories are seen,
248
+ not only manifest versions), **keeps** every directory any credential
249
+ directory names — through its `signer-runtime.json` or through the path its
250
+ wrapper launches — wired, superseded or retired; the prune never decides who
251
+ is live — and the connector's current pin (what
252
+ `--repair` installs), and removes the rest, reporting each with a #3121
253
+ level: kept/removed are `ok`, a dry-run candidate is an `advisory`, a removal
254
+ that failed (a signer process still holding the directory open on a platform
255
+ that refuses the unlink) is `failed` and the only thing that exits 1. On
256
+ POSIX a running signer keeps its open files until it restarts, and since
257
+ only directories no sidecar and no wrapper names go, a configured agent is
258
+ not started against a removed one. The prune trusts those two reference
259
+ sources (the sidecar's `runtime_directory` and the path the wrapper
260
+ launches — so a directory whose sidecar is missing or corrupt but whose
261
+ wrapper is intact keeps its runtime); it reads the default agents root AND
262
+ the parent of an explicit `--credentials-dir`, a union, never either/or. It
263
+ is its own flag — never part of `--repair`, never automatic; `--doctor`
264
+ reports unused directories as an advisory (`signer_runtime_unused`) that
265
+ names this command — names only; sizes come from `--dry-run`, and a kept
266
+ directory is never sized (its row says "not sized"). `--json` emits
267
+ `{ pruned: true, version: 1, root, dry_run, level, removed, reclaimed_bytes, entries[] }`.
175
268
 
176
269
  An **unnamed** pair (`haven` / `haven-signer`) is shared by every unnamed agent
177
270
  and is only removed when this directory's wrapper is the one the config
@@ -181,6 +274,67 @@ revoked on the backend; `connect reports, the user decides` (#1688) survives,
181
274
  and revocation stays an owner action on the Haven agent page. Restart every
182
275
  long-lived host afterwards, as with any retirement.
183
276
 
277
+ ### The wallet warning arrives before the write; names are bound locally (#3122)
278
+
279
+ Setup guards the **name slot** (a taken `--name`, the bare pair already wired
280
+ — #2551 refuses or asks) but never the **wallet**: a machine can carry several
281
+ agents with live keys, and until #3122 the "your previous agent(s) still exist
282
+ with their own keys" heads-up was printed only after the credentials were
283
+ written (that completion heads-up, #1688, is unchanged — this slice ADDS an
284
+ earlier notice). Two things changed, both from local files only (no network call is
285
+ added; the backend is not asked whether any key still authenticates):
286
+
287
+ - **Before the key is minted or anything is written**, setup names every
288
+ other credential directory that still holds a stored key, with the account
289
+ it can spend from — `Heads-up (before anything is written): this machine
290
+ already carries N agent director(y/ies) with stored keys — <id> (spends
291
+ from 0x…)`. It **warns, it does not refuse** (owner decision on #3119): a
292
+ refusal would change behaviour for every non-interactive caller, including
293
+ the dashboard's connect flow. `--json` carries the same list as
294
+ `existing_agents_before_write: [{ agent_id, account_address }]`, always
295
+ present on a completed run (`[]` on a clean machine) — a subset of
296
+ `superseded_agent_ids`, which names every other directory that has an
297
+ `identity.json` at all, key-less and tombstoned ones included.
298
+ "Holds a stored key" is the whole test: a tombstoned directory whose key
299
+ files were never deleted is counted (it can still spend), which is the
300
+ opposite side of the doctor's rule that ignores a retired directory's
301
+ binding record (it launches nothing). The scan covers exactly the credential
302
+ root this run writes into — `~/.haven/agents`, or the root given by
303
+ `--credentials-dir` — and never unions the two (#2551's collision check uses
304
+ the same root; `--doctor` shares the no-union rule but reads its
305
+ `--credentials-dir` as the agent directory itself, so its root is that
306
+ directory's parent; #3123's union was for the one shared `signer-runtime`
307
+ root).
308
+ - **Each setup records what it bound** in a non-secret
309
+ `mcp-server-binding.json` beside `last-connect-outcome.json`: `{ version:
310
+ 1, server_name, signer_name, agent_id, api_url, hosted_mcp_url?, bound_at }`
311
+ — no key material, by construction (pinned by test). Per credential
312
+ directory, not machine-wide: two concurrent setups never write the same
313
+ file, and "who else claims this name" is answered by scanning the root,
314
+ which setup and the doctor already do. When a setup is about to bind a
315
+ server name another directory's record holds, it says so before the write —
316
+ previous agent (the NEWEST holder when several records claim the name),
317
+ previous backend URL, when — and flags a **DIFFERENT backend** explicitly, because that is the case the backend cannot see and
318
+ the one that silently repoints a saved session; `--json` carries it as
319
+ `server_name_rebound_from: { server_name, agent_id, api_url, bound_at,
320
+ backend_changed }`. `--unwire` releases the record (its `--json` gains
321
+ `binding_released`), and so does a `--replace` retirement, so a legitimately
322
+ free name does not warn forever; `--tombstone` leaves it, and `--doctor`
323
+ ignores a retired directory's record. `--doctor` reports two records
324
+ claiming one name — excluding tombstoned (retired) directories — as the
325
+ `mcp_server_name_rebound` advisory (oldest → newest, backend change
326
+ flagged). The record is written
327
+ after the credentials, best-effort: a directory with credentials and no
328
+ record (a crash in between, or one retired by hand) is not proof the name
329
+ is free — the absence of a warning is not a guarantee, as with
330
+ `superseded_agent_ids`.
331
+
332
+ **What wins when the local record disagrees with the backend.** The backend's
333
+ `agents.mcp_server_name` is the authority for the same backend; the local
334
+ record is a reporting aid that covers the two cases the backend cannot —
335
+ another backend, or offline — and it never asserts itself as the backend's
336
+ current truth. Every message that reads it says "locally recorded".
337
+
184
338
  ### Structured output for automation
185
339
 
186
340
  Pass `--json` when a launcher needs a machine-readable completion record. Connect
@@ -192,9 +346,12 @@ whenever ready and verify later with the read-only `haven_get_agent`
192
346
  tool. The object includes runtime/topology status,
193
347
  probe result, activation and next-action guidance, approval state/expiry (null
194
348
  when the backend does not provide an approval expiry) and `approval.url`, the two
195
- read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`, and —
196
- on a run that replaced existing wiring — `superseded_agents_retired_locally`
197
- with `retired_agent_ids`. It
349
+ read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
350
+ `existing_agents_before_write` (#3122 — the other live-keyed directories named
351
+ before the first write, each with the account it spends from), and — on a run
352
+ that replaced existing wiring — `superseded_agents_retired_locally` with
353
+ `retired_agent_ids`; `server_name_rebound_from` (#3122) appears only when the
354
+ run took a server name over from another directory's local binding record. It
198
355
  contains no API key, private key, credential
199
356
  contents, full credential paths, or full delegate address. The same redacted
200
357
  object is available to library callers as `runConnect(...).outcome`; the older
@@ -361,9 +518,11 @@ already-configured machine behaves as follows (characterized in
361
518
  managed Codex/Hermes equivalents) and re-points them at the new agent's
362
519
  credentials; unrelated MCP servers and configuration are preserved. Once
363
520
  the runtime install has actually completed, each superseded directory is
364
- tombstoned and its local key files removed — the same teardown `--unwire`
365
- performs — so `--doctor` reads it as `retired` rather than still
366
- spend-capable. If the install ends with an error code the retirement is
521
+ tombstoned and its local key files removed — the unconditional key-material
522
+ teardown; `--unwire` itself runs that teardown only when its #3123 probe says
523
+ there is nothing to preserve, and `--replace` deliberately does not probe
524
+ (the owner has just chosen to overwrite; see the #3119 follow-up) — so
525
+ `--doctor` reads it as `retired` rather than still spend-capable. If the install ends with an error code the retirement is
367
526
  **skipped**, because the old wiring may still be the only working one; the
368
527
  outcome's `superseded_agents_retired_locally` says which happened and
369
528
  `retired_agent_ids` names exactly the directories it reached. With
@@ -496,9 +655,9 @@ the agent credential files, the pinned signer runtime install (and, since
496
655
  see the last section of this file), the hosted MCP
497
656
  (authorized `tools/list`), and starts the local signer for a real stdio
498
657
  handshake — reporting its advertised compat versions. Every failing check
499
- prints one concrete repair action; the exit code is non-zero on any failure.
500
- Add `--json` for a machine-readable report. No secret material is ever
501
- printed.
658
+ prints one concrete repair action; the exit code is non-zero on any failure
659
+ and zero on an advisory (see "Three verdicts, not two" above). Add `--json`
660
+ for a machine-readable report. No secret material is ever printed.
502
661
 
503
662
  `--doctor` also probes every OTHER agent credential directory it did not
504
663
  select (#1688). A re-run of setup mints a NEW agent and, unless it ran with