@haven_ai/connect 0.0.0-dev.202609181713.990ba70 → 0.0.0-dev.202609181927.cd4620a

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). `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
@@ -496,9 +522,9 @@ the agent credential files, the pinned signer runtime install (and, since
496
522
  see the last section of this file), the hosted MCP
497
523
  (authorized `tools/list`), and starts the local signer for a real stdio
498
524
  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.
525
+ prints one concrete repair action; the exit code is non-zero on any failure
526
+ and zero on an advisory (see "Three verdicts, not two" above). Add `--json`
527
+ for a machine-readable report. No secret material is ever printed.
502
528
 
503
529
  `--doctor` also probes every OTHER agent credential directory it did not
504
530
  select (#1688). A re-run of setup mints a NEW agent and, unless it ran with
package/dist/cli.cjs CHANGED
@@ -533,6 +533,16 @@ async function writeConnectOutcomeRecord(directory, outcome, warn) {
533
533
  await restrictPermissions(path$1, 384, warn);
534
534
  return path$1;
535
535
  }
536
+ async function readConnectOutcomeRuntime(directory) {
537
+ try {
538
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, CONNECT_OUTCOME_FILENAME), "utf8"));
539
+ if (typeof parsed !== "object" || parsed === null) return null;
540
+ const runtime = parsed.runtime;
541
+ return typeof runtime === "string" && runtime.length > 0 ? runtime : null;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
536
546
  var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME;
537
547
  var init_storage = __esm({
538
548
  "src/storage.ts"() {
@@ -557,9 +567,9 @@ var init_runtime_manifest = __esm({
557
567
  mcpPackage: "@haven_ai/mcp",
558
568
  mcpVersion: mcp.MCP_VERSION,
559
569
  sdkPackage: "@haven_ai/sdk",
560
- sdkVersion: "0.0.0-dev.202609181713.990ba70",
570
+ sdkVersion: "0.0.0-dev.202609181927.cd4620a",
561
571
  signerPackage: "@haven_ai/signer",
562
- signerVersion: "0.0.0-dev.202609181713.990ba70",
572
+ signerVersion: "0.0.0-dev.202609181927.cd4620a",
563
573
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
564
574
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
565
575
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -3517,9 +3527,32 @@ var init_rekey_restart = __esm({
3517
3527
  var doctor_exports = {};
3518
3528
  __export(doctor_exports, {
3519
3529
  describeAccountAddressKey: () => describeAccountAddressKey,
3530
+ rollUpLevel: () => rollUpLevel,
3520
3531
  runDoctor: () => runDoctor,
3521
3532
  runRepair: () => runRepair
3522
3533
  });
3534
+ function finalizeCheck(check) {
3535
+ return { ...check, ok: check.level !== "failed" };
3536
+ }
3537
+ function rollUpLevel(checks) {
3538
+ if (checks.some((check) => check.level === "failed")) return "failed";
3539
+ if (checks.some((check) => check.level === "advisory")) return "advisory";
3540
+ return "ok";
3541
+ }
3542
+ function runtimeFlagFor(runtime) {
3543
+ return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
3544
+ }
3545
+ async function resolveDoctorRuntime(input, directory) {
3546
+ const explicit = input.runtime.trim();
3547
+ if (explicit) return { runtime: explicit, origin: "flag" };
3548
+ if (directory) {
3549
+ const recorded = await readConnectOutcomeRuntime(directory);
3550
+ if (recorded !== null && normalizeRuntimeName(recorded)) {
3551
+ return { runtime: recorded, origin: "record" };
3552
+ }
3553
+ }
3554
+ return { runtime: "", origin: "unknown" };
3555
+ }
3523
3556
  async function discoverCredentialDirectory(homeDir, explicit) {
3524
3557
  const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
3525
3558
  let entries = [];
@@ -3580,13 +3613,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
3580
3613
  return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
3581
3614
  }
3582
3615
  function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3616
+ return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
3617
+ }
3618
+ function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
3583
3619
  const label = "Pending re-key";
3584
3620
  const nameFlag = slug ? ` --name ${slug}` : "";
3585
3621
  if (status.state === "unreadable") {
3586
3622
  return {
3587
3623
  id: "rekey_pending",
3588
3624
  label,
3589
- ok: false,
3625
+ level: "failed",
3590
3626
  detail: `A re-key was started here but ${status.path} does not parse, so neither the address it generated nor when it started can be read. The file still holds what was a private key.`,
3591
3627
  repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
3592
3628
  };
@@ -3598,9 +3634,9 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3598
3634
  return {
3599
3635
  id: "rekey_pending",
3600
3636
  label,
3601
- ok: false,
3637
+ level: "failed",
3602
3638
  detail: `A re-key started ${started} has COMPLETED on Haven \u2014 the agent's signing address is already ${address}, the one this machine generated \u2014 but the local half was never finished, so the credential files here still hold the old key. Parked at ${status.path}.` + (status.state === "expired" ? " The local file is also past its 24h TTL, which --rekey-finish refuses, so the finish command below will not accept it any more." : ""),
3603
- repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you> --runtime ${runtime}`
3639
+ repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you>${runtimeFlagFor(runtime)}`
3604
3640
  };
3605
3641
  }
3606
3642
  const wedgeNote = "Haven is NOT yet on this address, so the re-key did not complete. This machine cannot tell whether the on-chain revoke on the agent page already ran: if it did not, closing this costs nothing; if it did, the agent's old delegations are revoked, no new ones were issued, and only an owner re-grant restores its spend authority (#1868). Check the agent page before assuming the harmless case.";
@@ -3608,7 +3644,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3608
3644
  return {
3609
3645
  id: "rekey_pending",
3610
3646
  label,
3611
- ok: false,
3647
+ level: "failed",
3612
3648
  detail: `A re-key started ${started} EXPIRED ${status.expiresAt ?? ""} without being finished. Its address was ${address}; the private half it generated is still on disk at ${status.path}. ` + wedgeNote,
3613
3649
  repair: `Either delete ${status.path} to drop the parked key, or start over: ${RERUN} --rekey${nameFlag}. Connect never deletes it for you \u2014 an expired TTL is a refusal to USE the key, not a licence to destroy key material you may still be mid-flow on.`
3614
3650
  };
@@ -3616,7 +3652,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3616
3652
  return {
3617
3653
  id: "rekey_pending",
3618
3654
  label,
3619
- ok: true,
3655
+ level: "ok",
3620
3656
  detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
3621
3657
  };
3622
3658
  }
@@ -3644,13 +3680,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
3644
3680
  if (shell) facts.push(shell);
3645
3681
  if (facts.length === 0) return void 0;
3646
3682
  const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
3647
- return {
3683
+ return finalizeCheck({
3648
3684
  id: "runtime_spec_override",
3649
3685
  label: "Runtime spec override",
3650
- ok: false,
3686
+ level: "failed",
3651
3687
  detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
3652
3688
  repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>. If the override is intentional, this finding is the record of it.`
3653
- };
3689
+ });
3654
3690
  }
3655
3691
  async function readMcpSidecarOverride(directory) {
3656
3692
  try {
@@ -3677,6 +3713,13 @@ function describeAccountAddressKey(identity, signerFile) {
3677
3713
  return "no account address stored";
3678
3714
  }
3679
3715
  async function checksForAgent(entry, input, deps) {
3716
+ const verdicts = await verdictsForAgent(entry, input, deps);
3717
+ return {
3718
+ checks: verdicts.checks.map(finalizeCheck),
3719
+ ...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
3720
+ };
3721
+ }
3722
+ async function verdictsForAgent(entry, input, deps) {
3680
3723
  const { directory, identity, sidecar } = entry;
3681
3724
  const checks = [];
3682
3725
  let signerCapabilities;
@@ -3691,7 +3734,7 @@ async function checksForAgent(entry, input, deps) {
3691
3734
  checks.push({
3692
3735
  id: "credentials",
3693
3736
  label: "Agent credentials",
3694
- ok: credentialsOk,
3737
+ level: credentialsOk ? "ok" : "failed",
3695
3738
  detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"}; ${describeAccountAddressKey(identity, signerFile)})` : "identity.json or signer.json is missing or unparseable.",
3696
3739
  ...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
3697
3740
  });
@@ -3699,9 +3742,9 @@ async function checksForAgent(entry, input, deps) {
3699
3742
  checks.push({
3700
3743
  id: "signer_runtime",
3701
3744
  label: "Signer runtime (preinstalled wrapper)",
3702
- ok: false,
3745
+ level: "failed",
3703
3746
  detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
3704
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3747
+ repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
3705
3748
  });
3706
3749
  } else if (sidecar.runtime_spec_override) {
3707
3750
  const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
@@ -3711,9 +3754,9 @@ async function checksForAgent(entry, input, deps) {
3711
3754
  checks.push({
3712
3755
  id: "signer_runtime",
3713
3756
  label: "Signer runtime (preinstalled wrapper)",
3714
- ok: matches,
3757
+ level: matches ? "ok" : "failed",
3715
3758
  detail: matches ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory} (override install \u2014 see runtime_spec_override)` : `Override runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3716
- ...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime} with the same HAVEN_*_SPEC variables set.` }
3759
+ ...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
3717
3760
  });
3718
3761
  } else {
3719
3762
  const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
@@ -3725,9 +3768,9 @@ async function checksForAgent(entry, input, deps) {
3725
3768
  checks.push({
3726
3769
  id: "signer_runtime",
3727
3770
  label: "Signer runtime (preinstalled wrapper)",
3728
- ok,
3771
+ level: ok ? "ok" : intact ? "advisory" : "failed",
3729
3772
  detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : intact ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion} \u2014 intact, but outdated.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3730
- ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3773
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
3731
3774
  });
3732
3775
  }
3733
3776
  const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
@@ -3738,7 +3781,7 @@ async function checksForAgent(entry, input, deps) {
3738
3781
  checks.push({
3739
3782
  id: "hosted_mcp",
3740
3783
  label: "Hosted Haven MCP",
3741
- ok: probe.status === "ok",
3784
+ level: probe.status === "ok" ? "ok" : "failed",
3742
3785
  detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
3743
3786
  ...probe.status === "ok" ? {} : {
3744
3787
  repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
@@ -3748,7 +3791,7 @@ async function checksForAgent(entry, input, deps) {
3748
3791
  checks.push({
3749
3792
  id: "hosted_mcp",
3750
3793
  label: "Hosted Haven MCP",
3751
- ok: false,
3794
+ level: "failed",
3752
3795
  detail: "No stored API key / hosted MCP URL to probe with.",
3753
3796
  repair: `Re-run the full setup: ${RERUN} --setup <token>.`
3754
3797
  });
@@ -3766,15 +3809,15 @@ async function checksForAgent(entry, input, deps) {
3766
3809
  checks.push({
3767
3810
  id: "identity_match",
3768
3811
  label: "Hosted identity matches the local signing key",
3769
- ok: false,
3812
+ level: "failed",
3770
3813
  detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
3771
- repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor --runtime ${input.runtime}`
3814
+ repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor${runtimeFlagFor(input.runtime)}`
3772
3815
  });
3773
3816
  } else if (!localDelegate) {
3774
3817
  checks.push({
3775
3818
  id: "identity_match",
3776
3819
  label: "Hosted identity matches the local signing key",
3777
- ok: false,
3820
+ level: "failed",
3778
3821
  detail: "signer.json holds no delegate_address to compare against the hosted identity.",
3779
3822
  repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
3780
3823
  });
@@ -3783,7 +3826,7 @@ async function checksForAgent(entry, input, deps) {
3783
3826
  checks.push({
3784
3827
  id: "identity_match",
3785
3828
  label: "Hosted identity matches the local signing key",
3786
- ok: same,
3829
+ level: same ? "ok" : "failed",
3787
3830
  detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
3788
3831
  ...same ? {} : {
3789
3832
  repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
@@ -3801,7 +3844,7 @@ async function checksForAgent(entry, input, deps) {
3801
3844
  checks.push({
3802
3845
  id: "signer_process",
3803
3846
  label: "Signer stdio handshake",
3804
- ok: false,
3847
+ level: "failed",
3805
3848
  detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
3806
3849
  repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
3807
3850
  });
@@ -3818,18 +3861,18 @@ async function checksForAgent(entry, input, deps) {
3818
3861
  checks.push({
3819
3862
  id: "signer_process",
3820
3863
  label: "Signer stdio handshake",
3821
- ok: probe.status === "ok",
3864
+ level: probe.status === "ok" ? "ok" : "failed",
3822
3865
  detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
3823
- ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3866
+ ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
3824
3867
  });
3825
3868
  }
3826
3869
  } else {
3827
3870
  checks.push({
3828
3871
  id: "signer_process",
3829
3872
  label: "Signer stdio handshake",
3830
- ok: false,
3873
+ level: "failed",
3831
3874
  detail: "Skipped \u2014 no prepared signer runtime to probe.",
3832
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3875
+ repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
3833
3876
  });
3834
3877
  }
3835
3878
  return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
@@ -3839,7 +3882,11 @@ async function runDoctor(input, deps = {}) {
3839
3882
  const checks = [];
3840
3883
  let signerCapabilities;
3841
3884
  const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
3842
- const configPath = runtimeConfigPathFor(input.runtime, homeDir);
3885
+ const resolution = await resolveDoctorRuntime(input, directory);
3886
+ const runtime = resolution.runtime;
3887
+ const input2 = { ...input, runtime };
3888
+ const normalizedRuntime = normalizeRuntimeName(input2.runtime);
3889
+ const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
3843
3890
  let configText = null;
3844
3891
  if (configPath !== null) {
3845
3892
  try {
@@ -3876,7 +3923,7 @@ async function runDoctor(input, deps = {}) {
3876
3923
  // A tombstone is a deliberate record and outranks the discovery tell:
3877
3924
  // a retired directory that also holds a parked key stays `retired`.
3878
3925
  classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
3879
- checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)] : [],
3926
+ checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
3880
3927
  ...rekeyPending ? { rekeyPending } : {}
3881
3928
  });
3882
3929
  continue;
@@ -3891,11 +3938,11 @@ async function runDoctor(input, deps = {}) {
3891
3938
  ...rekeyPending ? { rekeyPending } : {}
3892
3939
  };
3893
3940
  if (wired) {
3894
- const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
3941
+ const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
3895
3942
  entry.checks = result.checks;
3896
3943
  capabilitiesByDirectory.set(dir, result.signerCapabilities);
3897
3944
  } else if (rekeyPending) {
3898
- entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)];
3945
+ entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
3899
3946
  }
3900
3947
  inventory.push(entry);
3901
3948
  }
@@ -3910,7 +3957,7 @@ async function runDoctor(input, deps = {}) {
3910
3957
  checks.push({
3911
3958
  id: "credentials",
3912
3959
  label: "Agent credentials",
3913
- ok: false,
3960
+ level: "failed",
3914
3961
  detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
3915
3962
  repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
3916
3963
  });
@@ -3920,7 +3967,7 @@ async function runDoctor(input, deps = {}) {
3920
3967
  if (!primaryChecksById.has("credentials")) {
3921
3968
  const result = await checksForAgent(
3922
3969
  { directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
3923
- input,
3970
+ input2,
3924
3971
  deps
3925
3972
  );
3926
3973
  signerCapabilities = result.signerCapabilities;
@@ -3931,20 +3978,37 @@ async function runDoctor(input, deps = {}) {
3931
3978
  if (check) checks.push(check);
3932
3979
  }
3933
3980
  }
3934
- if (configPath === null) {
3981
+ const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
3982
+ if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
3935
3983
  checks.push({
3936
3984
  id: "runtime_config",
3937
3985
  label: "Runtime MCP config",
3938
- ok: true,
3939
- detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
3986
+ level: "failed",
3987
+ detail: `Runtime '${input2.runtime}' is not one the connector recognises. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
3988
+ repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
3989
+ });
3990
+ } else if (configPath === null && input2.runtime === "") {
3991
+ checks.push({
3992
+ id: "runtime_config",
3993
+ label: "Runtime MCP config",
3994
+ level: "failed",
3995
+ detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? input.credentialsDir ?? "~/.haven/agents"} carries no resolvable ${CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
3996
+ repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
3997
+ });
3998
+ } else if (configPath === null) {
3999
+ checks.push({
4000
+ id: "runtime_config",
4001
+ label: "Runtime MCP config",
4002
+ level: "ok",
4003
+ detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.`
3940
4004
  });
3941
4005
  } else if (configText === null) {
3942
4006
  checks.push({
3943
4007
  id: "runtime_config",
3944
4008
  label: "Runtime MCP config",
3945
- ok: false,
4009
+ level: "failed",
3946
4010
  detail: `No runtime config at ${configPath}.`,
3947
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
4011
+ repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
3948
4012
  });
3949
4013
  } else {
3950
4014
  const primaryIdentity = await readIdentity(primaryDirectory ?? "");
@@ -3956,9 +4020,9 @@ async function runDoctor(input, deps = {}) {
3956
4020
  checks.push({
3957
4021
  id: "runtime_config",
3958
4022
  label: "Runtime MCP config",
3959
- ok,
4023
+ level: ok ? "ok" : "failed",
3960
4024
  detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
3961
- ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
4025
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
3962
4026
  });
3963
4027
  }
3964
4028
  for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
@@ -4005,13 +4069,17 @@ async function runDoctor(input, deps = {}) {
4005
4069
  );
4006
4070
  }
4007
4071
  const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
4072
+ const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
4073
+ const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
4008
4074
  checks.push({
4009
4075
  id: "superseded_agents",
4010
4076
  label: "Superseded agent credentials",
4011
- ok: supersededLive.length === 0,
4012
- detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
4013
- ...supersededLive.length > 0 ? {
4077
+ level: supersededLevel,
4078
+ detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
4079
+ ...supersededLevel === "failed" ? {
4014
4080
  repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
4081
+ } : supersededLevel === "advisory" ? {
4082
+ repair: `Check ${supersededLive.join(", ")} on the Haven agent page: revoke the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
4015
4083
  } : {}
4016
4084
  });
4017
4085
  }
@@ -4022,7 +4090,7 @@ async function runDoctor(input, deps = {}) {
4022
4090
  checks.push({
4023
4091
  id: "rekey_pending_elsewhere",
4024
4092
  label: "Parked re-keys in other credential directories",
4025
- ok: abandoned.length === 0,
4093
+ level: abandoned.length === 0 ? "ok" : "failed",
4026
4094
  detail: abandoned.length > 0 ? `ABANDONED re-key key material outside the agent this report describes: ${abandoned.map(describe).join(", ")}. Each holds a private key that was generated for a re-key nobody finished.` : `${parkedElsewhere.length} other director(y/ies) hold an open pending re-key: ${parkedElsewhere.map(describe).join(", ")}.`,
4027
4095
  ...abandoned.length > 0 ? {
4028
4096
  repair: "Check the Haven agent page for each before deleting: if its on-chain revoke already ran, the agent has no spend authority until you re-grant it (#1868), and that is not visible from this machine. Connect never deletes key material for you."
@@ -4031,20 +4099,23 @@ async function runDoctor(input, deps = {}) {
4031
4099
  }
4032
4100
  const signerProcess = primaryChecksById.get("signer_process");
4033
4101
  if (signerProcess) checks.push(signerProcess);
4034
- const restart = restartRequiredForRuntime(input.runtime, deps.env);
4102
+ const restart = restartRequiredForRuntime(input2.runtime, deps.env);
4035
4103
  checks.push({
4036
4104
  id: "restart",
4037
4105
  label: "Runtime restart",
4038
- ok: true,
4039
- detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
4106
+ level: "ok",
4107
+ detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : input2.runtime === "" ? "Runtime is unknown \u2014 whether a restart is needed cannot be determined. Re-run the doctor naming the runtime for a definitive answer." : "No restart requirement known for this runtime."
4040
4108
  });
4041
- const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
4109
+ const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
4110
+ const finalChecks = checks.map(finalizeCheck);
4111
+ const level = rollUpLevel([...finalChecks, ...wiredChecks]);
4042
4112
  return {
4043
4113
  version: 1,
4044
- ok: checks.every((check) => check.ok) && wiredOk,
4045
- runtime: input.runtime,
4114
+ ok: level !== "failed",
4115
+ level,
4116
+ runtime: input2.runtime,
4046
4117
  credentialDirectory: primaryDirectory,
4047
- checks,
4118
+ checks: finalChecks,
4048
4119
  agents: inventory,
4049
4120
  ...signerCapabilities ? { signerCapabilities } : {}
4050
4121
  };
@@ -4062,6 +4133,21 @@ async function runRepair(input, deps = {}) {
4062
4133
  messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
4063
4134
  };
4064
4135
  }
4136
+ const resolution = await resolveDoctorRuntime(input, directory);
4137
+ if (resolution.origin === "record") {
4138
+ messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${path.join(directory, CONNECT_OUTCOME_FILENAME)}.`);
4139
+ }
4140
+ const runtime = resolution.runtime;
4141
+ const input2 = { ...input, runtime };
4142
+ if (runtime === "") {
4143
+ return {
4144
+ ok: false,
4145
+ messages: [
4146
+ "Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
4147
+ `Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
4148
+ ]
4149
+ };
4150
+ }
4065
4151
  let identity;
4066
4152
  try {
4067
4153
  identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
@@ -4071,7 +4157,7 @@ async function runRepair(input, deps = {}) {
4071
4157
  if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
4072
4158
  return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
4073
4159
  }
4074
- const configPath = runtimeConfigPathFor(input.runtime, homeDir);
4160
+ const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
4075
4161
  if (configPath) {
4076
4162
  try {
4077
4163
  const existing = await promises.readFile(configPath, "utf8");
@@ -4098,7 +4184,10 @@ async function runRepair(input, deps = {}) {
4098
4184
  const names = serverNamesFor(serverName);
4099
4185
  messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
4100
4186
  const configResult = await writeRuntimeConfig({
4101
- runtime: input.runtime,
4187
+ // Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
4188
+ // switches on the id, and the raw alias fell to its "manual runtime" arm
4189
+ // — a repair that reported success while writing nothing.
4190
+ runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
4102
4191
  hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
4103
4192
  apiKey: identity.api_key,
4104
4193
  identityPath: path.join(directory, "identity.json"),
@@ -4429,7 +4518,7 @@ async function pathExists2(path) {
4429
4518
  init_unwire();
4430
4519
  init_local_mcp_runtime();
4431
4520
  init_runtime_manifest();
4432
- var CONNECTOR_VERSION = "0.0.0-dev.202609181713.990ba70";
4521
+ var CONNECTOR_VERSION = "0.0.0-dev.202609181927.cd4620a";
4433
4522
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4434
4523
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4435
4524
  function failureOutcomeFor(runtimeHint, error) {
@@ -5327,7 +5416,7 @@ function helpText() {
5327
5416
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
5328
5417
  " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
5329
5418
  " --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
5330
- " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on any failure.",
5419
+ " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
5331
5420
  " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
5332
5421
  " runtime, rewrite the wrapper and runtime config from stored credentials.",
5333
5422
  " Hosted topology only (refuses to touch a --local config). No keys, no token.",
@@ -5393,6 +5482,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
5393
5482
  }
5394
5483
  return 1;
5395
5484
  }
5485
+ function levelMarker(level) {
5486
+ return level === "ok" ? "\u2713" : level === "advisory" ? "!" : "\u2717";
5487
+ }
5488
+ function advisoryCount(report) {
5489
+ const others = report.agents.filter((agent) => agent.classification === "wired" && agent.directory !== report.credentialDirectory).flatMap((agent) => agent.checks);
5490
+ return [...report.checks, ...others].filter((check) => check.level === "advisory").length;
5491
+ }
5396
5492
  async function runCli(argv, io = {
5397
5493
  stdout: (message) => process.stdout.write(message),
5398
5494
  stderr: (message) => process.stderr.write(message)
@@ -5599,7 +5695,7 @@ async function runCli(argv, io = {
5599
5695
  `);
5600
5696
  } else {
5601
5697
  for (const check of report.checks) {
5602
- io.stdout(redactSecrets(`${check.ok ? "\u2713" : "\u2717"} ${check.label}: ${check.detail}
5698
+ io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
5603
5699
  `));
5604
5700
  if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
5605
5701
  `));
@@ -5609,21 +5705,25 @@ async function runCli(argv, io = {
5609
5705
  io.stdout("\nOther agents on this machine:\n");
5610
5706
  for (const agent of otherAgents) {
5611
5707
  const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
5612
- const failed = agent.checks.filter((check) => !check.ok);
5613
- const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
5614
- io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
5708
+ const failed = agent.checks.filter((check) => check.level === "failed");
5709
+ const advised = agent.checks.filter((check) => check.level === "advisory");
5710
+ const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
5711
+ io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
5615
5712
  `));
5616
- for (const check of failed) {
5617
- io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
5713
+ for (const check of [...failed, ...advised]) {
5714
+ io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
5618
5715
  `));
5619
5716
  if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
5620
5717
  `));
5621
5718
  }
5622
5719
  }
5623
5720
  }
5624
- io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
5721
+ io.stdout(
5722
+ report.level === "failed" ? "One or more checks FAILED \u2014 see repairs above.\n" : report.level === "advisory" ? `No failures. ${advisoryCount(report)} advisory finding(s) \u2014 see the ! line(s) above.
5723
+ ` : "All checks passed.\n"
5724
+ );
5625
5725
  }
5626
- return report.ok ? 0 : 1;
5726
+ return report.level === "failed" ? 1 : 0;
5627
5727
  } catch (err) {
5628
5728
  return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
5629
5729
  code: "doctor_failed",