@haven_ai/connect 0.3.0-alpha.0 → 0.5.0-alpha.1

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
@@ -133,7 +159,9 @@ minutes. Two rules follow:
133
159
  This replaces the directory's signer wrapper with a diagnostic that logs the
134
160
  retirement (agent id, date, reason, restart guidance) to the host's MCP
135
161
  stderr log on every probe, and records it in `TOMBSTONE.json` for
136
- `--doctor`. It touches no key material and revokes nothing — revoke the
162
+ `--doctor` — with a copy in `~/.haven/tombstones/` (or `.tombstones/` inside
163
+ a custom credentials root), so the retirement stays visible after the
164
+ directory is deleted. It touches no key material and revokes nothing — revoke the
137
165
  agent on the Haven agent page yourself. Delete the tombstone only once every
138
166
  long-lived host has been restarted.
139
167
 
@@ -147,7 +175,12 @@ minutes. Two rules follow:
147
175
  and a refusal is `{"tombstoned": false, "error": {"code", "next_action"}}`
148
176
  with exit 1 — so check the result rather than assuming silence means success
149
177
  (#2175). The `message` field is present only for connector-authored refusals;
150
- an unexpected filesystem error keeps its raw text on stderr alone.
178
+ an unexpected filesystem error keeps its raw text on stderr alone. If only the
179
+ surviving copy fails — the directory IS tombstoned — the run still succeeds
180
+ with `"recordPath": null` and `"mirrorError"` set to the errno code (or
181
+ `mirror_write_failed` when there is none), and `--unwire --json` carries the
182
+ same as `mirror_error`; `--replace` still removes the old directory's key
183
+ files and names the failure in `retirement_mirror_errors` (#3259).
151
184
 
152
185
  ### Unwiring an agent (`--unwire`, #2169)
153
186
 
@@ -165,13 +198,80 @@ npx @haven_ai/connect@<channel> --unwire --name research [--reason "..."]
165
198
  touches local files only, so any published connector does the same job.
166
199
 
167
200
  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
201
+ never a masked `ENOENT`), releases the directory's local server-name binding
202
+ record (#3122, whatever the key-material decision below), then removes THAT
203
+ agent's hosted + signer pair from
169
204
  every runtime config it appears in (Hermes YAML, Codex TOML, the Cursor / VS
170
205
  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.
206
+ line — bare `MCP_HAVEN_API_KEY` or named `MCP_HAVEN_<SLUG>_API_KEY`. Then it
207
+ decides about the target directory's local key material (signer key, any
208
+ abandoned re-key, the stored API key) — and since #3123 it **asks before it
209
+ destroys**.
210
+
211
+ #### Teardown refuses before destroying the recovery credential (#3123)
212
+
213
+ A revoked agent's API key and delegate signature are exactly what the
214
+ sweep-recovery routes still accept — they are the only local means of
215
+ recovering a stranded delegate balance. So after the wiring is removed,
216
+ `--unwire` runs the one read it has (the identity probe, `GET
217
+ /machine-payments/agent` with the stored key — no new network call, no
218
+ backend change) and **refuses to destroy the key material on every answer**:
219
+
220
+ | Probe | What it means | What `--unwire` does |
221
+ | --- | --- | --- |
222
+ | `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. |
223
+ | `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). |
224
+ | `network_error` / `bad_response` | Could not verify. | Refuses: unknown is not "safe to delete". Retry. |
225
+ | *(no stored API key + URL)* | Nothing the recovery routes would accept. | Proceeds, unprobed — the pre-#3123 shape. |
226
+
227
+ The refusal is exit 1 with the wiring already gone; the key stays in the
228
+ 0o600 credential file only (the config and Hermes-env copies are scrubbed
229
+ first, so a refusal leaves the key in the credential file and in any config
230
+ this run could not clean — those are reported `✗` above, never silently),
231
+ and `--doctor` keeps reporting the directory as `superseded` until the key is
232
+ revoked or destroyed — that is the honest state. **`--destroy-key-material`**
233
+ proceeds on every answer, states what it destroyed and that local recovery
234
+ of a stranded balance ends with it. In `--json` the record carries an
235
+ additive `teardown: { status: destroyed | retained | forced, probe, detail,
236
+ remedy? }` and, since #3122, `binding_released` (whether a local
237
+ `mcp-server-binding.json` was removed).
238
+
239
+ The `claude-code` copy of the key (written by `claude mcp add`, into a config
240
+ the connector does not own) is out of scope for `--unwire`, as it always was:
241
+ remove it with `claude mcp remove haven` / `haven-signer` (or the named
242
+ pair) yourself; `--doctor --runtime claude-code` cannot see it either.
243
+
244
+ ### Pruning signer-runtime directories (`--prune-signer-runtimes`, #3123)
245
+
246
+ Every pinned version — and, since #2424, every `HAVEN_*_SPEC` override — gets
247
+ its own `~/.haven/signer-runtime/<key>/`, and nothing reclaimed them:
248
+
249
+ ```
250
+ npx @haven_ai/connect@<channel> --prune-signer-runtimes --dry-run
251
+ npx @haven_ai/connect@<channel> --prune-signer-runtimes
252
+ ```
253
+
254
+ It walks the root (so override-keyed `override-<hash>` directories are seen,
255
+ not only manifest versions), **keeps** every directory any credential
256
+ directory names — through its `signer-runtime.json` or through the path its
257
+ wrapper launches — wired, superseded or retired; the prune never decides who
258
+ is live — and the connector's current pin (what
259
+ `--repair` installs), and removes the rest, reporting each with a #3121
260
+ level: kept/removed are `ok`, a dry-run candidate is an `advisory`, a removal
261
+ that failed (a signer process still holding the directory open on a platform
262
+ that refuses the unlink) is `failed` and the only thing that exits 1. On
263
+ POSIX a running signer keeps its open files until it restarts, and since
264
+ only directories no sidecar and no wrapper names go, a configured agent is
265
+ not started against a removed one. The prune trusts those two reference
266
+ sources (the sidecar's `runtime_directory` and the path the wrapper
267
+ launches — so a directory whose sidecar is missing or corrupt but whose
268
+ wrapper is intact keeps its runtime); it reads the default agents root AND
269
+ the parent of an explicit `--credentials-dir`, a union, never either/or. It
270
+ is its own flag — never part of `--repair`, never automatic; `--doctor`
271
+ reports unused directories as an advisory (`signer_runtime_unused`) that
272
+ names this command — names only; sizes come from `--dry-run`, and a kept
273
+ directory is never sized (its row says "not sized"). `--json` emits
274
+ `{ pruned: true, version: 1, root, dry_run, level, removed, reclaimed_bytes, entries[] }`.
175
275
 
176
276
  An **unnamed** pair (`haven` / `haven-signer`) is shared by every unnamed agent
177
277
  and is only removed when this directory's wrapper is the one the config
@@ -181,6 +281,67 @@ revoked on the backend; `connect reports, the user decides` (#1688) survives,
181
281
  and revocation stays an owner action on the Haven agent page. Restart every
182
282
  long-lived host afterwards, as with any retirement.
183
283
 
284
+ ### The wallet warning arrives before the write; names are bound locally (#3122)
285
+
286
+ Setup guards the **name slot** (a taken `--name`, the bare pair already wired
287
+ — #2551 refuses or asks) but never the **wallet**: a machine can carry several
288
+ agents with live keys, and until #3122 the "your previous agent(s) still exist
289
+ with their own keys" heads-up was printed only after the credentials were
290
+ written (that completion heads-up, #1688, is unchanged — this slice ADDS an
291
+ earlier notice). Two things changed, both from local files only (no network call is
292
+ added; the backend is not asked whether any key still authenticates):
293
+
294
+ - **Before the key is minted or anything is written**, setup names every
295
+ other credential directory that still holds a stored key, with the account
296
+ it can spend from — `Heads-up (before anything is written): this machine
297
+ already carries N agent director(y/ies) with stored keys — <id> (spends
298
+ from 0x…)`. It **warns, it does not refuse** (owner decision on #3119): a
299
+ refusal would change behaviour for every non-interactive caller, including
300
+ the dashboard's connect flow. `--json` carries the same list as
301
+ `existing_agents_before_write: [{ agent_id, account_address }]`, always
302
+ present on a completed run (`[]` on a clean machine) — a subset of
303
+ `superseded_agent_ids`, which names every other directory that has an
304
+ `identity.json` at all, key-less and tombstoned ones included.
305
+ "Holds a stored key" is the whole test: a tombstoned directory whose key
306
+ files were never deleted is counted (it can still spend), which is the
307
+ opposite side of the doctor's rule that ignores a retired directory's
308
+ binding record (it launches nothing). The scan covers exactly the credential
309
+ root this run writes into — `~/.haven/agents`, or the root given by
310
+ `--credentials-dir` — and never unions the two (#2551's collision check uses
311
+ the same root; `--doctor` shares the no-union rule but reads its
312
+ `--credentials-dir` as the agent directory itself, so its root is that
313
+ directory's parent; #3123's union was for the one shared `signer-runtime`
314
+ root).
315
+ - **Each setup records what it bound** in a non-secret
316
+ `mcp-server-binding.json` beside `last-connect-outcome.json`: `{ version:
317
+ 1, server_name, signer_name, agent_id, api_url, hosted_mcp_url?, bound_at }`
318
+ — no key material, by construction (pinned by test). Per credential
319
+ directory, not machine-wide: two concurrent setups never write the same
320
+ file, and "who else claims this name" is answered by scanning the root,
321
+ which setup and the doctor already do. When a setup is about to bind a
322
+ server name another directory's record holds, it says so before the write —
323
+ previous agent (the NEWEST holder when several records claim the name),
324
+ previous backend URL, when — and flags a **DIFFERENT backend** explicitly, because that is the case the backend cannot see and
325
+ the one that silently repoints a saved session; `--json` carries it as
326
+ `server_name_rebound_from: { server_name, agent_id, api_url, bound_at,
327
+ backend_changed }`. `--unwire` releases the record (its `--json` gains
328
+ `binding_released`), and so does a `--replace` retirement, so a legitimately
329
+ free name does not warn forever; `--tombstone` leaves it, and `--doctor`
330
+ ignores a retired directory's record. `--doctor` reports two records
331
+ claiming one name — excluding tombstoned (retired) directories — as the
332
+ `mcp_server_name_rebound` advisory (oldest → newest, backend change
333
+ flagged). The record is written
334
+ after the credentials, best-effort: a directory with credentials and no
335
+ record (a crash in between, or one retired by hand) is not proof the name
336
+ is free — the absence of a warning is not a guarantee, as with
337
+ `superseded_agent_ids`.
338
+
339
+ **What wins when the local record disagrees with the backend.** The backend's
340
+ `agents.mcp_server_name` is the authority for the same backend; the local
341
+ record is a reporting aid that covers the two cases the backend cannot —
342
+ another backend, or offline — and it never asserts itself as the backend's
343
+ current truth. Every message that reads it says "locally recorded".
344
+
184
345
  ### Structured output for automation
185
346
 
186
347
  Pass `--json` when a launcher needs a machine-readable completion record. Connect
@@ -192,9 +353,13 @@ whenever ready and verify later with the read-only `haven_get_agent`
192
353
  tool. The object includes runtime/topology status,
193
354
  probe result, activation and next-action guidance, approval state/expiry (null
194
355
  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
356
+ read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
357
+ `existing_agents_before_write` (#3122 — the other live-keyed directories named
358
+ before the first write, each with the account it spends from), and — on a run
359
+ that replaced existing wiring — `superseded_agents_retired_locally` with
360
+ `retired_agent_ids`, plus `retirement_mirror_errors` (#3259) only when a retired
361
+ directory's surviving ledger record could not be written; `server_name_rebound_from` (#3122) appears only when the
362
+ run took a server name over from another directory's local binding record. It
198
363
  contains no API key, private key, credential
199
364
  contents, full credential paths, or full delegate address. The same redacted
200
365
  object is available to library callers as `runConnect(...).outcome`; the older
@@ -361,9 +526,11 @@ already-configured machine behaves as follows (characterized in
361
526
  managed Codex/Hermes equivalents) and re-points them at the new agent's
362
527
  credentials; unrelated MCP servers and configuration are preserved. Once
363
528
  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
529
+ tombstoned and its local key files removed — the unconditional key-material
530
+ teardown; `--unwire` itself runs that teardown only when its #3123 probe says
531
+ there is nothing to preserve, and `--replace` deliberately does not probe
532
+ (the owner has just chosen to overwrite; see the #3119 follow-up) — so
533
+ `--doctor` reads it as `retired` rather than still spend-capable. If the install ends with an error code the retirement is
367
534
  **skipped**, because the old wiring may still be the only working one; the
368
535
  outcome's `superseded_agents_retired_locally` says which happened and
369
536
  `retired_agent_ids` names exactly the directories it reached. With
@@ -481,6 +648,7 @@ across the rest is yours.
481
648
  ## Diagnosing a stuck setup: `--doctor` / `--repair` (#1589)
482
649
 
483
650
  ```bash
651
+ npx @haven_ai/connect@<channel> --doctor
484
652
  npx @haven_ai/connect@<channel> --doctor --runtime codex-desktop
485
653
  npx @haven_ai/connect@<channel> --doctor --repair --runtime codex-desktop
486
654
  ```
@@ -490,15 +658,25 @@ not indifferent: `signer_runtime` compares the sidecar against the manifest of
490
658
  the connector **that runs the check**, so a doctor from another channel reports
491
659
  a skew that is not there. Use the channel your dashboard hands out.
492
660
 
661
+ `--runtime` is optional for `--doctor` and required for `--repair` (#3210).
662
+ Without the flag the doctor checks the runtime the setup recorded in the
663
+ agent directory's `last-connect-outcome.json` (#3120), and the `runtime_config`
664
+ verdict says which runtime it resolved and from which file; when no
665
+ record names one, the `runtime_config` check FAILS with "Runtime is unknown —
666
+ the runtime config was NOT checked" and the exit code is 1 — never a pass on a
667
+ config it did not open. `--repair` rewrites that config, so the runtime it
668
+ rewrites is named on the command line rather than inherited from a record on
669
+ disk: a flagless `--repair` is refused before anything runs.
670
+
493
671
  `--doctor` is read-only and needs NO setup token: it checks the runtime config,
494
672
  the agent credential files, the pinned signer runtime install (and, since
495
673
  #2424, whether that install was made under a local runtime-spec override —
496
674
  see the last section of this file), the hosted MCP
497
675
  (authorized `tools/list`), and starts the local signer for a real stdio
498
676
  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.
677
+ prints one concrete repair action; the exit code is non-zero on any failure
678
+ and zero on an advisory (see "Three verdicts, not two" above). Add `--json`
679
+ for a machine-readable report. No secret material is ever printed.
502
680
 
503
681
  `--doctor` also probes every OTHER agent credential directory it did not
504
682
  select (#1688). A re-run of setup mints a NEW agent and, unless it ran with