@haven_ai/connect 0.2.1-alpha.0 → 0.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -15
- package/dist/cli.cjs +660 -89
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +661 -90
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +660 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -8
- package/dist/index.d.ts +46 -8
- package/dist/index.js +661 -90
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -171,6 +171,16 @@ function redactSecrets(value) {
|
|
|
171
171
|
function redactForAutomation(value) {
|
|
172
172
|
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
173
173
|
}
|
|
174
|
+
function withoutUserinfo(url) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = new URL(url);
|
|
177
|
+
parsed.username = "";
|
|
178
|
+
parsed.password = "";
|
|
179
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
180
|
+
} catch {
|
|
181
|
+
return url;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
174
184
|
function shortAddress(address) {
|
|
175
185
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
176
186
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -532,12 +542,64 @@ async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
|
532
542
|
await restrictPermissions(path$1, 384, warn);
|
|
533
543
|
return path$1;
|
|
534
544
|
}
|
|
535
|
-
|
|
545
|
+
async function readConnectOutcomeRuntime(directory) {
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, exports.CONNECT_OUTCOME_FILENAME), "utf8"));
|
|
548
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
549
|
+
const runtime = parsed.runtime;
|
|
550
|
+
return typeof runtime === "string" && runtime.length > 0 ? runtime : null;
|
|
551
|
+
} catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function writeMcpServerBinding(directory, binding) {
|
|
556
|
+
const path$1 = path.join(directory, MCP_SERVER_BINDING_FILENAME);
|
|
557
|
+
await promises.writeFile(path$1, `${JSON.stringify(binding, null, 2)}
|
|
558
|
+
`, { mode: 384 });
|
|
559
|
+
return path$1;
|
|
560
|
+
}
|
|
561
|
+
async function readMcpServerBinding(directory) {
|
|
562
|
+
try {
|
|
563
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, MCP_SERVER_BINDING_FILENAME), "utf8"));
|
|
564
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
565
|
+
const record = parsed;
|
|
566
|
+
if (record.version !== 1 || typeof record.server_name !== "string" || typeof record.agent_id !== "string" || typeof record.api_url !== "string" || typeof record.bound_at !== "string") return null;
|
|
567
|
+
return record;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async function clearMcpServerBinding(directory) {
|
|
573
|
+
try {
|
|
574
|
+
await promises.rm(path.join(directory, MCP_SERVER_BINDING_FILENAME));
|
|
575
|
+
return true;
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
581
|
+
const root = defaultCredentialRoot(baseDir);
|
|
582
|
+
let entries = [];
|
|
583
|
+
try {
|
|
584
|
+
entries = await promises.readdir(root);
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
const out = [];
|
|
589
|
+
for (const entry of entries) {
|
|
590
|
+
const directory = path.join(root, entry);
|
|
591
|
+
const binding = await readMcpServerBinding(directory);
|
|
592
|
+
if (binding) out.push({ directory, binding });
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS; exports.CONNECT_OUTCOME_FILENAME = void 0; var MCP_SERVER_BINDING_FILENAME;
|
|
536
597
|
var init_storage = __esm({
|
|
537
598
|
"src/storage.ts"() {
|
|
538
599
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
539
600
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
540
601
|
exports.CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
602
|
+
MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
|
|
541
603
|
}
|
|
542
604
|
});
|
|
543
605
|
function mcpPackageSpec() {
|
|
@@ -556,9 +618,9 @@ var init_runtime_manifest = __esm({
|
|
|
556
618
|
mcpPackage: "@haven_ai/mcp",
|
|
557
619
|
mcpVersion: mcp.MCP_VERSION,
|
|
558
620
|
sdkPackage: "@haven_ai/sdk",
|
|
559
|
-
sdkVersion: "0.
|
|
621
|
+
sdkVersion: "0.4.0-alpha.0",
|
|
560
622
|
signerPackage: "@haven_ai/signer",
|
|
561
|
-
signerVersion: "0.
|
|
623
|
+
signerVersion: "0.4.0-alpha.0",
|
|
562
624
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
563
625
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
564
626
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -1446,7 +1508,7 @@ async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
|
1446
1508
|
apiKey: credentials.apiKey,
|
|
1447
1509
|
apiUrl: credentials.apiUrl,
|
|
1448
1510
|
agentId: credentials.agentId,
|
|
1449
|
-
|
|
1511
|
+
accountAddress: credentials.accountAddress,
|
|
1450
1512
|
delegateAddress: credentials.delegateAddress,
|
|
1451
1513
|
chainId: credentials.chainId,
|
|
1452
1514
|
allowanceSummary: credentials.allowanceSummary
|
|
@@ -1527,7 +1589,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
1527
1589
|
}
|
|
1528
1590
|
}
|
|
1529
1591
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
1530
|
-
return new Promise((
|
|
1592
|
+
return new Promise((resolve10) => {
|
|
1531
1593
|
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
1532
1594
|
let stdout = "";
|
|
1533
1595
|
let settled = false;
|
|
@@ -1539,7 +1601,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1539
1601
|
settled = true;
|
|
1540
1602
|
clearTimeout(timeout);
|
|
1541
1603
|
child.kill();
|
|
1542
|
-
|
|
1604
|
+
resolve10(result);
|
|
1543
1605
|
};
|
|
1544
1606
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
1545
1607
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -2488,7 +2550,7 @@ async function buildSignerConsentInput(signerPath) {
|
|
|
2488
2550
|
});
|
|
2489
2551
|
return {
|
|
2490
2552
|
delegateAddress: signer$1.delegateAddress,
|
|
2491
|
-
|
|
2553
|
+
accountAddress: credentials.accountAddress,
|
|
2492
2554
|
agentId: credentials.agentId,
|
|
2493
2555
|
chainId: credentials.chainId,
|
|
2494
2556
|
network: credentials.network,
|
|
@@ -3184,8 +3246,52 @@ async function unwireAgent(input) {
|
|
|
3184
3246
|
}
|
|
3185
3247
|
}
|
|
3186
3248
|
}
|
|
3187
|
-
await
|
|
3188
|
-
|
|
3249
|
+
const bindingReleased = await clearMcpServerBinding(input.directory);
|
|
3250
|
+
const teardown = await decideTeardown(identity, input);
|
|
3251
|
+
if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
|
|
3252
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
|
|
3253
|
+
}
|
|
3254
|
+
async function decideTeardown(identity, input) {
|
|
3255
|
+
const ended = "Local recovery of a stranded delegate balance ends with it: the sweep-recovery routes accept only this agent's API key and delegate signature, and neither exists on this machine any more.";
|
|
3256
|
+
if (!identity?.api_key || !identity.api_url) {
|
|
3257
|
+
return {
|
|
3258
|
+
status: input.destroyKeyMaterial ? "forced" : "destroyed",
|
|
3259
|
+
probe: "not_probed",
|
|
3260
|
+
detail: "No stored API key + API URL to probe with \u2014 nothing the sweep-recovery routes would accept, so nothing to preserve."
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
const probe = await (input.probeHostedIdentity ?? probeHostedAgentIdentity)(identity.api_key, identity.api_url, input.fetch);
|
|
3264
|
+
if (input.destroyKeyMaterial) {
|
|
3265
|
+
return {
|
|
3266
|
+
status: "forced",
|
|
3267
|
+
probe: probe.status,
|
|
3268
|
+
detail: `Key material destroyed under ${DESTROY_FLAG} (probe: ${probe.status}): signer.json, any parked re-key, and the API key in identity.json.`,
|
|
3269
|
+
remedy: ended
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
switch (probe.status) {
|
|
3273
|
+
case "ok":
|
|
3274
|
+
return {
|
|
3275
|
+
status: "retained",
|
|
3276
|
+
probe: "ok",
|
|
3277
|
+
detail: "This agent is still ACTIVE on the backend: its API key and delegate key still carry spend authority, so destroying them would be a live spend-authority change. Key material kept in this directory (0o600); its MCP wiring above is gone.",
|
|
3278
|
+
remedy: `Revoke the agent on the Haven agent page (connect never revokes), then re-run --unwire; or, to delete the key anyway, re-run with ${DESTROY_FLAG}.`
|
|
3279
|
+
};
|
|
3280
|
+
case "unauthorized":
|
|
3281
|
+
return {
|
|
3282
|
+
status: "retained",
|
|
3283
|
+
probe: "unauthorized",
|
|
3284
|
+
detail: "This key no longer authenticates on normal routes (revoked, archived, paused, pending approval, rotated, or not a key the backend knows \u2014 it does not say which). A stranded delegate balance MAY still exist and the connector CANNOT check: this directory may hold the only local credential the sweep-recovery routes would still accept. Key material kept.",
|
|
3285
|
+
remedy: `Recover any stranded balance first (haven_sweep_delegate from a runtime still wired to this agent, or the Haven agent page), then re-run with ${DESTROY_FLAG} to remove the key material.`
|
|
3286
|
+
};
|
|
3287
|
+
default:
|
|
3288
|
+
return {
|
|
3289
|
+
status: "retained",
|
|
3290
|
+
probe: probe.status,
|
|
3291
|
+
detail: `Could not verify what this key is still good for (${probe.status}). Unknown is not "safe to delete": destroying it on a failed check would erase the evidence of a live key. Key material kept.`,
|
|
3292
|
+
remedy: `Retry when the backend is reachable, or re-run with ${DESTROY_FLAG} to remove the key material regardless.`
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3189
3295
|
}
|
|
3190
3296
|
async function tombstoneDirectoryIfAbsent(input) {
|
|
3191
3297
|
if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
|
|
@@ -3206,9 +3312,10 @@ async function teardownLocalKeyMaterial(directory, identity) {
|
|
|
3206
3312
|
async function readIdentityFile(directory) {
|
|
3207
3313
|
return identityAt(directory);
|
|
3208
3314
|
}
|
|
3209
|
-
var RUNTIMES;
|
|
3315
|
+
var RUNTIMES, DESTROY_FLAG;
|
|
3210
3316
|
var init_unwire = __esm({
|
|
3211
3317
|
"src/unwire.ts"() {
|
|
3318
|
+
init_probes();
|
|
3212
3319
|
init_config_writers();
|
|
3213
3320
|
init_server_names();
|
|
3214
3321
|
init_signer_runtime();
|
|
@@ -3222,6 +3329,7 @@ var init_unwire = __esm({
|
|
|
3222
3329
|
{ runtime: "vscode-insiders", label: "VS Code Insiders MCP config", kind: "json", serverRoot: "servers" },
|
|
3223
3330
|
{ runtime: "claude-desktop", label: "Claude Desktop config", kind: "json", serverRoot: "mcpServers" }
|
|
3224
3331
|
];
|
|
3332
|
+
DESTROY_FLAG = "--destroy-key-material";
|
|
3225
3333
|
}
|
|
3226
3334
|
});
|
|
3227
3335
|
|
|
@@ -3233,6 +3341,174 @@ var init_rekey_messages = __esm({
|
|
|
3233
3341
|
}
|
|
3234
3342
|
});
|
|
3235
3343
|
|
|
3344
|
+
// src/prune-runtimes.ts
|
|
3345
|
+
var prune_runtimes_exports = {};
|
|
3346
|
+
__export(prune_runtimes_exports, {
|
|
3347
|
+
normalizeRuntimePath: () => normalizeRuntimePath,
|
|
3348
|
+
pruneSignerRuntimes: () => pruneSignerRuntimes,
|
|
3349
|
+
referencedRuntimeDirectories: () => referencedRuntimeDirectories,
|
|
3350
|
+
rollUpPruneLevel: () => rollUpPruneLevel,
|
|
3351
|
+
signerRuntimeRoot: () => signerRuntimeRoot
|
|
3352
|
+
});
|
|
3353
|
+
function signerRuntimeRoot(homeDir) {
|
|
3354
|
+
return path.join(homeDir, ".haven", "signer-runtime");
|
|
3355
|
+
}
|
|
3356
|
+
function classifyKey(key) {
|
|
3357
|
+
if (key.startsWith("override-")) return "override";
|
|
3358
|
+
if (key === exports.MCP_RUNTIME_MANIFEST.signerVersion || /^\d+\.\d+\.\d+/.test(key)) return "version";
|
|
3359
|
+
return "unknown";
|
|
3360
|
+
}
|
|
3361
|
+
async function directoryBytes(directory) {
|
|
3362
|
+
let total = 0;
|
|
3363
|
+
const walk = async (dir) => {
|
|
3364
|
+
let entries = [];
|
|
3365
|
+
try {
|
|
3366
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
3367
|
+
} catch {
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
for (const entry of entries) {
|
|
3371
|
+
const path$1 = path.join(dir, entry.name);
|
|
3372
|
+
if (entry.isDirectory()) await walk(path$1);
|
|
3373
|
+
else if (entry.isFile()) {
|
|
3374
|
+
try {
|
|
3375
|
+
total += (await promises.stat(path$1)).size;
|
|
3376
|
+
} catch {
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
};
|
|
3381
|
+
await walk(directory);
|
|
3382
|
+
return total;
|
|
3383
|
+
}
|
|
3384
|
+
async function normalizeRuntimePath(path$1) {
|
|
3385
|
+
const resolved = path.resolve(path$1).replace(/[\\/]+$/, "");
|
|
3386
|
+
try {
|
|
3387
|
+
return await promises.realpath(resolved);
|
|
3388
|
+
} catch {
|
|
3389
|
+
return resolved;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
function escapeRegExp(text) {
|
|
3393
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3394
|
+
}
|
|
3395
|
+
async function referencedRuntimeDirectories(homeDir, credentialsDir) {
|
|
3396
|
+
const roots = /* @__PURE__ */ new Set([path.join(homeDir, ".haven", "agents")]);
|
|
3397
|
+
if (credentialsDir) roots.add(path.dirname(path.resolve(credentialsDir)));
|
|
3398
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
3399
|
+
const runtimeRoot = await normalizeRuntimePath(signerRuntimeRoot(homeDir));
|
|
3400
|
+
const rootSpellings = [.../* @__PURE__ */ new Set([path.resolve(signerRuntimeRoot(homeDir)), runtimeRoot])];
|
|
3401
|
+
const wrapperRe = new RegExp(`(?:${rootSpellings.map(escapeRegExp).join("|")})[\\/]+([^\\/'"\\s]+)`, "g");
|
|
3402
|
+
const add = async (runtimeDirectory, by) => {
|
|
3403
|
+
const key = await normalizeRuntimePath(runtimeDirectory);
|
|
3404
|
+
const list = referenced.get(key) ?? [];
|
|
3405
|
+
if (!list.includes(by)) list.push(by);
|
|
3406
|
+
referenced.set(key, list);
|
|
3407
|
+
};
|
|
3408
|
+
for (const root of roots) {
|
|
3409
|
+
let entries = [];
|
|
3410
|
+
try {
|
|
3411
|
+
entries = await promises.readdir(root);
|
|
3412
|
+
} catch {
|
|
3413
|
+
continue;
|
|
3414
|
+
}
|
|
3415
|
+
for (const entry of entries) {
|
|
3416
|
+
const directory = path.join(root, entry);
|
|
3417
|
+
try {
|
|
3418
|
+
const sidecar = JSON.parse(await promises.readFile(path.join(directory, "signer-runtime.json"), "utf8"));
|
|
3419
|
+
if (typeof sidecar.runtime_directory === "string" && sidecar.runtime_directory.length > 0) {
|
|
3420
|
+
await add(sidecar.runtime_directory, directory);
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
}
|
|
3424
|
+
try {
|
|
3425
|
+
const wrapper = await promises.readFile(path.join(directory, "bin", "haven-signer.mjs"), "utf8");
|
|
3426
|
+
for (const match of wrapper.matchAll(wrapperRe)) await add(path.join(runtimeRoot, match[1]), directory);
|
|
3427
|
+
} catch {
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return referenced;
|
|
3432
|
+
}
|
|
3433
|
+
function rollUpPruneLevel(entries) {
|
|
3434
|
+
if (entries.some((e) => e.level === "failed")) return "failed";
|
|
3435
|
+
if (entries.some((e) => e.level === "advisory")) return "advisory";
|
|
3436
|
+
return "ok";
|
|
3437
|
+
}
|
|
3438
|
+
async function pruneSignerRuntimes(input, deps = {}) {
|
|
3439
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
3440
|
+
const root = signerRuntimeRoot(homeDir);
|
|
3441
|
+
const measure = input.measure ?? true;
|
|
3442
|
+
const referenced = await referencedRuntimeDirectories(homeDir, deps.credentialsDir);
|
|
3443
|
+
const pin = await normalizeRuntimePath(path.join(root, exports.MCP_RUNTIME_MANIFEST.signerVersion));
|
|
3444
|
+
const remove = deps.rm ?? (async (directory) => promises.rm(directory, { recursive: true, force: false }));
|
|
3445
|
+
const entries = [];
|
|
3446
|
+
let keys = [];
|
|
3447
|
+
try {
|
|
3448
|
+
keys = (await promises.readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
3449
|
+
} catch {
|
|
3450
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed: 0, reclaimedBytes: 0, level: "ok" };
|
|
3451
|
+
}
|
|
3452
|
+
let removed = 0;
|
|
3453
|
+
let reclaimedBytes = 0;
|
|
3454
|
+
for (const key of keys) {
|
|
3455
|
+
const directory = path.join(root, key);
|
|
3456
|
+
const normalized = await normalizeRuntimePath(directory);
|
|
3457
|
+
const referencedBy = referenced.get(normalized) ?? [];
|
|
3458
|
+
const kind = classifyKey(key);
|
|
3459
|
+
if (referencedBy.length > 0 || normalized === pin) {
|
|
3460
|
+
entries.push({
|
|
3461
|
+
directory,
|
|
3462
|
+
key,
|
|
3463
|
+
kind,
|
|
3464
|
+
bytes: 0,
|
|
3465
|
+
referencedBy,
|
|
3466
|
+
action: "kept",
|
|
3467
|
+
level: "ok",
|
|
3468
|
+
detail: referencedBy.length > 0 ? `kept \u2014 named by ${referencedBy.length} credential director${referencedBy.length === 1 ? "y" : "ies"} (sidecar or wrapper)` : "kept \u2014 the connector's current pinned version (what --repair installs)"
|
|
3469
|
+
});
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
const bytes = measure ? await directoryBytes(directory) : 0;
|
|
3473
|
+
if (input.dryRun) {
|
|
3474
|
+
entries.push({
|
|
3475
|
+
directory,
|
|
3476
|
+
key,
|
|
3477
|
+
kind,
|
|
3478
|
+
bytes,
|
|
3479
|
+
referencedBy,
|
|
3480
|
+
action: "would_remove",
|
|
3481
|
+
level: "advisory",
|
|
3482
|
+
detail: `would remove \u2014 no credential directory names it (${kind}-keyed)`
|
|
3483
|
+
});
|
|
3484
|
+
continue;
|
|
3485
|
+
}
|
|
3486
|
+
try {
|
|
3487
|
+
await remove(directory);
|
|
3488
|
+
removed += 1;
|
|
3489
|
+
reclaimedBytes += bytes;
|
|
3490
|
+
entries.push({ directory, key, kind, bytes, referencedBy, action: "removed", level: "ok", detail: `removed \u2014 no credential directory named it (${kind}-keyed)` });
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
entries.push({
|
|
3493
|
+
directory,
|
|
3494
|
+
key,
|
|
3495
|
+
kind,
|
|
3496
|
+
bytes,
|
|
3497
|
+
referencedBy,
|
|
3498
|
+
action: "failed",
|
|
3499
|
+
level: "failed",
|
|
3500
|
+
detail: `removal failed: ${err instanceof Error ? err.message : String(err)} \u2014 a signer process may still hold it open; stop it and re-run`
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed, reclaimedBytes, level: rollUpPruneLevel(entries) };
|
|
3505
|
+
}
|
|
3506
|
+
var init_prune_runtimes = __esm({
|
|
3507
|
+
"src/prune-runtimes.ts"() {
|
|
3508
|
+
init_runtime_manifest();
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3236
3512
|
// src/rekey.ts
|
|
3237
3513
|
var rekey_exports = {};
|
|
3238
3514
|
__export(rekey_exports, {
|
|
@@ -3326,9 +3602,10 @@ async function finishRekey(options, deps = {}) {
|
|
|
3326
3602
|
apiKey: options.newApiKey,
|
|
3327
3603
|
delegateKey: pending.new_delegate_key,
|
|
3328
3604
|
delegateAddress: pending.new_delegate_address,
|
|
3329
|
-
//
|
|
3330
|
-
// identity's `account_address`
|
|
3331
|
-
|
|
3605
|
+
// The stored value (already new-name-first, from the credential-FILE
|
|
3606
|
+
// permanent fallback) wins; the hosted identity's `account_address` is a
|
|
3607
|
+
// LIVE server read, which as of #2914 emits only that name.
|
|
3608
|
+
accountAddress: stored.accountAddress ?? identity.account_address ?? void 0,
|
|
3332
3609
|
chainId: stored.chainId ?? identity.chain_id ?? void 0,
|
|
3333
3610
|
network: stored.network,
|
|
3334
3611
|
agentBudget: stored.agentBudget,
|
|
@@ -3515,9 +3792,32 @@ var init_rekey_restart = __esm({
|
|
|
3515
3792
|
var doctor_exports = {};
|
|
3516
3793
|
__export(doctor_exports, {
|
|
3517
3794
|
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
3795
|
+
rollUpLevel: () => rollUpLevel,
|
|
3518
3796
|
runDoctor: () => runDoctor,
|
|
3519
3797
|
runRepair: () => runRepair
|
|
3520
3798
|
});
|
|
3799
|
+
function finalizeCheck(check) {
|
|
3800
|
+
return { ...check, ok: check.level !== "failed" };
|
|
3801
|
+
}
|
|
3802
|
+
function rollUpLevel(checks) {
|
|
3803
|
+
if (checks.some((check) => check.level === "failed")) return "failed";
|
|
3804
|
+
if (checks.some((check) => check.level === "advisory")) return "advisory";
|
|
3805
|
+
return "ok";
|
|
3806
|
+
}
|
|
3807
|
+
function runtimeFlagFor(runtime) {
|
|
3808
|
+
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
3809
|
+
}
|
|
3810
|
+
async function resolveDoctorRuntime(input, directory) {
|
|
3811
|
+
const explicit = input.runtime.trim();
|
|
3812
|
+
if (explicit) return { runtime: explicit, origin: "flag" };
|
|
3813
|
+
if (directory) {
|
|
3814
|
+
const recorded = await readConnectOutcomeRuntime(directory);
|
|
3815
|
+
if (recorded !== null && normalizeRuntimeName(recorded)) {
|
|
3816
|
+
return { runtime: recorded, origin: "record" };
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return { runtime: "", origin: "unknown" };
|
|
3820
|
+
}
|
|
3521
3821
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
3522
3822
|
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
3523
3823
|
let entries = [];
|
|
@@ -3578,13 +3878,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
|
|
|
3578
3878
|
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3579
3879
|
}
|
|
3580
3880
|
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3881
|
+
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
3882
|
+
}
|
|
3883
|
+
function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
|
|
3581
3884
|
const label = "Pending re-key";
|
|
3582
3885
|
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3583
3886
|
if (status.state === "unreadable") {
|
|
3584
3887
|
return {
|
|
3585
3888
|
id: "rekey_pending",
|
|
3586
3889
|
label,
|
|
3587
|
-
|
|
3890
|
+
level: "failed",
|
|
3588
3891
|
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.`,
|
|
3589
3892
|
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3590
3893
|
};
|
|
@@ -3596,9 +3899,9 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3596
3899
|
return {
|
|
3597
3900
|
id: "rekey_pending",
|
|
3598
3901
|
label,
|
|
3599
|
-
|
|
3902
|
+
level: "failed",
|
|
3600
3903
|
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." : ""),
|
|
3601
|
-
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
|
|
3904
|
+
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)}`
|
|
3602
3905
|
};
|
|
3603
3906
|
}
|
|
3604
3907
|
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.";
|
|
@@ -3606,7 +3909,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3606
3909
|
return {
|
|
3607
3910
|
id: "rekey_pending",
|
|
3608
3911
|
label,
|
|
3609
|
-
|
|
3912
|
+
level: "failed",
|
|
3610
3913
|
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,
|
|
3611
3914
|
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.`
|
|
3612
3915
|
};
|
|
@@ -3614,7 +3917,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3614
3917
|
return {
|
|
3615
3918
|
id: "rekey_pending",
|
|
3616
3919
|
label,
|
|
3617
|
-
|
|
3920
|
+
level: "ok",
|
|
3618
3921
|
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
|
|
3619
3922
|
};
|
|
3620
3923
|
}
|
|
@@ -3642,13 +3945,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
|
3642
3945
|
if (shell) facts.push(shell);
|
|
3643
3946
|
if (facts.length === 0) return void 0;
|
|
3644
3947
|
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3645
|
-
return {
|
|
3948
|
+
return finalizeCheck({
|
|
3646
3949
|
id: "runtime_spec_override",
|
|
3647
3950
|
label: "Runtime spec override",
|
|
3648
|
-
|
|
3951
|
+
level: "failed",
|
|
3649
3952
|
detail: `runtime spec overridden \u2014 not the pinned manifest (${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}, ${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
|
|
3650
3953
|
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.`
|
|
3651
|
-
};
|
|
3954
|
+
});
|
|
3652
3955
|
}
|
|
3653
3956
|
async function readMcpSidecarOverride(directory) {
|
|
3654
3957
|
try {
|
|
@@ -3675,6 +3978,13 @@ function describeAccountAddressKey(identity, signerFile) {
|
|
|
3675
3978
|
return "no account address stored";
|
|
3676
3979
|
}
|
|
3677
3980
|
async function checksForAgent(entry, input, deps) {
|
|
3981
|
+
const verdicts = await verdictsForAgent(entry, input, deps);
|
|
3982
|
+
return {
|
|
3983
|
+
checks: verdicts.checks.map(finalizeCheck),
|
|
3984
|
+
...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
async function verdictsForAgent(entry, input, deps) {
|
|
3678
3988
|
const { directory, identity, sidecar } = entry;
|
|
3679
3989
|
const checks = [];
|
|
3680
3990
|
let signerCapabilities;
|
|
@@ -3689,7 +3999,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3689
3999
|
checks.push({
|
|
3690
4000
|
id: "credentials",
|
|
3691
4001
|
label: "Agent credentials",
|
|
3692
|
-
ok:
|
|
4002
|
+
level: credentialsOk ? "ok" : "failed",
|
|
3693
4003
|
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.",
|
|
3694
4004
|
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
3695
4005
|
});
|
|
@@ -3697,9 +4007,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3697
4007
|
checks.push({
|
|
3698
4008
|
id: "signer_runtime",
|
|
3699
4009
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3700
|
-
|
|
4010
|
+
level: "failed",
|
|
3701
4011
|
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3702
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4012
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3703
4013
|
});
|
|
3704
4014
|
} else if (sidecar.runtime_spec_override) {
|
|
3705
4015
|
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3709,9 +4019,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3709
4019
|
checks.push({
|
|
3710
4020
|
id: "signer_runtime",
|
|
3711
4021
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3712
|
-
ok:
|
|
4022
|
+
level: matches ? "ok" : "failed",
|
|
3713
4023
|
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.`,
|
|
3714
|
-
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4024
|
+
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
|
|
3715
4025
|
});
|
|
3716
4026
|
} else {
|
|
3717
4027
|
const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3723,9 +4033,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3723
4033
|
checks.push({
|
|
3724
4034
|
id: "signer_runtime",
|
|
3725
4035
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3726
|
-
ok,
|
|
4036
|
+
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
3727
4037
|
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 ${exports.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.`,
|
|
3728
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4038
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3729
4039
|
});
|
|
3730
4040
|
}
|
|
3731
4041
|
const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
|
|
@@ -3736,7 +4046,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3736
4046
|
checks.push({
|
|
3737
4047
|
id: "hosted_mcp",
|
|
3738
4048
|
label: "Hosted Haven MCP",
|
|
3739
|
-
|
|
4049
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3740
4050
|
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3741
4051
|
...probe.status === "ok" ? {} : {
|
|
3742
4052
|
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
@@ -3746,7 +4056,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3746
4056
|
checks.push({
|
|
3747
4057
|
id: "hosted_mcp",
|
|
3748
4058
|
label: "Hosted Haven MCP",
|
|
3749
|
-
|
|
4059
|
+
level: "failed",
|
|
3750
4060
|
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3751
4061
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3752
4062
|
});
|
|
@@ -3764,15 +4074,15 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3764
4074
|
checks.push({
|
|
3765
4075
|
id: "identity_match",
|
|
3766
4076
|
label: "Hosted identity matches the local signing key",
|
|
3767
|
-
|
|
4077
|
+
level: "failed",
|
|
3768
4078
|
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.`,
|
|
3769
|
-
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
|
|
4079
|
+
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)}`
|
|
3770
4080
|
});
|
|
3771
4081
|
} else if (!localDelegate) {
|
|
3772
4082
|
checks.push({
|
|
3773
4083
|
id: "identity_match",
|
|
3774
4084
|
label: "Hosted identity matches the local signing key",
|
|
3775
|
-
|
|
4085
|
+
level: "failed",
|
|
3776
4086
|
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3777
4087
|
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3778
4088
|
});
|
|
@@ -3781,7 +4091,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3781
4091
|
checks.push({
|
|
3782
4092
|
id: "identity_match",
|
|
3783
4093
|
label: "Hosted identity matches the local signing key",
|
|
3784
|
-
ok:
|
|
4094
|
+
level: same ? "ok" : "failed",
|
|
3785
4095
|
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.`,
|
|
3786
4096
|
...same ? {} : {
|
|
3787
4097
|
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.`
|
|
@@ -3799,7 +4109,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3799
4109
|
checks.push({
|
|
3800
4110
|
id: "signer_process",
|
|
3801
4111
|
label: "Signer stdio handshake",
|
|
3802
|
-
|
|
4112
|
+
level: "failed",
|
|
3803
4113
|
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3804
4114
|
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
3805
4115
|
});
|
|
@@ -3816,18 +4126,18 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3816
4126
|
checks.push({
|
|
3817
4127
|
id: "signer_process",
|
|
3818
4128
|
label: "Signer stdio handshake",
|
|
3819
|
-
|
|
4129
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3820
4130
|
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
3821
|
-
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4131
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3822
4132
|
});
|
|
3823
4133
|
}
|
|
3824
4134
|
} else {
|
|
3825
4135
|
checks.push({
|
|
3826
4136
|
id: "signer_process",
|
|
3827
4137
|
label: "Signer stdio handshake",
|
|
3828
|
-
|
|
4138
|
+
level: "failed",
|
|
3829
4139
|
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3830
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4140
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3831
4141
|
});
|
|
3832
4142
|
}
|
|
3833
4143
|
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
@@ -3837,7 +4147,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3837
4147
|
const checks = [];
|
|
3838
4148
|
let signerCapabilities;
|
|
3839
4149
|
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3840
|
-
const
|
|
4150
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4151
|
+
const runtime = resolution.runtime;
|
|
4152
|
+
const input2 = { ...input, runtime };
|
|
4153
|
+
const normalizedRuntime = normalizeRuntimeName(input2.runtime);
|
|
4154
|
+
const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
|
|
3841
4155
|
let configText = null;
|
|
3842
4156
|
if (configPath !== null) {
|
|
3843
4157
|
try {
|
|
@@ -3874,7 +4188,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3874
4188
|
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
3875
4189
|
// a retired directory that also holds a parked key stays `retired`.
|
|
3876
4190
|
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
3877
|
-
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0,
|
|
4191
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
|
|
3878
4192
|
...rekeyPending ? { rekeyPending } : {}
|
|
3879
4193
|
});
|
|
3880
4194
|
continue;
|
|
@@ -3889,11 +4203,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3889
4203
|
...rekeyPending ? { rekeyPending } : {}
|
|
3890
4204
|
};
|
|
3891
4205
|
if (wired) {
|
|
3892
|
-
const result = await checksForAgent({ directory: dir, identity, sidecar },
|
|
4206
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
|
|
3893
4207
|
entry.checks = result.checks;
|
|
3894
4208
|
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
3895
4209
|
} else if (rekeyPending) {
|
|
3896
|
-
entry.checks = [rekeyPendingCheck(rekeyPending, void 0,
|
|
4210
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
|
|
3897
4211
|
}
|
|
3898
4212
|
inventory.push(entry);
|
|
3899
4213
|
}
|
|
@@ -3908,7 +4222,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3908
4222
|
checks.push({
|
|
3909
4223
|
id: "credentials",
|
|
3910
4224
|
label: "Agent credentials",
|
|
3911
|
-
|
|
4225
|
+
level: "failed",
|
|
3912
4226
|
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3913
4227
|
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3914
4228
|
});
|
|
@@ -3918,7 +4232,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3918
4232
|
if (!primaryChecksById.has("credentials")) {
|
|
3919
4233
|
const result = await checksForAgent(
|
|
3920
4234
|
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
3921
|
-
|
|
4235
|
+
input2,
|
|
3922
4236
|
deps
|
|
3923
4237
|
);
|
|
3924
4238
|
signerCapabilities = result.signerCapabilities;
|
|
@@ -3929,20 +4243,37 @@ async function runDoctor(input, deps = {}) {
|
|
|
3929
4243
|
if (check) checks.push(check);
|
|
3930
4244
|
}
|
|
3931
4245
|
}
|
|
3932
|
-
|
|
4246
|
+
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
4247
|
+
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
3933
4248
|
checks.push({
|
|
3934
4249
|
id: "runtime_config",
|
|
3935
4250
|
label: "Runtime MCP config",
|
|
3936
|
-
|
|
3937
|
-
detail: `Runtime '${
|
|
4251
|
+
level: "failed",
|
|
4252
|
+
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(", ")}.`,
|
|
4253
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4254
|
+
});
|
|
4255
|
+
} else if (configPath === null && input2.runtime === "") {
|
|
4256
|
+
checks.push({
|
|
4257
|
+
id: "runtime_config",
|
|
4258
|
+
label: "Runtime MCP config",
|
|
4259
|
+
level: "failed",
|
|
4260
|
+
detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? input.credentialsDir ?? "~/.haven/agents"} carries no resolvable ${exports.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(", ")}.`,
|
|
4261
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4262
|
+
});
|
|
4263
|
+
} else if (configPath === null) {
|
|
4264
|
+
checks.push({
|
|
4265
|
+
id: "runtime_config",
|
|
4266
|
+
label: "Runtime MCP config",
|
|
4267
|
+
level: "ok",
|
|
4268
|
+
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.`
|
|
3938
4269
|
});
|
|
3939
4270
|
} else if (configText === null) {
|
|
3940
4271
|
checks.push({
|
|
3941
4272
|
id: "runtime_config",
|
|
3942
4273
|
label: "Runtime MCP config",
|
|
3943
|
-
|
|
4274
|
+
level: "failed",
|
|
3944
4275
|
detail: `No runtime config at ${configPath}.`,
|
|
3945
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4276
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
|
|
3946
4277
|
});
|
|
3947
4278
|
} else {
|
|
3948
4279
|
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
@@ -3954,9 +4285,9 @@ async function runDoctor(input, deps = {}) {
|
|
|
3954
4285
|
checks.push({
|
|
3955
4286
|
id: "runtime_config",
|
|
3956
4287
|
label: "Runtime MCP config",
|
|
3957
|
-
ok,
|
|
4288
|
+
level: ok ? "ok" : "failed",
|
|
3958
4289
|
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)" : ""}.`,
|
|
3959
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4290
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
|
|
3960
4291
|
});
|
|
3961
4292
|
}
|
|
3962
4293
|
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
@@ -4003,16 +4334,44 @@ async function runDoctor(input, deps = {}) {
|
|
|
4003
4334
|
);
|
|
4004
4335
|
}
|
|
4005
4336
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
4337
|
+
const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
|
|
4338
|
+
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
4006
4339
|
checks.push({
|
|
4007
4340
|
id: "superseded_agents",
|
|
4008
4341
|
label: "Superseded agent credentials",
|
|
4009
|
-
|
|
4010
|
-
detail:
|
|
4011
|
-
...
|
|
4342
|
+
level: supersededLevel,
|
|
4343
|
+
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("; ")}.`,
|
|
4344
|
+
...supersededLevel === "failed" ? {
|
|
4012
4345
|
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.`
|
|
4346
|
+
} : supersededLevel === "advisory" ? {
|
|
4347
|
+
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.`
|
|
4013
4348
|
} : {}
|
|
4014
4349
|
});
|
|
4015
4350
|
}
|
|
4351
|
+
const bindings = (await Promise.all(
|
|
4352
|
+
inventory.filter((entry) => entry.classification !== "retired").map(async (entry) => ({ entry, binding: await readMcpServerBinding(entry.directory) }))
|
|
4353
|
+
)).filter((item) => item.binding !== null);
|
|
4354
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4355
|
+
for (const item of bindings) {
|
|
4356
|
+
const list = byName.get(item.binding.server_name) ?? [];
|
|
4357
|
+
list.push(item);
|
|
4358
|
+
byName.set(item.binding.server_name, list);
|
|
4359
|
+
}
|
|
4360
|
+
const rebound = [...byName.entries()].filter(([, items]) => items.length > 1);
|
|
4361
|
+
if (rebound.length > 0) {
|
|
4362
|
+
const parts = rebound.map(([name, items]) => {
|
|
4363
|
+
const ordered = [...items].sort((a, b) => a.binding.bound_at < b.binding.bound_at ? -1 : a.binding.bound_at > b.binding.bound_at ? 1 : 0);
|
|
4364
|
+
const backends = new Set(ordered.map((i) => withoutUserinfo(i.binding.api_url)));
|
|
4365
|
+
return `'${name}': ` + ordered.map((i) => `${i.binding.agent_id} on ${withoutUserinfo(i.binding.api_url)} at ${i.binding.bound_at} [${i.entry.classification}]`).join(" \u2192 ") + (backends.size > 1 ? " (BACKEND CHANGED)" : "");
|
|
4366
|
+
});
|
|
4367
|
+
checks.push({
|
|
4368
|
+
id: "mcp_server_name_rebound",
|
|
4369
|
+
label: "MCP server names bound more than once",
|
|
4370
|
+
level: "advisory",
|
|
4371
|
+
detail: `${rebound.length} MCP server name${rebound.length === 1 ? "" : "s"} changed hands on this machine (locally recorded bindings; the backend's own record is the authority for the same backend): ${parts.join("; ")}. A saved session, script or document naming the server may still mean the earlier agent.`,
|
|
4372
|
+
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4016
4375
|
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
4017
4376
|
if (parkedElsewhere.length > 0) {
|
|
4018
4377
|
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
@@ -4020,7 +4379,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
4020
4379
|
checks.push({
|
|
4021
4380
|
id: "rekey_pending_elsewhere",
|
|
4022
4381
|
label: "Parked re-keys in other credential directories",
|
|
4023
|
-
|
|
4382
|
+
level: abandoned.length === 0 ? "ok" : "failed",
|
|
4024
4383
|
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(", ")}.`,
|
|
4025
4384
|
...abandoned.length > 0 ? {
|
|
4026
4385
|
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."
|
|
@@ -4029,20 +4388,34 @@ async function runDoctor(input, deps = {}) {
|
|
|
4029
4388
|
}
|
|
4030
4389
|
const signerProcess = primaryChecksById.get("signer_process");
|
|
4031
4390
|
if (signerProcess) checks.push(signerProcess);
|
|
4032
|
-
const
|
|
4391
|
+
const prune = await (deps.pruneSignerRuntimes ?? pruneSignerRuntimes)({ dryRun: true, measure: false }, { homeDir, credentialsDir: input.credentialsDir });
|
|
4392
|
+
const unused = prune.entries.filter((entry) => entry.action === "would_remove");
|
|
4393
|
+
if (unused.length > 0) {
|
|
4394
|
+
checks.push({
|
|
4395
|
+
id: "signer_runtime_unused",
|
|
4396
|
+
label: "Unused signer-runtime directories",
|
|
4397
|
+
level: "advisory",
|
|
4398
|
+
detail: `${unused.length} signer-runtime director${unused.length === 1 ? "y" : "ies"} under ${prune.root} that no credential directory names: ${unused.map((entry) => entry.key).join(", ")}. Nothing is broken; they are left over from earlier pins or overrides (sizes: --prune-signer-runtimes --dry-run).`,
|
|
4399
|
+
repair: `Run: ${RERUN} --prune-signer-runtimes (add --dry-run to list only).`
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
const restart = restartRequiredForRuntime(input2.runtime, deps.env);
|
|
4033
4403
|
checks.push({
|
|
4034
4404
|
id: "restart",
|
|
4035
4405
|
label: "Runtime restart",
|
|
4036
|
-
|
|
4037
|
-
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."
|
|
4406
|
+
level: "ok",
|
|
4407
|
+
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."
|
|
4038
4408
|
});
|
|
4039
|
-
const
|
|
4409
|
+
const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
|
|
4410
|
+
const finalChecks = checks.map(finalizeCheck);
|
|
4411
|
+
const level = rollUpLevel([...finalChecks, ...wiredChecks]);
|
|
4040
4412
|
return {
|
|
4041
4413
|
version: 1,
|
|
4042
|
-
ok:
|
|
4043
|
-
|
|
4414
|
+
ok: level !== "failed",
|
|
4415
|
+
level,
|
|
4416
|
+
runtime: input2.runtime,
|
|
4044
4417
|
credentialDirectory: primaryDirectory,
|
|
4045
|
-
checks,
|
|
4418
|
+
checks: finalChecks,
|
|
4046
4419
|
agents: inventory,
|
|
4047
4420
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
4048
4421
|
};
|
|
@@ -4060,6 +4433,21 @@ async function runRepair(input, deps = {}) {
|
|
|
4060
4433
|
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
4061
4434
|
};
|
|
4062
4435
|
}
|
|
4436
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4437
|
+
if (resolution.origin === "record") {
|
|
4438
|
+
messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${path.join(directory, exports.CONNECT_OUTCOME_FILENAME)}.`);
|
|
4439
|
+
}
|
|
4440
|
+
const runtime = resolution.runtime;
|
|
4441
|
+
const input2 = { ...input, runtime };
|
|
4442
|
+
if (runtime === "") {
|
|
4443
|
+
return {
|
|
4444
|
+
ok: false,
|
|
4445
|
+
messages: [
|
|
4446
|
+
"Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
|
|
4447
|
+
`Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4448
|
+
]
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4063
4451
|
let identity;
|
|
4064
4452
|
try {
|
|
4065
4453
|
identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
@@ -4069,7 +4457,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4069
4457
|
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
4070
4458
|
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
4071
4459
|
}
|
|
4072
|
-
const configPath = runtimeConfigPathFor(
|
|
4460
|
+
const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
|
|
4073
4461
|
if (configPath) {
|
|
4074
4462
|
try {
|
|
4075
4463
|
const existing = await promises.readFile(configPath, "utf8");
|
|
@@ -4096,7 +4484,10 @@ async function runRepair(input, deps = {}) {
|
|
|
4096
4484
|
const names = serverNamesFor(serverName);
|
|
4097
4485
|
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
4098
4486
|
const configResult = await writeRuntimeConfig({
|
|
4099
|
-
|
|
4487
|
+
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
4488
|
+
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
4489
|
+
// — a repair that reported success while writing nothing.
|
|
4490
|
+
runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
|
|
4100
4491
|
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
4101
4492
|
apiKey: identity.api_key,
|
|
4102
4493
|
identityPath: path.join(directory, "identity.json"),
|
|
@@ -4114,6 +4505,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4114
4505
|
var RERUN;
|
|
4115
4506
|
var init_doctor = __esm({
|
|
4116
4507
|
"src/doctor.ts"() {
|
|
4508
|
+
init_prune_runtimes();
|
|
4117
4509
|
init_runtime_manifest();
|
|
4118
4510
|
init_probes();
|
|
4119
4511
|
init_signer_runtime();
|
|
@@ -4427,7 +4819,7 @@ async function pathExists2(path) {
|
|
|
4427
4819
|
init_unwire();
|
|
4428
4820
|
init_local_mcp_runtime();
|
|
4429
4821
|
init_runtime_manifest();
|
|
4430
|
-
var CONNECTOR_VERSION = "0.
|
|
4822
|
+
var CONNECTOR_VERSION = "0.4.0-alpha.0";
|
|
4431
4823
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4432
4824
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4433
4825
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -4554,6 +4946,27 @@ async function executeConnect(options, deps, trace) {
|
|
|
4554
4946
|
}
|
|
4555
4947
|
}
|
|
4556
4948
|
}
|
|
4949
|
+
const existingAgents = await listExistingKeyedAgents(options.credentialsDir);
|
|
4950
|
+
if (existingAgents.length > 0) {
|
|
4951
|
+
log("");
|
|
4952
|
+
log(
|
|
4953
|
+
`Heads-up (before anything is written to this machine): it already carries ${existingAgents.length} agent director${existingAgents.length === 1 ? "y" : "ies"} with stored keys \u2014 ` + existingAgents.map((a) => `${a.agentId}${a.accountAddress ? ` (spends from ${shortAddress(a.accountAddress)})` : ""}`).join(", ") + ". This setup creates a NEW agent alongside them and revokes nothing."
|
|
4954
|
+
);
|
|
4955
|
+
}
|
|
4956
|
+
const hostedNameForRun = serverNamesFor(serverName).hosted;
|
|
4957
|
+
let reboundFrom;
|
|
4958
|
+
const holders = (await listMcpServerBindings(options.credentialsDir)).filter(({ binding }) => binding.server_name === hostedNameForRun).sort((a, b) => a.binding.bound_at < b.binding.bound_at ? 1 : a.binding.bound_at > b.binding.bound_at ? -1 : 0);
|
|
4959
|
+
const newest = holders[0];
|
|
4960
|
+
if (newest) {
|
|
4961
|
+
const { binding, directory } = newest;
|
|
4962
|
+
const backendChanged = withoutUserinfo(binding.api_url) !== withoutUserinfo(options.apiBaseUrl);
|
|
4963
|
+
const previousApiUrl = withoutUserinfo(binding.api_url);
|
|
4964
|
+
reboundFrom = { server_name: binding.server_name, agent_id: binding.agent_id, api_url: previousApiUrl, bound_at: binding.bound_at, backend_changed: backendChanged };
|
|
4965
|
+
const beingReplaced = replacing?.superseded.some((entry) => entry.directory === directory) ?? false;
|
|
4966
|
+
log(
|
|
4967
|
+
`Heads-up: the MCP server name '${binding.server_name}' was bound to agent ${binding.agent_id} on ${previousApiUrl} at ${binding.bound_at} (locally recorded${holders.length > 1 ? `; ${holders.length - 1} older record${holders.length > 2 ? "s" : ""} also claim${holders.length > 2 ? "" : "s"} it` : ""}). ` + (beingReplaced ? `This run replaces that wiring, as you chose, with a new agent on ${withoutUserinfo(options.apiBaseUrl)}` : `This run rebinds it to a new agent on ${withoutUserinfo(options.apiBaseUrl)}`) + (backendChanged ? " \u2014 a DIFFERENT backend: any saved session, script or document naming this MCP server now resolves to a different backend and agent." : ".")
|
|
4968
|
+
);
|
|
4969
|
+
}
|
|
4557
4970
|
const localKey = generateKey();
|
|
4558
4971
|
const localApiKey = generateLocalApiKey();
|
|
4559
4972
|
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
@@ -4617,6 +5030,21 @@ async function executeConnect(options, deps, trace) {
|
|
|
4617
5030
|
warn: log
|
|
4618
5031
|
});
|
|
4619
5032
|
trace.directory = credentialPaths.directory;
|
|
5033
|
+
try {
|
|
5034
|
+
const bindingRecord = {
|
|
5035
|
+
version: 1,
|
|
5036
|
+
server_name: hostedNameForRun,
|
|
5037
|
+
signer_name: serverNamesFor(serverName).signer,
|
|
5038
|
+
agent_id: registration.agent_id,
|
|
5039
|
+
api_url: withoutUserinfo(options.apiBaseUrl),
|
|
5040
|
+
// never persist `user:pass@` (#3154 doc review r2)
|
|
5041
|
+
...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
|
|
5042
|
+
bound_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5043
|
+
};
|
|
5044
|
+
await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
|
|
5045
|
+
} catch {
|
|
5046
|
+
log("Could not record the MCP server-name binding locally (non-fatal; the next setup cannot see this binding).");
|
|
5047
|
+
}
|
|
4620
5048
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
4621
5049
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
4622
5050
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -4696,6 +5124,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4696
5124
|
replacedBy: registration.agent_id
|
|
4697
5125
|
});
|
|
4698
5126
|
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
5127
|
+
await clearMcpServerBinding(entry.directory);
|
|
4699
5128
|
retiredAgentIds.push(entry.agentId);
|
|
4700
5129
|
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
4701
5130
|
} catch (err) {
|
|
@@ -4765,6 +5194,8 @@ async function executeConnect(options, deps, trace) {
|
|
|
4765
5194
|
supersededAgentIds,
|
|
4766
5195
|
supersededAgentsRetiredLocally,
|
|
4767
5196
|
...replacing ? { retiredAgentIds } : {},
|
|
5197
|
+
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
5198
|
+
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
4768
5199
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4769
5200
|
approvalRequired: registration.agent_status === "pending_approval",
|
|
4770
5201
|
approvalUrl: registration.approval_url
|
|
@@ -4829,6 +5260,10 @@ function completionOutcome(input) {
|
|
|
4829
5260
|
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
4830
5261
|
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
4831
5262
|
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
5263
|
+
// #3122: always emitted on a completed run (empty list included), for the
|
|
5264
|
+
// same reason as superseded_agent_ids above.
|
|
5265
|
+
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
5266
|
+
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
4832
5267
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4833
5268
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
4834
5269
|
};
|
|
@@ -5013,7 +5448,7 @@ function describeWaitBound(timeoutMs) {
|
|
|
5013
5448
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
5014
5449
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
5015
5450
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
5016
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
5451
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
|
|
5017
5452
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
5018
5453
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
5019
5454
|
let waitingAnnounced = false;
|
|
@@ -5104,6 +5539,32 @@ function activationInstructionWithWhy(profile) {
|
|
|
5104
5539
|
return profile.activationInstruction;
|
|
5105
5540
|
}
|
|
5106
5541
|
var RERUN_HINT = sdk.connectorRerunCommand();
|
|
5542
|
+
async function listExistingKeyedAgents(baseDir) {
|
|
5543
|
+
const root = defaultCredentialRoot(baseDir);
|
|
5544
|
+
let entries = [];
|
|
5545
|
+
try {
|
|
5546
|
+
entries = await promises.readdir(root);
|
|
5547
|
+
} catch {
|
|
5548
|
+
return [];
|
|
5549
|
+
}
|
|
5550
|
+
const out = [];
|
|
5551
|
+
for (const entry of entries) {
|
|
5552
|
+
const directory = path.join(root, entry);
|
|
5553
|
+
try {
|
|
5554
|
+
const identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
5555
|
+
if (typeof identity.api_key !== "string" || identity.api_key.length === 0) continue;
|
|
5556
|
+
let agent = {};
|
|
5557
|
+
try {
|
|
5558
|
+
agent = JSON.parse(await promises.readFile(path.join(directory, "agent.json"), "utf8"));
|
|
5559
|
+
} catch {
|
|
5560
|
+
}
|
|
5561
|
+
const { accountAddress } = readStoredAccountAddress(identity, agent);
|
|
5562
|
+
out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
|
|
5563
|
+
} catch {
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
return out;
|
|
5567
|
+
}
|
|
5107
5568
|
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
5108
5569
|
const root = defaultCredentialRoot(baseDir);
|
|
5109
5570
|
let entries = [];
|
|
@@ -5152,6 +5613,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
5152
5613
|
let tombstoneReason;
|
|
5153
5614
|
let tombstoneReplacedBy;
|
|
5154
5615
|
let unwire;
|
|
5616
|
+
let destroyKeyMaterial = false;
|
|
5617
|
+
let pruneSignerRuntimes2 = false;
|
|
5618
|
+
let dryRun = false;
|
|
5155
5619
|
let unwireDir;
|
|
5156
5620
|
let replace = false;
|
|
5157
5621
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -5179,6 +5643,12 @@ function parseArgs(argv, env = process.env) {
|
|
|
5179
5643
|
unwireDir = next;
|
|
5180
5644
|
i += 1;
|
|
5181
5645
|
}
|
|
5646
|
+
} else if (arg === "--destroy-key-material") {
|
|
5647
|
+
destroyKeyMaterial = true;
|
|
5648
|
+
} else if (arg === "--prune-signer-runtimes") {
|
|
5649
|
+
pruneSignerRuntimes2 = true;
|
|
5650
|
+
} else if (arg === "--dry-run") {
|
|
5651
|
+
dryRun = true;
|
|
5182
5652
|
} else if (arg === "--reason") {
|
|
5183
5653
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
5184
5654
|
} else if (arg === "--replaced-by") {
|
|
@@ -5221,7 +5691,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
5221
5691
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5222
5692
|
}
|
|
5223
5693
|
if (replace) {
|
|
5224
|
-
if (rekeyPhase || tombstoneDir || unwire || doctor || repair) {
|
|
5694
|
+
if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
|
|
5225
5695
|
throw new Error("--replace belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.");
|
|
5226
5696
|
}
|
|
5227
5697
|
if (options.serverName) {
|
|
@@ -5231,6 +5701,22 @@ function parseArgs(argv, env = process.env) {
|
|
|
5231
5701
|
}
|
|
5232
5702
|
options.replaceExistingWiring = true;
|
|
5233
5703
|
}
|
|
5704
|
+
if (destroyKeyMaterial && !unwire) {
|
|
5705
|
+
throw new Error("--destroy-key-material only applies to --unwire.");
|
|
5706
|
+
}
|
|
5707
|
+
if (destroyKeyMaterial && unwire) unwire = { ...unwire, destroyKeyMaterial: true };
|
|
5708
|
+
if (dryRun && !pruneSignerRuntimes2) {
|
|
5709
|
+
throw new Error("--dry-run only applies to --prune-signer-runtimes.");
|
|
5710
|
+
}
|
|
5711
|
+
if (pruneSignerRuntimes2) {
|
|
5712
|
+
if (unwire || tombstoneDir || rekeyPhase || doctor || repair) {
|
|
5713
|
+
throw new Error("--prune-signer-runtimes is its own operation; run it alone.");
|
|
5714
|
+
}
|
|
5715
|
+
if (options.setupToken) {
|
|
5716
|
+
throw new Error("--prune-signer-runtimes takes no --setup token; it reads stored state only.");
|
|
5717
|
+
}
|
|
5718
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir, pruneSignerRuntimes: { dryRun } };
|
|
5719
|
+
}
|
|
5234
5720
|
if (rekey) {
|
|
5235
5721
|
if (options.setupToken) {
|
|
5236
5722
|
throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
|
|
@@ -5325,7 +5811,7 @@ function helpText() {
|
|
|
5325
5811
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
5326
5812
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
5327
5813
|
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
5328
|
-
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on
|
|
5814
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
|
|
5329
5815
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
5330
5816
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
5331
5817
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
@@ -5352,6 +5838,12 @@ function helpText() {
|
|
|
5352
5838
|
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
5353
5839
|
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
5354
5840
|
" Haven agent page.",
|
|
5841
|
+
" --destroy-key-material With --unwire: destroy the signer key + stored API key even when the agent",
|
|
5842
|
+
" is still active or cannot be verified (#3123). Local sweep recovery ends.",
|
|
5843
|
+
" --prune-signer-runtimes Remove ~/.haven/signer-runtime directories no credential directory references",
|
|
5844
|
+
" (version- and override-keyed alike); --dry-run lists them. Exits 1 only on a",
|
|
5845
|
+
" removal that failed (#3123).",
|
|
5846
|
+
" --dry-run With --prune-signer-runtimes: report, remove nothing.",
|
|
5355
5847
|
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
5356
5848
|
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
5357
5849
|
" --help Show this help.",
|
|
@@ -5394,6 +5886,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
|
|
|
5394
5886
|
}
|
|
5395
5887
|
return 1;
|
|
5396
5888
|
}
|
|
5889
|
+
function levelMarker(level) {
|
|
5890
|
+
return level === "ok" ? "\u2713" : level === "advisory" ? "!" : "\u2717";
|
|
5891
|
+
}
|
|
5892
|
+
function advisoryCount(report) {
|
|
5893
|
+
const others = report.agents.filter((agent) => agent.classification === "wired" && agent.directory !== report.credentialDirectory).flatMap((agent) => agent.checks);
|
|
5894
|
+
return [...report.checks, ...others].filter((check) => check.level === "advisory").length;
|
|
5895
|
+
}
|
|
5397
5896
|
async function runCli(argv, io = {
|
|
5398
5897
|
stdout: (message) => process.stdout.write(message),
|
|
5399
5898
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -5421,13 +5920,13 @@ async function runCli(argv, io = {
|
|
|
5421
5920
|
}
|
|
5422
5921
|
if (parsed.tombstone) {
|
|
5423
5922
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
5424
|
-
const { readFile:
|
|
5425
|
-
const { join:
|
|
5923
|
+
const { readFile: readFile15 } = await import('fs/promises');
|
|
5924
|
+
const { join: join13 } = await import('path');
|
|
5426
5925
|
try {
|
|
5427
5926
|
let agentId = "unknown";
|
|
5428
5927
|
try {
|
|
5429
5928
|
const identity = JSON.parse(
|
|
5430
|
-
await
|
|
5929
|
+
await readFile15(join13(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
5431
5930
|
);
|
|
5432
5931
|
agentId = identity.agent_id ?? "unknown";
|
|
5433
5932
|
} catch {
|
|
@@ -5465,20 +5964,22 @@ async function runCli(argv, io = {
|
|
|
5465
5964
|
}
|
|
5466
5965
|
if (parsed.unwire) {
|
|
5467
5966
|
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
5468
|
-
const { homedir:
|
|
5469
|
-
const { join:
|
|
5470
|
-
const homeDir =
|
|
5471
|
-
const root = parsed.options.credentialsDir ??
|
|
5472
|
-
const directory = parsed.unwireDir ?? (parsed.options.serverName ?
|
|
5967
|
+
const { homedir: homedir11 } = await import('os');
|
|
5968
|
+
const { join: join13 } = await import('path');
|
|
5969
|
+
const homeDir = homedir11();
|
|
5970
|
+
const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
|
|
5971
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join13(root, parsed.options.serverName) : root);
|
|
5473
5972
|
try {
|
|
5474
5973
|
const result = await unwireAgent2({
|
|
5475
5974
|
directory,
|
|
5476
5975
|
slug: parsed.options.serverName,
|
|
5477
5976
|
reason: parsed.unwire.reason,
|
|
5478
5977
|
replacedBy: parsed.unwire.replacedBy,
|
|
5978
|
+
destroyKeyMaterial: parsed.unwire.destroyKeyMaterial,
|
|
5479
5979
|
homeDir
|
|
5480
5980
|
});
|
|
5481
5981
|
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
5982
|
+
const retained = result.teardown.status === "retained";
|
|
5482
5983
|
if (parsed.json) {
|
|
5483
5984
|
io.stdout(
|
|
5484
5985
|
`${redactSecrets(
|
|
@@ -5493,7 +5994,16 @@ async function runCli(argv, io = {
|
|
|
5493
5994
|
label: r.label,
|
|
5494
5995
|
status: r.status,
|
|
5495
5996
|
...r.detail ? { detail: r.detail } : {}
|
|
5496
|
-
}))
|
|
5997
|
+
})),
|
|
5998
|
+
// #3122: additive — whether the local server-name binding record was released.
|
|
5999
|
+
binding_released: result.bindingReleased,
|
|
6000
|
+
// #3123: additive — what happened to the key material and why.
|
|
6001
|
+
teardown: {
|
|
6002
|
+
status: result.teardown.status,
|
|
6003
|
+
probe: result.teardown.probe,
|
|
6004
|
+
detail: result.teardown.detail,
|
|
6005
|
+
...result.teardown.remedy ? { remedy: result.teardown.remedy } : {}
|
|
6006
|
+
}
|
|
5497
6007
|
})
|
|
5498
6008
|
)}
|
|
5499
6009
|
`
|
|
@@ -5509,14 +6019,20 @@ async function runCli(argv, io = {
|
|
|
5509
6019
|
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
5510
6020
|
`));
|
|
5511
6021
|
}
|
|
6022
|
+
io.stdout(result.bindingReleased ? " \u2713 MCP server-name binding: released (the name is free for the next setup).\n" : " \u2013 MCP server-name binding: none recorded for this directory.\n");
|
|
6023
|
+
const t = result.teardown;
|
|
6024
|
+
io.stdout(redactSecrets(` ${t.status === "retained" ? "\u2717" : t.status === "forced" ? "!" : "\u2713"} Key material: ${t.status} (probe: ${t.probe}) \u2014 ${t.detail}
|
|
6025
|
+
`));
|
|
6026
|
+
if (t.remedy) io.stdout(redactSecrets(` \u21B3 ${t.remedy}
|
|
6027
|
+
`));
|
|
5512
6028
|
io.stdout(
|
|
5513
|
-
failures.length > 0 ? " Some entries were NOT removed (\u2717 above). Re-run `--unwire` after resolving each refusal \u2014\n it is idempotent.\n" : " Verify: `--doctor --runtime <runtime>` per host should report this agent as `retired` with a\n clean runtime-config check.\n"
|
|
6029
|
+
failures.length > 0 ? " Some entries were NOT removed (\u2717 above). Re-run `--unwire` after resolving each refusal \u2014\n it is idempotent.\n" : retained ? " The wiring is gone; the key material is not (\u2717 above). `--doctor` will keep reporting this directory\n as `superseded` until the key is revoked or destroyed \u2014 that is the honest state.\n" : " Verify: `--doctor --runtime <runtime>` per host should report this agent as `retired` with a\n clean runtime-config check.\n"
|
|
5514
6030
|
);
|
|
5515
6031
|
io.stdout(
|
|
5516
|
-
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. This directory\u2019s local key material was removed and the tombstone\
|
|
6032
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. " + (retained ? "This directory\u2019s local key material was KEPT (see above); the tombstone\n" : "This directory\u2019s local key material was removed and the tombstone\n") + "record + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready, revoke the agent on the Haven agent page to stop it spending entirely.\n"
|
|
5517
6033
|
);
|
|
5518
6034
|
}
|
|
5519
|
-
return failures.length > 0 ? 1 : 0;
|
|
6035
|
+
return failures.length > 0 || retained ? 1 : 0;
|
|
5520
6036
|
} catch (err) {
|
|
5521
6037
|
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
5522
6038
|
code: "unwire_failed",
|
|
@@ -5524,6 +6040,57 @@ async function runCli(argv, io = {
|
|
|
5524
6040
|
});
|
|
5525
6041
|
}
|
|
5526
6042
|
}
|
|
6043
|
+
if (parsed.pruneSignerRuntimes) {
|
|
6044
|
+
const { pruneSignerRuntimes: pruneSignerRuntimes2 } = await Promise.resolve().then(() => (init_prune_runtimes(), prune_runtimes_exports));
|
|
6045
|
+
try {
|
|
6046
|
+
const report = await pruneSignerRuntimes2(
|
|
6047
|
+
{ dryRun: parsed.pruneSignerRuntimes.dryRun },
|
|
6048
|
+
{ credentialsDir: parsed.options.credentialsDir }
|
|
6049
|
+
);
|
|
6050
|
+
if (parsed.json) {
|
|
6051
|
+
io.stdout(`${redactSecrets(JSON.stringify({
|
|
6052
|
+
pruned: true,
|
|
6053
|
+
version: report.version,
|
|
6054
|
+
dry_run: report.dryRun,
|
|
6055
|
+
level: report.level,
|
|
6056
|
+
root: report.root,
|
|
6057
|
+
removed: report.removed,
|
|
6058
|
+
reclaimed_bytes: report.reclaimedBytes,
|
|
6059
|
+
entries: report.entries.map((e) => ({
|
|
6060
|
+
key: e.key,
|
|
6061
|
+
kind: e.kind,
|
|
6062
|
+
bytes: e.bytes,
|
|
6063
|
+
action: e.action,
|
|
6064
|
+
level: e.level,
|
|
6065
|
+
referenced_by: e.referencedBy,
|
|
6066
|
+
detail: e.detail
|
|
6067
|
+
}))
|
|
6068
|
+
}))}
|
|
6069
|
+
`);
|
|
6070
|
+
} else {
|
|
6071
|
+
io.stdout(`Signer runtimes under ${report.root}${report.dryRun ? " (dry run \u2014 nothing removed)" : ""}:
|
|
6072
|
+
`);
|
|
6073
|
+
if (report.entries.length === 0) io.stdout(" (none)\n");
|
|
6074
|
+
for (const e of report.entries) {
|
|
6075
|
+
const mark = e.level === "failed" ? "\u2717" : e.level === "advisory" ? "!" : e.action === "removed" ? "\u2713" : "\u2022";
|
|
6076
|
+
const size = e.bytes > 0 ? `${Math.round(e.bytes / 1024 / 1024)} MB` : e.action === "kept" ? "not sized" : "0 MB";
|
|
6077
|
+
io.stdout(redactSecrets(` ${mark} ${e.key} (${e.kind}, ${size}): ${e.detail}
|
|
6078
|
+
`));
|
|
6079
|
+
}
|
|
6080
|
+
io.stdout(
|
|
6081
|
+
report.dryRun ? `Would remove ${report.entries.filter((e) => e.action === "would_remove").length} director(y/ies); re-run without --dry-run to reclaim.
|
|
6082
|
+
` : `Removed ${report.removed} director(y/ies), reclaimed ${Math.round(report.reclaimedBytes / 1024 / 1024)} MB.
|
|
6083
|
+
`
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
return report.level === "failed" ? 1 : 0;
|
|
6087
|
+
} catch (err) {
|
|
6088
|
+
return failSubcommand(io, parsed.json, err, { pruned: false }, {
|
|
6089
|
+
code: "prune_failed",
|
|
6090
|
+
nextAction: "review_the_error_and_rerun_prune_which_is_idempotent"
|
|
6091
|
+
});
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
5527
6094
|
if (parsed.rekey) {
|
|
5528
6095
|
const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
|
|
5529
6096
|
const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
|
|
@@ -5600,7 +6167,7 @@ async function runCli(argv, io = {
|
|
|
5600
6167
|
`);
|
|
5601
6168
|
} else {
|
|
5602
6169
|
for (const check of report.checks) {
|
|
5603
|
-
io.stdout(redactSecrets(`${check.
|
|
6170
|
+
io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5604
6171
|
`));
|
|
5605
6172
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5606
6173
|
`));
|
|
@@ -5610,21 +6177,25 @@ async function runCli(argv, io = {
|
|
|
5610
6177
|
io.stdout("\nOther agents on this machine:\n");
|
|
5611
6178
|
for (const agent of otherAgents) {
|
|
5612
6179
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
5613
|
-
const failed = agent.checks.filter((check) =>
|
|
5614
|
-
const
|
|
5615
|
-
|
|
6180
|
+
const failed = agent.checks.filter((check) => check.level === "failed");
|
|
6181
|
+
const advised = agent.checks.filter((check) => check.level === "advisory");
|
|
6182
|
+
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;
|
|
6183
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
|
|
5616
6184
|
`));
|
|
5617
|
-
for (const check of failed) {
|
|
5618
|
-
io.stdout(redactSecrets(`
|
|
6185
|
+
for (const check of [...failed, ...advised]) {
|
|
6186
|
+
io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5619
6187
|
`));
|
|
5620
6188
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5621
6189
|
`));
|
|
5622
6190
|
}
|
|
5623
6191
|
}
|
|
5624
6192
|
}
|
|
5625
|
-
io.stdout(
|
|
6193
|
+
io.stdout(
|
|
6194
|
+
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.
|
|
6195
|
+
` : "All checks passed.\n"
|
|
6196
|
+
);
|
|
5626
6197
|
}
|
|
5627
|
-
return report.
|
|
6198
|
+
return report.level === "failed" ? 1 : 0;
|
|
5628
6199
|
} catch (err) {
|
|
5629
6200
|
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5630
6201
|
code: "doctor_failed",
|