@haven_ai/connect 0.0.0-dev.202609181807.87b28b1 → 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 +30 -4
- package/dist/cli.cjs +98 -48
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +98 -48
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +98 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +98 -48
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
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
|
-
|
|
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
|
@@ -567,9 +567,9 @@ var init_runtime_manifest = __esm({
|
|
|
567
567
|
mcpPackage: "@haven_ai/mcp",
|
|
568
568
|
mcpVersion: mcp.MCP_VERSION,
|
|
569
569
|
sdkPackage: "@haven_ai/sdk",
|
|
570
|
-
sdkVersion: "0.0.0-dev.
|
|
570
|
+
sdkVersion: "0.0.0-dev.202609181927.cd4620a",
|
|
571
571
|
signerPackage: "@haven_ai/signer",
|
|
572
|
-
signerVersion: "0.0.0-dev.
|
|
572
|
+
signerVersion: "0.0.0-dev.202609181927.cd4620a",
|
|
573
573
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
574
574
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
575
575
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -3527,9 +3527,18 @@ var init_rekey_restart = __esm({
|
|
|
3527
3527
|
var doctor_exports = {};
|
|
3528
3528
|
__export(doctor_exports, {
|
|
3529
3529
|
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
3530
|
+
rollUpLevel: () => rollUpLevel,
|
|
3530
3531
|
runDoctor: () => runDoctor,
|
|
3531
3532
|
runRepair: () => runRepair
|
|
3532
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
|
+
}
|
|
3533
3542
|
function runtimeFlagFor(runtime) {
|
|
3534
3543
|
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
3535
3544
|
}
|
|
@@ -3604,13 +3613,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
|
|
|
3604
3613
|
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3605
3614
|
}
|
|
3606
3615
|
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3616
|
+
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
3617
|
+
}
|
|
3618
|
+
function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
|
|
3607
3619
|
const label = "Pending re-key";
|
|
3608
3620
|
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3609
3621
|
if (status.state === "unreadable") {
|
|
3610
3622
|
return {
|
|
3611
3623
|
id: "rekey_pending",
|
|
3612
3624
|
label,
|
|
3613
|
-
|
|
3625
|
+
level: "failed",
|
|
3614
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.`,
|
|
3615
3627
|
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3616
3628
|
};
|
|
@@ -3622,7 +3634,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3622
3634
|
return {
|
|
3623
3635
|
id: "rekey_pending",
|
|
3624
3636
|
label,
|
|
3625
|
-
|
|
3637
|
+
level: "failed",
|
|
3626
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." : ""),
|
|
3627
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)}`
|
|
3628
3640
|
};
|
|
@@ -3632,7 +3644,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3632
3644
|
return {
|
|
3633
3645
|
id: "rekey_pending",
|
|
3634
3646
|
label,
|
|
3635
|
-
|
|
3647
|
+
level: "failed",
|
|
3636
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,
|
|
3637
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.`
|
|
3638
3650
|
};
|
|
@@ -3640,7 +3652,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3640
3652
|
return {
|
|
3641
3653
|
id: "rekey_pending",
|
|
3642
3654
|
label,
|
|
3643
|
-
|
|
3655
|
+
level: "ok",
|
|
3644
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
|
|
3645
3657
|
};
|
|
3646
3658
|
}
|
|
@@ -3668,13 +3680,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
|
3668
3680
|
if (shell) facts.push(shell);
|
|
3669
3681
|
if (facts.length === 0) return void 0;
|
|
3670
3682
|
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3671
|
-
return {
|
|
3683
|
+
return finalizeCheck({
|
|
3672
3684
|
id: "runtime_spec_override",
|
|
3673
3685
|
label: "Runtime spec override",
|
|
3674
|
-
|
|
3686
|
+
level: "failed",
|
|
3675
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(". ")}.`,
|
|
3676
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.`
|
|
3677
|
-
};
|
|
3689
|
+
});
|
|
3678
3690
|
}
|
|
3679
3691
|
async function readMcpSidecarOverride(directory) {
|
|
3680
3692
|
try {
|
|
@@ -3701,6 +3713,13 @@ function describeAccountAddressKey(identity, signerFile) {
|
|
|
3701
3713
|
return "no account address stored";
|
|
3702
3714
|
}
|
|
3703
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) {
|
|
3704
3723
|
const { directory, identity, sidecar } = entry;
|
|
3705
3724
|
const checks = [];
|
|
3706
3725
|
let signerCapabilities;
|
|
@@ -3715,7 +3734,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3715
3734
|
checks.push({
|
|
3716
3735
|
id: "credentials",
|
|
3717
3736
|
label: "Agent credentials",
|
|
3718
|
-
ok:
|
|
3737
|
+
level: credentialsOk ? "ok" : "failed",
|
|
3719
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.",
|
|
3720
3739
|
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
3721
3740
|
});
|
|
@@ -3723,7 +3742,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3723
3742
|
checks.push({
|
|
3724
3743
|
id: "signer_runtime",
|
|
3725
3744
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3726
|
-
|
|
3745
|
+
level: "failed",
|
|
3727
3746
|
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3728
3747
|
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3729
3748
|
});
|
|
@@ -3735,7 +3754,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3735
3754
|
checks.push({
|
|
3736
3755
|
id: "signer_runtime",
|
|
3737
3756
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3738
|
-
ok:
|
|
3757
|
+
level: matches ? "ok" : "failed",
|
|
3739
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.`,
|
|
3740
3759
|
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
|
|
3741
3760
|
});
|
|
@@ -3749,7 +3768,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3749
3768
|
checks.push({
|
|
3750
3769
|
id: "signer_runtime",
|
|
3751
3770
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3752
|
-
ok,
|
|
3771
|
+
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
3753
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.`,
|
|
3754
3773
|
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3755
3774
|
});
|
|
@@ -3762,7 +3781,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3762
3781
|
checks.push({
|
|
3763
3782
|
id: "hosted_mcp",
|
|
3764
3783
|
label: "Hosted Haven MCP",
|
|
3765
|
-
|
|
3784
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3766
3785
|
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3767
3786
|
...probe.status === "ok" ? {} : {
|
|
3768
3787
|
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
@@ -3772,7 +3791,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3772
3791
|
checks.push({
|
|
3773
3792
|
id: "hosted_mcp",
|
|
3774
3793
|
label: "Hosted Haven MCP",
|
|
3775
|
-
|
|
3794
|
+
level: "failed",
|
|
3776
3795
|
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3777
3796
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3778
3797
|
});
|
|
@@ -3790,7 +3809,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3790
3809
|
checks.push({
|
|
3791
3810
|
id: "identity_match",
|
|
3792
3811
|
label: "Hosted identity matches the local signing key",
|
|
3793
|
-
|
|
3812
|
+
level: "failed",
|
|
3794
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.`,
|
|
3795
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)}`
|
|
3796
3815
|
});
|
|
@@ -3798,7 +3817,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3798
3817
|
checks.push({
|
|
3799
3818
|
id: "identity_match",
|
|
3800
3819
|
label: "Hosted identity matches the local signing key",
|
|
3801
|
-
|
|
3820
|
+
level: "failed",
|
|
3802
3821
|
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3803
3822
|
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3804
3823
|
});
|
|
@@ -3807,7 +3826,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3807
3826
|
checks.push({
|
|
3808
3827
|
id: "identity_match",
|
|
3809
3828
|
label: "Hosted identity matches the local signing key",
|
|
3810
|
-
ok:
|
|
3829
|
+
level: same ? "ok" : "failed",
|
|
3811
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.`,
|
|
3812
3831
|
...same ? {} : {
|
|
3813
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.`
|
|
@@ -3825,7 +3844,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3825
3844
|
checks.push({
|
|
3826
3845
|
id: "signer_process",
|
|
3827
3846
|
label: "Signer stdio handshake",
|
|
3828
|
-
|
|
3847
|
+
level: "failed",
|
|
3829
3848
|
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3830
3849
|
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
3831
3850
|
});
|
|
@@ -3842,7 +3861,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3842
3861
|
checks.push({
|
|
3843
3862
|
id: "signer_process",
|
|
3844
3863
|
label: "Signer stdio handshake",
|
|
3845
|
-
|
|
3864
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3846
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}.`,
|
|
3847
3866
|
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3848
3867
|
});
|
|
@@ -3851,7 +3870,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3851
3870
|
checks.push({
|
|
3852
3871
|
id: "signer_process",
|
|
3853
3872
|
label: "Signer stdio handshake",
|
|
3854
|
-
|
|
3873
|
+
level: "failed",
|
|
3855
3874
|
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3856
3875
|
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3857
3876
|
});
|
|
@@ -3866,7 +3885,8 @@ async function runDoctor(input, deps = {}) {
|
|
|
3866
3885
|
const resolution = await resolveDoctorRuntime(input, directory);
|
|
3867
3886
|
const runtime = resolution.runtime;
|
|
3868
3887
|
const input2 = { ...input, runtime };
|
|
3869
|
-
const
|
|
3888
|
+
const normalizedRuntime = normalizeRuntimeName(input2.runtime);
|
|
3889
|
+
const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
|
|
3870
3890
|
let configText = null;
|
|
3871
3891
|
if (configPath !== null) {
|
|
3872
3892
|
try {
|
|
@@ -3937,7 +3957,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3937
3957
|
checks.push({
|
|
3938
3958
|
id: "credentials",
|
|
3939
3959
|
label: "Agent credentials",
|
|
3940
|
-
|
|
3960
|
+
level: "failed",
|
|
3941
3961
|
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3942
3962
|
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3943
3963
|
});
|
|
@@ -3958,11 +3978,20 @@ async function runDoctor(input, deps = {}) {
|
|
|
3958
3978
|
if (check) checks.push(check);
|
|
3959
3979
|
}
|
|
3960
3980
|
}
|
|
3961
|
-
|
|
3981
|
+
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
3982
|
+
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
3962
3983
|
checks.push({
|
|
3963
3984
|
id: "runtime_config",
|
|
3964
3985
|
label: "Runtime MCP config",
|
|
3965
|
-
|
|
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",
|
|
3966
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(", ")}.`,
|
|
3967
3996
|
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
3968
3997
|
});
|
|
@@ -3970,14 +3999,14 @@ async function runDoctor(input, deps = {}) {
|
|
|
3970
3999
|
checks.push({
|
|
3971
4000
|
id: "runtime_config",
|
|
3972
4001
|
label: "Runtime MCP config",
|
|
3973
|
-
|
|
4002
|
+
level: "ok",
|
|
3974
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.`
|
|
3975
4004
|
});
|
|
3976
4005
|
} else if (configText === null) {
|
|
3977
4006
|
checks.push({
|
|
3978
4007
|
id: "runtime_config",
|
|
3979
4008
|
label: "Runtime MCP config",
|
|
3980
|
-
|
|
4009
|
+
level: "failed",
|
|
3981
4010
|
detail: `No runtime config at ${configPath}.`,
|
|
3982
4011
|
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
|
|
3983
4012
|
});
|
|
@@ -3991,7 +4020,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3991
4020
|
checks.push({
|
|
3992
4021
|
id: "runtime_config",
|
|
3993
4022
|
label: "Runtime MCP config",
|
|
3994
|
-
ok,
|
|
4023
|
+
level: ok ? "ok" : "failed",
|
|
3995
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)" : ""}.`,
|
|
3996
4025
|
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
|
|
3997
4026
|
});
|
|
@@ -4040,13 +4069,17 @@ async function runDoctor(input, deps = {}) {
|
|
|
4040
4069
|
);
|
|
4041
4070
|
}
|
|
4042
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";
|
|
4043
4074
|
checks.push({
|
|
4044
4075
|
id: "superseded_agents",
|
|
4045
4076
|
label: "Superseded agent credentials",
|
|
4046
|
-
|
|
4047
|
-
detail:
|
|
4048
|
-
...
|
|
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" ? {
|
|
4049
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.`
|
|
4050
4083
|
} : {}
|
|
4051
4084
|
});
|
|
4052
4085
|
}
|
|
@@ -4057,7 +4090,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
4057
4090
|
checks.push({
|
|
4058
4091
|
id: "rekey_pending_elsewhere",
|
|
4059
4092
|
label: "Parked re-keys in other credential directories",
|
|
4060
|
-
|
|
4093
|
+
level: abandoned.length === 0 ? "ok" : "failed",
|
|
4061
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(", ")}.`,
|
|
4062
4095
|
...abandoned.length > 0 ? {
|
|
4063
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."
|
|
@@ -4070,16 +4103,19 @@ async function runDoctor(input, deps = {}) {
|
|
|
4070
4103
|
checks.push({
|
|
4071
4104
|
id: "restart",
|
|
4072
4105
|
label: "Runtime restart",
|
|
4073
|
-
|
|
4106
|
+
level: "ok",
|
|
4074
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."
|
|
4075
4108
|
});
|
|
4076
|
-
const
|
|
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]);
|
|
4077
4112
|
return {
|
|
4078
4113
|
version: 1,
|
|
4079
|
-
ok:
|
|
4114
|
+
ok: level !== "failed",
|
|
4115
|
+
level,
|
|
4080
4116
|
runtime: input2.runtime,
|
|
4081
4117
|
credentialDirectory: primaryDirectory,
|
|
4082
|
-
checks,
|
|
4118
|
+
checks: finalChecks,
|
|
4083
4119
|
agents: inventory,
|
|
4084
4120
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
4085
4121
|
};
|
|
@@ -4121,7 +4157,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4121
4157
|
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
4122
4158
|
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
4123
4159
|
}
|
|
4124
|
-
const configPath = runtimeConfigPathFor(input2.runtime, homeDir);
|
|
4160
|
+
const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
|
|
4125
4161
|
if (configPath) {
|
|
4126
4162
|
try {
|
|
4127
4163
|
const existing = await promises.readFile(configPath, "utf8");
|
|
@@ -4148,7 +4184,10 @@ async function runRepair(input, deps = {}) {
|
|
|
4148
4184
|
const names = serverNamesFor(serverName);
|
|
4149
4185
|
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
4150
4186
|
const configResult = await writeRuntimeConfig({
|
|
4151
|
-
|
|
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,
|
|
4152
4191
|
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
4153
4192
|
apiKey: identity.api_key,
|
|
4154
4193
|
identityPath: path.join(directory, "identity.json"),
|
|
@@ -4479,7 +4518,7 @@ async function pathExists2(path) {
|
|
|
4479
4518
|
init_unwire();
|
|
4480
4519
|
init_local_mcp_runtime();
|
|
4481
4520
|
init_runtime_manifest();
|
|
4482
|
-
var CONNECTOR_VERSION = "0.0.0-dev.
|
|
4521
|
+
var CONNECTOR_VERSION = "0.0.0-dev.202609181927.cd4620a";
|
|
4483
4522
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4484
4523
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4485
4524
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -5377,7 +5416,7 @@ function helpText() {
|
|
|
5377
5416
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
5378
5417
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
5379
5418
|
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
5380
|
-
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on
|
|
5419
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
|
|
5381
5420
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
5382
5421
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
5383
5422
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
@@ -5443,6 +5482,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
|
|
|
5443
5482
|
}
|
|
5444
5483
|
return 1;
|
|
5445
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
|
+
}
|
|
5446
5492
|
async function runCli(argv, io = {
|
|
5447
5493
|
stdout: (message) => process.stdout.write(message),
|
|
5448
5494
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -5649,7 +5695,7 @@ async function runCli(argv, io = {
|
|
|
5649
5695
|
`);
|
|
5650
5696
|
} else {
|
|
5651
5697
|
for (const check of report.checks) {
|
|
5652
|
-
io.stdout(redactSecrets(`${check.
|
|
5698
|
+
io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5653
5699
|
`));
|
|
5654
5700
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5655
5701
|
`));
|
|
@@ -5659,21 +5705,25 @@ async function runCli(argv, io = {
|
|
|
5659
5705
|
io.stdout("\nOther agents on this machine:\n");
|
|
5660
5706
|
for (const agent of otherAgents) {
|
|
5661
5707
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
5662
|
-
const failed = agent.checks.filter((check) =>
|
|
5663
|
-
const
|
|
5664
|
-
|
|
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}
|
|
5665
5712
|
`));
|
|
5666
|
-
for (const check of failed) {
|
|
5667
|
-
io.stdout(redactSecrets(`
|
|
5713
|
+
for (const check of [...failed, ...advised]) {
|
|
5714
|
+
io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5668
5715
|
`));
|
|
5669
5716
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5670
5717
|
`));
|
|
5671
5718
|
}
|
|
5672
5719
|
}
|
|
5673
5720
|
}
|
|
5674
|
-
io.stdout(
|
|
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
|
+
);
|
|
5675
5725
|
}
|
|
5676
|
-
return report.
|
|
5726
|
+
return report.level === "failed" ? 1 : 0;
|
|
5677
5727
|
} catch (err) {
|
|
5678
5728
|
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5679
5729
|
code: "doctor_failed",
|