@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto, { createHash } from 'crypto';
|
|
2
2
|
import { Wallet } from 'ethers';
|
|
3
|
-
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, rename } from 'fs/promises';
|
|
3
|
+
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, realpath, rename } from 'fs/promises';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path';
|
|
6
6
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
@@ -164,6 +164,16 @@ function redactSecrets(value) {
|
|
|
164
164
|
function redactForAutomation(value) {
|
|
165
165
|
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
166
166
|
}
|
|
167
|
+
function withoutUserinfo(url) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = new URL(url);
|
|
170
|
+
parsed.username = "";
|
|
171
|
+
parsed.password = "";
|
|
172
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
173
|
+
} catch {
|
|
174
|
+
return url;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
167
177
|
function shortAddress(address) {
|
|
168
178
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
169
179
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -525,12 +535,64 @@ async function writeConnectOutcomeRecord(directory, outcome, warn) {
|
|
|
525
535
|
await restrictPermissions(path, 384, warn);
|
|
526
536
|
return path;
|
|
527
537
|
}
|
|
528
|
-
|
|
538
|
+
async function readConnectOutcomeRuntime(directory) {
|
|
539
|
+
try {
|
|
540
|
+
const parsed = JSON.parse(await readFile(join(directory, CONNECT_OUTCOME_FILENAME), "utf8"));
|
|
541
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
542
|
+
const runtime = parsed.runtime;
|
|
543
|
+
return typeof runtime === "string" && runtime.length > 0 ? runtime : null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function writeMcpServerBinding(directory, binding) {
|
|
549
|
+
const path = join(directory, MCP_SERVER_BINDING_FILENAME);
|
|
550
|
+
await writeFile(path, `${JSON.stringify(binding, null, 2)}
|
|
551
|
+
`, { mode: 384 });
|
|
552
|
+
return path;
|
|
553
|
+
}
|
|
554
|
+
async function readMcpServerBinding(directory) {
|
|
555
|
+
try {
|
|
556
|
+
const parsed = JSON.parse(await readFile(join(directory, MCP_SERVER_BINDING_FILENAME), "utf8"));
|
|
557
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
558
|
+
const record = parsed;
|
|
559
|
+
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;
|
|
560
|
+
return record;
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function clearMcpServerBinding(directory) {
|
|
566
|
+
try {
|
|
567
|
+
await rm(join(directory, MCP_SERVER_BINDING_FILENAME));
|
|
568
|
+
return true;
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
574
|
+
const root = defaultCredentialRoot(baseDir);
|
|
575
|
+
let entries = [];
|
|
576
|
+
try {
|
|
577
|
+
entries = await readdir(root);
|
|
578
|
+
} catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
const directory = join(root, entry);
|
|
584
|
+
const binding = await readMcpServerBinding(directory);
|
|
585
|
+
if (binding) out.push({ directory, binding });
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
|
|
529
590
|
var init_storage = __esm({
|
|
530
591
|
"src/storage.ts"() {
|
|
531
592
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
532
593
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
533
594
|
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
595
|
+
MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
|
|
534
596
|
}
|
|
535
597
|
});
|
|
536
598
|
function mcpPackageSpec() {
|
|
@@ -549,9 +611,9 @@ var init_runtime_manifest = __esm({
|
|
|
549
611
|
mcpPackage: "@haven_ai/mcp",
|
|
550
612
|
mcpVersion: MCP_VERSION,
|
|
551
613
|
sdkPackage: "@haven_ai/sdk",
|
|
552
|
-
sdkVersion: "0.
|
|
614
|
+
sdkVersion: "0.4.0-alpha.0",
|
|
553
615
|
signerPackage: "@haven_ai/signer",
|
|
554
|
-
signerVersion: "0.
|
|
616
|
+
signerVersion: "0.4.0-alpha.0",
|
|
555
617
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
556
618
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
557
619
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -1439,7 +1501,7 @@ async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
|
1439
1501
|
apiKey: credentials.apiKey,
|
|
1440
1502
|
apiUrl: credentials.apiUrl,
|
|
1441
1503
|
agentId: credentials.agentId,
|
|
1442
|
-
|
|
1504
|
+
accountAddress: credentials.accountAddress,
|
|
1443
1505
|
delegateAddress: credentials.delegateAddress,
|
|
1444
1506
|
chainId: credentials.chainId,
|
|
1445
1507
|
allowanceSummary: credentials.allowanceSummary
|
|
@@ -1520,7 +1582,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
1520
1582
|
}
|
|
1521
1583
|
}
|
|
1522
1584
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
1523
|
-
return new Promise((
|
|
1585
|
+
return new Promise((resolve10) => {
|
|
1524
1586
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
1525
1587
|
let stdout = "";
|
|
1526
1588
|
let settled = false;
|
|
@@ -1532,7 +1594,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
1532
1594
|
settled = true;
|
|
1533
1595
|
clearTimeout(timeout);
|
|
1534
1596
|
child.kill();
|
|
1535
|
-
|
|
1597
|
+
resolve10(result);
|
|
1536
1598
|
};
|
|
1537
1599
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
1538
1600
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -2481,7 +2543,7 @@ async function buildSignerConsentInput(signerPath) {
|
|
|
2481
2543
|
});
|
|
2482
2544
|
return {
|
|
2483
2545
|
delegateAddress: signer.delegateAddress,
|
|
2484
|
-
|
|
2546
|
+
accountAddress: credentials.accountAddress,
|
|
2485
2547
|
agentId: credentials.agentId,
|
|
2486
2548
|
chainId: credentials.chainId,
|
|
2487
2549
|
network: credentials.network,
|
|
@@ -3177,8 +3239,52 @@ async function unwireAgent(input) {
|
|
|
3177
3239
|
}
|
|
3178
3240
|
}
|
|
3179
3241
|
}
|
|
3180
|
-
await
|
|
3181
|
-
|
|
3242
|
+
const bindingReleased = await clearMcpServerBinding(input.directory);
|
|
3243
|
+
const teardown = await decideTeardown(identity, input);
|
|
3244
|
+
if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
|
|
3245
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
|
|
3246
|
+
}
|
|
3247
|
+
async function decideTeardown(identity, input) {
|
|
3248
|
+
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.";
|
|
3249
|
+
if (!identity?.api_key || !identity.api_url) {
|
|
3250
|
+
return {
|
|
3251
|
+
status: input.destroyKeyMaterial ? "forced" : "destroyed",
|
|
3252
|
+
probe: "not_probed",
|
|
3253
|
+
detail: "No stored API key + API URL to probe with \u2014 nothing the sweep-recovery routes would accept, so nothing to preserve."
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
const probe = await (input.probeHostedIdentity ?? probeHostedAgentIdentity)(identity.api_key, identity.api_url, input.fetch);
|
|
3257
|
+
if (input.destroyKeyMaterial) {
|
|
3258
|
+
return {
|
|
3259
|
+
status: "forced",
|
|
3260
|
+
probe: probe.status,
|
|
3261
|
+
detail: `Key material destroyed under ${DESTROY_FLAG} (probe: ${probe.status}): signer.json, any parked re-key, and the API key in identity.json.`,
|
|
3262
|
+
remedy: ended
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
switch (probe.status) {
|
|
3266
|
+
case "ok":
|
|
3267
|
+
return {
|
|
3268
|
+
status: "retained",
|
|
3269
|
+
probe: "ok",
|
|
3270
|
+
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.",
|
|
3271
|
+
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}.`
|
|
3272
|
+
};
|
|
3273
|
+
case "unauthorized":
|
|
3274
|
+
return {
|
|
3275
|
+
status: "retained",
|
|
3276
|
+
probe: "unauthorized",
|
|
3277
|
+
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.",
|
|
3278
|
+
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.`
|
|
3279
|
+
};
|
|
3280
|
+
default:
|
|
3281
|
+
return {
|
|
3282
|
+
status: "retained",
|
|
3283
|
+
probe: probe.status,
|
|
3284
|
+
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.`,
|
|
3285
|
+
remedy: `Retry when the backend is reachable, or re-run with ${DESTROY_FLAG} to remove the key material regardless.`
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3182
3288
|
}
|
|
3183
3289
|
async function tombstoneDirectoryIfAbsent(input) {
|
|
3184
3290
|
if (await readOptionalText(join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
|
|
@@ -3199,9 +3305,10 @@ async function teardownLocalKeyMaterial(directory, identity) {
|
|
|
3199
3305
|
async function readIdentityFile(directory) {
|
|
3200
3306
|
return identityAt(directory);
|
|
3201
3307
|
}
|
|
3202
|
-
var RUNTIMES;
|
|
3308
|
+
var RUNTIMES, DESTROY_FLAG;
|
|
3203
3309
|
var init_unwire = __esm({
|
|
3204
3310
|
"src/unwire.ts"() {
|
|
3311
|
+
init_probes();
|
|
3205
3312
|
init_config_writers();
|
|
3206
3313
|
init_server_names();
|
|
3207
3314
|
init_signer_runtime();
|
|
@@ -3215,6 +3322,7 @@ var init_unwire = __esm({
|
|
|
3215
3322
|
{ runtime: "vscode-insiders", label: "VS Code Insiders MCP config", kind: "json", serverRoot: "servers" },
|
|
3216
3323
|
{ runtime: "claude-desktop", label: "Claude Desktop config", kind: "json", serverRoot: "mcpServers" }
|
|
3217
3324
|
];
|
|
3325
|
+
DESTROY_FLAG = "--destroy-key-material";
|
|
3218
3326
|
}
|
|
3219
3327
|
});
|
|
3220
3328
|
|
|
@@ -3226,6 +3334,174 @@ var init_rekey_messages = __esm({
|
|
|
3226
3334
|
}
|
|
3227
3335
|
});
|
|
3228
3336
|
|
|
3337
|
+
// src/prune-runtimes.ts
|
|
3338
|
+
var prune_runtimes_exports = {};
|
|
3339
|
+
__export(prune_runtimes_exports, {
|
|
3340
|
+
normalizeRuntimePath: () => normalizeRuntimePath,
|
|
3341
|
+
pruneSignerRuntimes: () => pruneSignerRuntimes,
|
|
3342
|
+
referencedRuntimeDirectories: () => referencedRuntimeDirectories,
|
|
3343
|
+
rollUpPruneLevel: () => rollUpPruneLevel,
|
|
3344
|
+
signerRuntimeRoot: () => signerRuntimeRoot
|
|
3345
|
+
});
|
|
3346
|
+
function signerRuntimeRoot(homeDir) {
|
|
3347
|
+
return join(homeDir, ".haven", "signer-runtime");
|
|
3348
|
+
}
|
|
3349
|
+
function classifyKey(key) {
|
|
3350
|
+
if (key.startsWith("override-")) return "override";
|
|
3351
|
+
if (key === MCP_RUNTIME_MANIFEST.signerVersion || /^\d+\.\d+\.\d+/.test(key)) return "version";
|
|
3352
|
+
return "unknown";
|
|
3353
|
+
}
|
|
3354
|
+
async function directoryBytes(directory) {
|
|
3355
|
+
let total = 0;
|
|
3356
|
+
const walk = async (dir) => {
|
|
3357
|
+
let entries = [];
|
|
3358
|
+
try {
|
|
3359
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
3360
|
+
} catch {
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
for (const entry of entries) {
|
|
3364
|
+
const path = join(dir, entry.name);
|
|
3365
|
+
if (entry.isDirectory()) await walk(path);
|
|
3366
|
+
else if (entry.isFile()) {
|
|
3367
|
+
try {
|
|
3368
|
+
total += (await stat(path)).size;
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
};
|
|
3374
|
+
await walk(directory);
|
|
3375
|
+
return total;
|
|
3376
|
+
}
|
|
3377
|
+
async function normalizeRuntimePath(path) {
|
|
3378
|
+
const resolved = resolve(path).replace(/[\\/]+$/, "");
|
|
3379
|
+
try {
|
|
3380
|
+
return await realpath(resolved);
|
|
3381
|
+
} catch {
|
|
3382
|
+
return resolved;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function escapeRegExp(text) {
|
|
3386
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3387
|
+
}
|
|
3388
|
+
async function referencedRuntimeDirectories(homeDir, credentialsDir) {
|
|
3389
|
+
const roots = /* @__PURE__ */ new Set([join(homeDir, ".haven", "agents")]);
|
|
3390
|
+
if (credentialsDir) roots.add(dirname(resolve(credentialsDir)));
|
|
3391
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
3392
|
+
const runtimeRoot = await normalizeRuntimePath(signerRuntimeRoot(homeDir));
|
|
3393
|
+
const rootSpellings = [.../* @__PURE__ */ new Set([resolve(signerRuntimeRoot(homeDir)), runtimeRoot])];
|
|
3394
|
+
const wrapperRe = new RegExp(`(?:${rootSpellings.map(escapeRegExp).join("|")})[\\/]+([^\\/'"\\s]+)`, "g");
|
|
3395
|
+
const add = async (runtimeDirectory, by) => {
|
|
3396
|
+
const key = await normalizeRuntimePath(runtimeDirectory);
|
|
3397
|
+
const list = referenced.get(key) ?? [];
|
|
3398
|
+
if (!list.includes(by)) list.push(by);
|
|
3399
|
+
referenced.set(key, list);
|
|
3400
|
+
};
|
|
3401
|
+
for (const root of roots) {
|
|
3402
|
+
let entries = [];
|
|
3403
|
+
try {
|
|
3404
|
+
entries = await readdir(root);
|
|
3405
|
+
} catch {
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
for (const entry of entries) {
|
|
3409
|
+
const directory = join(root, entry);
|
|
3410
|
+
try {
|
|
3411
|
+
const sidecar = JSON.parse(await readFile(join(directory, "signer-runtime.json"), "utf8"));
|
|
3412
|
+
if (typeof sidecar.runtime_directory === "string" && sidecar.runtime_directory.length > 0) {
|
|
3413
|
+
await add(sidecar.runtime_directory, directory);
|
|
3414
|
+
}
|
|
3415
|
+
} catch {
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
const wrapper = await readFile(join(directory, "bin", "haven-signer.mjs"), "utf8");
|
|
3419
|
+
for (const match of wrapper.matchAll(wrapperRe)) await add(join(runtimeRoot, match[1]), directory);
|
|
3420
|
+
} catch {
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
return referenced;
|
|
3425
|
+
}
|
|
3426
|
+
function rollUpPruneLevel(entries) {
|
|
3427
|
+
if (entries.some((e) => e.level === "failed")) return "failed";
|
|
3428
|
+
if (entries.some((e) => e.level === "advisory")) return "advisory";
|
|
3429
|
+
return "ok";
|
|
3430
|
+
}
|
|
3431
|
+
async function pruneSignerRuntimes(input, deps = {}) {
|
|
3432
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
3433
|
+
const root = signerRuntimeRoot(homeDir);
|
|
3434
|
+
const measure = input.measure ?? true;
|
|
3435
|
+
const referenced = await referencedRuntimeDirectories(homeDir, deps.credentialsDir);
|
|
3436
|
+
const pin = await normalizeRuntimePath(join(root, MCP_RUNTIME_MANIFEST.signerVersion));
|
|
3437
|
+
const remove = deps.rm ?? (async (directory) => rm(directory, { recursive: true, force: false }));
|
|
3438
|
+
const entries = [];
|
|
3439
|
+
let keys = [];
|
|
3440
|
+
try {
|
|
3441
|
+
keys = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
3442
|
+
} catch {
|
|
3443
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed: 0, reclaimedBytes: 0, level: "ok" };
|
|
3444
|
+
}
|
|
3445
|
+
let removed = 0;
|
|
3446
|
+
let reclaimedBytes = 0;
|
|
3447
|
+
for (const key of keys) {
|
|
3448
|
+
const directory = join(root, key);
|
|
3449
|
+
const normalized = await normalizeRuntimePath(directory);
|
|
3450
|
+
const referencedBy = referenced.get(normalized) ?? [];
|
|
3451
|
+
const kind = classifyKey(key);
|
|
3452
|
+
if (referencedBy.length > 0 || normalized === pin) {
|
|
3453
|
+
entries.push({
|
|
3454
|
+
directory,
|
|
3455
|
+
key,
|
|
3456
|
+
kind,
|
|
3457
|
+
bytes: 0,
|
|
3458
|
+
referencedBy,
|
|
3459
|
+
action: "kept",
|
|
3460
|
+
level: "ok",
|
|
3461
|
+
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)"
|
|
3462
|
+
});
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
const bytes = measure ? await directoryBytes(directory) : 0;
|
|
3466
|
+
if (input.dryRun) {
|
|
3467
|
+
entries.push({
|
|
3468
|
+
directory,
|
|
3469
|
+
key,
|
|
3470
|
+
kind,
|
|
3471
|
+
bytes,
|
|
3472
|
+
referencedBy,
|
|
3473
|
+
action: "would_remove",
|
|
3474
|
+
level: "advisory",
|
|
3475
|
+
detail: `would remove \u2014 no credential directory names it (${kind}-keyed)`
|
|
3476
|
+
});
|
|
3477
|
+
continue;
|
|
3478
|
+
}
|
|
3479
|
+
try {
|
|
3480
|
+
await remove(directory);
|
|
3481
|
+
removed += 1;
|
|
3482
|
+
reclaimedBytes += bytes;
|
|
3483
|
+
entries.push({ directory, key, kind, bytes, referencedBy, action: "removed", level: "ok", detail: `removed \u2014 no credential directory named it (${kind}-keyed)` });
|
|
3484
|
+
} catch (err) {
|
|
3485
|
+
entries.push({
|
|
3486
|
+
directory,
|
|
3487
|
+
key,
|
|
3488
|
+
kind,
|
|
3489
|
+
bytes,
|
|
3490
|
+
referencedBy,
|
|
3491
|
+
action: "failed",
|
|
3492
|
+
level: "failed",
|
|
3493
|
+
detail: `removal failed: ${err instanceof Error ? err.message : String(err)} \u2014 a signer process may still hold it open; stop it and re-run`
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed, reclaimedBytes, level: rollUpPruneLevel(entries) };
|
|
3498
|
+
}
|
|
3499
|
+
var init_prune_runtimes = __esm({
|
|
3500
|
+
"src/prune-runtimes.ts"() {
|
|
3501
|
+
init_runtime_manifest();
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3229
3505
|
// src/rekey.ts
|
|
3230
3506
|
var rekey_exports = {};
|
|
3231
3507
|
__export(rekey_exports, {
|
|
@@ -3319,9 +3595,10 @@ async function finishRekey(options, deps = {}) {
|
|
|
3319
3595
|
apiKey: options.newApiKey,
|
|
3320
3596
|
delegateKey: pending.new_delegate_key,
|
|
3321
3597
|
delegateAddress: pending.new_delegate_address,
|
|
3322
|
-
//
|
|
3323
|
-
// identity's `account_address`
|
|
3324
|
-
|
|
3598
|
+
// The stored value (already new-name-first, from the credential-FILE
|
|
3599
|
+
// permanent fallback) wins; the hosted identity's `account_address` is a
|
|
3600
|
+
// LIVE server read, which as of #2914 emits only that name.
|
|
3601
|
+
accountAddress: stored.accountAddress ?? identity.account_address ?? void 0,
|
|
3325
3602
|
chainId: stored.chainId ?? identity.chain_id ?? void 0,
|
|
3326
3603
|
network: stored.network,
|
|
3327
3604
|
agentBudget: stored.agentBudget,
|
|
@@ -3508,9 +3785,32 @@ var init_rekey_restart = __esm({
|
|
|
3508
3785
|
var doctor_exports = {};
|
|
3509
3786
|
__export(doctor_exports, {
|
|
3510
3787
|
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
3788
|
+
rollUpLevel: () => rollUpLevel,
|
|
3511
3789
|
runDoctor: () => runDoctor,
|
|
3512
3790
|
runRepair: () => runRepair
|
|
3513
3791
|
});
|
|
3792
|
+
function finalizeCheck(check) {
|
|
3793
|
+
return { ...check, ok: check.level !== "failed" };
|
|
3794
|
+
}
|
|
3795
|
+
function rollUpLevel(checks) {
|
|
3796
|
+
if (checks.some((check) => check.level === "failed")) return "failed";
|
|
3797
|
+
if (checks.some((check) => check.level === "advisory")) return "advisory";
|
|
3798
|
+
return "ok";
|
|
3799
|
+
}
|
|
3800
|
+
function runtimeFlagFor(runtime) {
|
|
3801
|
+
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
3802
|
+
}
|
|
3803
|
+
async function resolveDoctorRuntime(input, directory) {
|
|
3804
|
+
const explicit = input.runtime.trim();
|
|
3805
|
+
if (explicit) return { runtime: explicit, origin: "flag" };
|
|
3806
|
+
if (directory) {
|
|
3807
|
+
const recorded = await readConnectOutcomeRuntime(directory);
|
|
3808
|
+
if (recorded !== null && normalizeRuntimeName(recorded)) {
|
|
3809
|
+
return { runtime: recorded, origin: "record" };
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
return { runtime: "", origin: "unknown" };
|
|
3813
|
+
}
|
|
3514
3814
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
3515
3815
|
const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
|
|
3516
3816
|
let entries = [];
|
|
@@ -3571,13 +3871,16 @@ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bar
|
|
|
3571
3871
|
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3572
3872
|
}
|
|
3573
3873
|
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3874
|
+
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
3875
|
+
}
|
|
3876
|
+
function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
|
|
3574
3877
|
const label = "Pending re-key";
|
|
3575
3878
|
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
3576
3879
|
if (status.state === "unreadable") {
|
|
3577
3880
|
return {
|
|
3578
3881
|
id: "rekey_pending",
|
|
3579
3882
|
label,
|
|
3580
|
-
|
|
3883
|
+
level: "failed",
|
|
3581
3884
|
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.`,
|
|
3582
3885
|
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3583
3886
|
};
|
|
@@ -3589,9 +3892,9 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3589
3892
|
return {
|
|
3590
3893
|
id: "rekey_pending",
|
|
3591
3894
|
label,
|
|
3592
|
-
|
|
3895
|
+
level: "failed",
|
|
3593
3896
|
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." : ""),
|
|
3594
|
-
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
|
|
3897
|
+
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)}`
|
|
3595
3898
|
};
|
|
3596
3899
|
}
|
|
3597
3900
|
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.";
|
|
@@ -3599,7 +3902,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3599
3902
|
return {
|
|
3600
3903
|
id: "rekey_pending",
|
|
3601
3904
|
label,
|
|
3602
|
-
|
|
3905
|
+
level: "failed",
|
|
3603
3906
|
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,
|
|
3604
3907
|
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.`
|
|
3605
3908
|
};
|
|
@@ -3607,7 +3910,7 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3607
3910
|
return {
|
|
3608
3911
|
id: "rekey_pending",
|
|
3609
3912
|
label,
|
|
3610
|
-
|
|
3913
|
+
level: "ok",
|
|
3611
3914
|
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
|
|
3612
3915
|
};
|
|
3613
3916
|
}
|
|
@@ -3635,13 +3938,13 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
|
3635
3938
|
if (shell) facts.push(shell);
|
|
3636
3939
|
if (facts.length === 0) return void 0;
|
|
3637
3940
|
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3638
|
-
return {
|
|
3941
|
+
return finalizeCheck({
|
|
3639
3942
|
id: "runtime_spec_override",
|
|
3640
3943
|
label: "Runtime spec override",
|
|
3641
|
-
|
|
3944
|
+
level: "failed",
|
|
3642
3945
|
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(". ")}.`,
|
|
3643
3946
|
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.`
|
|
3644
|
-
};
|
|
3947
|
+
});
|
|
3645
3948
|
}
|
|
3646
3949
|
async function readMcpSidecarOverride(directory) {
|
|
3647
3950
|
try {
|
|
@@ -3668,6 +3971,13 @@ function describeAccountAddressKey(identity, signerFile) {
|
|
|
3668
3971
|
return "no account address stored";
|
|
3669
3972
|
}
|
|
3670
3973
|
async function checksForAgent(entry, input, deps) {
|
|
3974
|
+
const verdicts = await verdictsForAgent(entry, input, deps);
|
|
3975
|
+
return {
|
|
3976
|
+
checks: verdicts.checks.map(finalizeCheck),
|
|
3977
|
+
...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
async function verdictsForAgent(entry, input, deps) {
|
|
3671
3981
|
const { directory, identity, sidecar } = entry;
|
|
3672
3982
|
const checks = [];
|
|
3673
3983
|
let signerCapabilities;
|
|
@@ -3682,7 +3992,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3682
3992
|
checks.push({
|
|
3683
3993
|
id: "credentials",
|
|
3684
3994
|
label: "Agent credentials",
|
|
3685
|
-
ok:
|
|
3995
|
+
level: credentialsOk ? "ok" : "failed",
|
|
3686
3996
|
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.",
|
|
3687
3997
|
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
3688
3998
|
});
|
|
@@ -3690,9 +4000,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3690
4000
|
checks.push({
|
|
3691
4001
|
id: "signer_runtime",
|
|
3692
4002
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3693
|
-
|
|
4003
|
+
level: "failed",
|
|
3694
4004
|
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3695
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4005
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3696
4006
|
});
|
|
3697
4007
|
} else if (sidecar.runtime_spec_override) {
|
|
3698
4008
|
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3702,9 +4012,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3702
4012
|
checks.push({
|
|
3703
4013
|
id: "signer_runtime",
|
|
3704
4014
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3705
|
-
ok:
|
|
4015
|
+
level: matches ? "ok" : "failed",
|
|
3706
4016
|
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.`,
|
|
3707
|
-
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4017
|
+
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)} with the same HAVEN_*_SPEC variables set.` }
|
|
3708
4018
|
});
|
|
3709
4019
|
} else {
|
|
3710
4020
|
const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
@@ -3716,9 +4026,9 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3716
4026
|
checks.push({
|
|
3717
4027
|
id: "signer_runtime",
|
|
3718
4028
|
label: "Signer runtime (preinstalled wrapper)",
|
|
3719
|
-
ok,
|
|
4029
|
+
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
3720
4030
|
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.`,
|
|
3721
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4031
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3722
4032
|
});
|
|
3723
4033
|
}
|
|
3724
4034
|
const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
|
|
@@ -3729,7 +4039,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3729
4039
|
checks.push({
|
|
3730
4040
|
id: "hosted_mcp",
|
|
3731
4041
|
label: "Hosted Haven MCP",
|
|
3732
|
-
|
|
4042
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3733
4043
|
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
3734
4044
|
...probe.status === "ok" ? {} : {
|
|
3735
4045
|
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
@@ -3739,7 +4049,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3739
4049
|
checks.push({
|
|
3740
4050
|
id: "hosted_mcp",
|
|
3741
4051
|
label: "Hosted Haven MCP",
|
|
3742
|
-
|
|
4052
|
+
level: "failed",
|
|
3743
4053
|
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
3744
4054
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
3745
4055
|
});
|
|
@@ -3757,15 +4067,15 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3757
4067
|
checks.push({
|
|
3758
4068
|
id: "identity_match",
|
|
3759
4069
|
label: "Hosted identity matches the local signing key",
|
|
3760
|
-
|
|
4070
|
+
level: "failed",
|
|
3761
4071
|
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.`,
|
|
3762
|
-
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
|
|
4072
|
+
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)}`
|
|
3763
4073
|
});
|
|
3764
4074
|
} else if (!localDelegate) {
|
|
3765
4075
|
checks.push({
|
|
3766
4076
|
id: "identity_match",
|
|
3767
4077
|
label: "Hosted identity matches the local signing key",
|
|
3768
|
-
|
|
4078
|
+
level: "failed",
|
|
3769
4079
|
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
3770
4080
|
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
3771
4081
|
});
|
|
@@ -3774,7 +4084,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3774
4084
|
checks.push({
|
|
3775
4085
|
id: "identity_match",
|
|
3776
4086
|
label: "Hosted identity matches the local signing key",
|
|
3777
|
-
ok:
|
|
4087
|
+
level: same ? "ok" : "failed",
|
|
3778
4088
|
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.`,
|
|
3779
4089
|
...same ? {} : {
|
|
3780
4090
|
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.`
|
|
@@ -3792,7 +4102,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3792
4102
|
checks.push({
|
|
3793
4103
|
id: "signer_process",
|
|
3794
4104
|
label: "Signer stdio handshake",
|
|
3795
|
-
|
|
4105
|
+
level: "failed",
|
|
3796
4106
|
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3797
4107
|
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
3798
4108
|
});
|
|
@@ -3809,18 +4119,18 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3809
4119
|
checks.push({
|
|
3810
4120
|
id: "signer_process",
|
|
3811
4121
|
label: "Signer stdio handshake",
|
|
3812
|
-
|
|
4122
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
3813
4123
|
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
3814
|
-
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4124
|
+
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}` }
|
|
3815
4125
|
});
|
|
3816
4126
|
}
|
|
3817
4127
|
} else {
|
|
3818
4128
|
checks.push({
|
|
3819
4129
|
id: "signer_process",
|
|
3820
4130
|
label: "Signer stdio handshake",
|
|
3821
|
-
|
|
4131
|
+
level: "failed",
|
|
3822
4132
|
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
3823
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4133
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input.runtime)}`
|
|
3824
4134
|
});
|
|
3825
4135
|
}
|
|
3826
4136
|
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
@@ -3830,7 +4140,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3830
4140
|
const checks = [];
|
|
3831
4141
|
let signerCapabilities;
|
|
3832
4142
|
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
3833
|
-
const
|
|
4143
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4144
|
+
const runtime = resolution.runtime;
|
|
4145
|
+
const input2 = { ...input, runtime };
|
|
4146
|
+
const normalizedRuntime = normalizeRuntimeName(input2.runtime);
|
|
4147
|
+
const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
|
|
3834
4148
|
let configText = null;
|
|
3835
4149
|
if (configPath !== null) {
|
|
3836
4150
|
try {
|
|
@@ -3867,7 +4181,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3867
4181
|
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
3868
4182
|
// a retired directory that also holds a parked key stays `retired`.
|
|
3869
4183
|
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
3870
|
-
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0,
|
|
4184
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
|
|
3871
4185
|
...rekeyPending ? { rekeyPending } : {}
|
|
3872
4186
|
});
|
|
3873
4187
|
continue;
|
|
@@ -3882,11 +4196,11 @@ async function runDoctor(input, deps = {}) {
|
|
|
3882
4196
|
...rekeyPending ? { rekeyPending } : {}
|
|
3883
4197
|
};
|
|
3884
4198
|
if (wired) {
|
|
3885
|
-
const result = await checksForAgent({ directory: dir, identity, sidecar },
|
|
4199
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
|
|
3886
4200
|
entry.checks = result.checks;
|
|
3887
4201
|
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
3888
4202
|
} else if (rekeyPending) {
|
|
3889
|
-
entry.checks = [rekeyPendingCheck(rekeyPending, void 0,
|
|
4203
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
|
|
3890
4204
|
}
|
|
3891
4205
|
inventory.push(entry);
|
|
3892
4206
|
}
|
|
@@ -3901,7 +4215,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3901
4215
|
checks.push({
|
|
3902
4216
|
id: "credentials",
|
|
3903
4217
|
label: "Agent credentials",
|
|
3904
|
-
|
|
4218
|
+
level: "failed",
|
|
3905
4219
|
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
3906
4220
|
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
3907
4221
|
});
|
|
@@ -3911,7 +4225,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3911
4225
|
if (!primaryChecksById.has("credentials")) {
|
|
3912
4226
|
const result = await checksForAgent(
|
|
3913
4227
|
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
3914
|
-
|
|
4228
|
+
input2,
|
|
3915
4229
|
deps
|
|
3916
4230
|
);
|
|
3917
4231
|
signerCapabilities = result.signerCapabilities;
|
|
@@ -3922,20 +4236,37 @@ async function runDoctor(input, deps = {}) {
|
|
|
3922
4236
|
if (check) checks.push(check);
|
|
3923
4237
|
}
|
|
3924
4238
|
}
|
|
3925
|
-
|
|
4239
|
+
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
4240
|
+
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
3926
4241
|
checks.push({
|
|
3927
4242
|
id: "runtime_config",
|
|
3928
4243
|
label: "Runtime MCP config",
|
|
3929
|
-
|
|
3930
|
-
detail: `Runtime '${
|
|
4244
|
+
level: "failed",
|
|
4245
|
+
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(", ")}.`,
|
|
4246
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4247
|
+
});
|
|
4248
|
+
} else if (configPath === null && input2.runtime === "") {
|
|
4249
|
+
checks.push({
|
|
4250
|
+
id: "runtime_config",
|
|
4251
|
+
label: "Runtime MCP config",
|
|
4252
|
+
level: "failed",
|
|
4253
|
+
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(", ")}.`,
|
|
4254
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4255
|
+
});
|
|
4256
|
+
} else if (configPath === null) {
|
|
4257
|
+
checks.push({
|
|
4258
|
+
id: "runtime_config",
|
|
4259
|
+
label: "Runtime MCP config",
|
|
4260
|
+
level: "ok",
|
|
4261
|
+
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.`
|
|
3931
4262
|
});
|
|
3932
4263
|
} else if (configText === null) {
|
|
3933
4264
|
checks.push({
|
|
3934
4265
|
id: "runtime_config",
|
|
3935
4266
|
label: "Runtime MCP config",
|
|
3936
|
-
|
|
4267
|
+
level: "failed",
|
|
3937
4268
|
detail: `No runtime config at ${configPath}.`,
|
|
3938
|
-
repair: `Run: ${RERUN} --doctor --repair
|
|
4269
|
+
repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}`
|
|
3939
4270
|
});
|
|
3940
4271
|
} else {
|
|
3941
4272
|
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
@@ -3947,9 +4278,9 @@ async function runDoctor(input, deps = {}) {
|
|
|
3947
4278
|
checks.push({
|
|
3948
4279
|
id: "runtime_config",
|
|
3949
4280
|
label: "Runtime MCP config",
|
|
3950
|
-
ok,
|
|
4281
|
+
level: ok ? "ok" : "failed",
|
|
3951
4282
|
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)" : ""}.`,
|
|
3952
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair
|
|
4283
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair${runtimeFlagFor(input2.runtime)}` }
|
|
3953
4284
|
});
|
|
3954
4285
|
}
|
|
3955
4286
|
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
@@ -3996,16 +4327,44 @@ async function runDoctor(input, deps = {}) {
|
|
|
3996
4327
|
);
|
|
3997
4328
|
}
|
|
3998
4329
|
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
4330
|
+
const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
|
|
4331
|
+
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
3999
4332
|
checks.push({
|
|
4000
4333
|
id: "superseded_agents",
|
|
4001
4334
|
label: "Superseded agent credentials",
|
|
4002
|
-
|
|
4003
|
-
detail:
|
|
4004
|
-
...
|
|
4335
|
+
level: supersededLevel,
|
|
4336
|
+
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("; ")}.`,
|
|
4337
|
+
...supersededLevel === "failed" ? {
|
|
4005
4338
|
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.`
|
|
4339
|
+
} : supersededLevel === "advisory" ? {
|
|
4340
|
+
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.`
|
|
4006
4341
|
} : {}
|
|
4007
4342
|
});
|
|
4008
4343
|
}
|
|
4344
|
+
const bindings = (await Promise.all(
|
|
4345
|
+
inventory.filter((entry) => entry.classification !== "retired").map(async (entry) => ({ entry, binding: await readMcpServerBinding(entry.directory) }))
|
|
4346
|
+
)).filter((item) => item.binding !== null);
|
|
4347
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4348
|
+
for (const item of bindings) {
|
|
4349
|
+
const list = byName.get(item.binding.server_name) ?? [];
|
|
4350
|
+
list.push(item);
|
|
4351
|
+
byName.set(item.binding.server_name, list);
|
|
4352
|
+
}
|
|
4353
|
+
const rebound = [...byName.entries()].filter(([, items]) => items.length > 1);
|
|
4354
|
+
if (rebound.length > 0) {
|
|
4355
|
+
const parts = rebound.map(([name, items]) => {
|
|
4356
|
+
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);
|
|
4357
|
+
const backends = new Set(ordered.map((i) => withoutUserinfo(i.binding.api_url)));
|
|
4358
|
+
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)" : "");
|
|
4359
|
+
});
|
|
4360
|
+
checks.push({
|
|
4361
|
+
id: "mcp_server_name_rebound",
|
|
4362
|
+
label: "MCP server names bound more than once",
|
|
4363
|
+
level: "advisory",
|
|
4364
|
+
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.`,
|
|
4365
|
+
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4009
4368
|
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
4010
4369
|
if (parkedElsewhere.length > 0) {
|
|
4011
4370
|
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
@@ -4013,7 +4372,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
4013
4372
|
checks.push({
|
|
4014
4373
|
id: "rekey_pending_elsewhere",
|
|
4015
4374
|
label: "Parked re-keys in other credential directories",
|
|
4016
|
-
|
|
4375
|
+
level: abandoned.length === 0 ? "ok" : "failed",
|
|
4017
4376
|
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(", ")}.`,
|
|
4018
4377
|
...abandoned.length > 0 ? {
|
|
4019
4378
|
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."
|
|
@@ -4022,20 +4381,34 @@ async function runDoctor(input, deps = {}) {
|
|
|
4022
4381
|
}
|
|
4023
4382
|
const signerProcess = primaryChecksById.get("signer_process");
|
|
4024
4383
|
if (signerProcess) checks.push(signerProcess);
|
|
4025
|
-
const
|
|
4384
|
+
const prune = await (deps.pruneSignerRuntimes ?? pruneSignerRuntimes)({ dryRun: true, measure: false }, { homeDir, credentialsDir: input.credentialsDir });
|
|
4385
|
+
const unused = prune.entries.filter((entry) => entry.action === "would_remove");
|
|
4386
|
+
if (unused.length > 0) {
|
|
4387
|
+
checks.push({
|
|
4388
|
+
id: "signer_runtime_unused",
|
|
4389
|
+
label: "Unused signer-runtime directories",
|
|
4390
|
+
level: "advisory",
|
|
4391
|
+
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).`,
|
|
4392
|
+
repair: `Run: ${RERUN} --prune-signer-runtimes (add --dry-run to list only).`
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
const restart = restartRequiredForRuntime(input2.runtime, deps.env);
|
|
4026
4396
|
checks.push({
|
|
4027
4397
|
id: "restart",
|
|
4028
4398
|
label: "Runtime restart",
|
|
4029
|
-
|
|
4030
|
-
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."
|
|
4399
|
+
level: "ok",
|
|
4400
|
+
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."
|
|
4031
4401
|
});
|
|
4032
|
-
const
|
|
4402
|
+
const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
|
|
4403
|
+
const finalChecks = checks.map(finalizeCheck);
|
|
4404
|
+
const level = rollUpLevel([...finalChecks, ...wiredChecks]);
|
|
4033
4405
|
return {
|
|
4034
4406
|
version: 1,
|
|
4035
|
-
ok:
|
|
4036
|
-
|
|
4407
|
+
ok: level !== "failed",
|
|
4408
|
+
level,
|
|
4409
|
+
runtime: input2.runtime,
|
|
4037
4410
|
credentialDirectory: primaryDirectory,
|
|
4038
|
-
checks,
|
|
4411
|
+
checks: finalChecks,
|
|
4039
4412
|
agents: inventory,
|
|
4040
4413
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
4041
4414
|
};
|
|
@@ -4053,6 +4426,21 @@ async function runRepair(input, deps = {}) {
|
|
|
4053
4426
|
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
4054
4427
|
};
|
|
4055
4428
|
}
|
|
4429
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4430
|
+
if (resolution.origin === "record") {
|
|
4431
|
+
messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${join(directory, CONNECT_OUTCOME_FILENAME)}.`);
|
|
4432
|
+
}
|
|
4433
|
+
const runtime = resolution.runtime;
|
|
4434
|
+
const input2 = { ...input, runtime };
|
|
4435
|
+
if (runtime === "") {
|
|
4436
|
+
return {
|
|
4437
|
+
ok: false,
|
|
4438
|
+
messages: [
|
|
4439
|
+
"Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
|
|
4440
|
+
`Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4441
|
+
]
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4056
4444
|
let identity;
|
|
4057
4445
|
try {
|
|
4058
4446
|
identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
@@ -4062,7 +4450,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4062
4450
|
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
4063
4451
|
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
4064
4452
|
}
|
|
4065
|
-
const configPath = runtimeConfigPathFor(
|
|
4453
|
+
const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
|
|
4066
4454
|
if (configPath) {
|
|
4067
4455
|
try {
|
|
4068
4456
|
const existing = await readFile(configPath, "utf8");
|
|
@@ -4089,7 +4477,10 @@ async function runRepair(input, deps = {}) {
|
|
|
4089
4477
|
const names = serverNamesFor(serverName);
|
|
4090
4478
|
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
4091
4479
|
const configResult = await writeRuntimeConfig({
|
|
4092
|
-
|
|
4480
|
+
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
4481
|
+
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
4482
|
+
// — a repair that reported success while writing nothing.
|
|
4483
|
+
runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
|
|
4093
4484
|
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
4094
4485
|
apiKey: identity.api_key,
|
|
4095
4486
|
identityPath: join(directory, "identity.json"),
|
|
@@ -4107,6 +4498,7 @@ async function runRepair(input, deps = {}) {
|
|
|
4107
4498
|
var RERUN;
|
|
4108
4499
|
var init_doctor = __esm({
|
|
4109
4500
|
"src/doctor.ts"() {
|
|
4501
|
+
init_prune_runtimes();
|
|
4110
4502
|
init_runtime_manifest();
|
|
4111
4503
|
init_probes();
|
|
4112
4504
|
init_signer_runtime();
|
|
@@ -4420,7 +4812,7 @@ async function pathExists2(path) {
|
|
|
4420
4812
|
init_unwire();
|
|
4421
4813
|
init_local_mcp_runtime();
|
|
4422
4814
|
init_runtime_manifest();
|
|
4423
|
-
var CONNECTOR_VERSION = "0.
|
|
4815
|
+
var CONNECTOR_VERSION = "0.4.0-alpha.0";
|
|
4424
4816
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4425
4817
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4426
4818
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -4547,6 +4939,27 @@ async function executeConnect(options, deps, trace) {
|
|
|
4547
4939
|
}
|
|
4548
4940
|
}
|
|
4549
4941
|
}
|
|
4942
|
+
const existingAgents = await listExistingKeyedAgents(options.credentialsDir);
|
|
4943
|
+
if (existingAgents.length > 0) {
|
|
4944
|
+
log("");
|
|
4945
|
+
log(
|
|
4946
|
+
`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."
|
|
4947
|
+
);
|
|
4948
|
+
}
|
|
4949
|
+
const hostedNameForRun = serverNamesFor(serverName).hosted;
|
|
4950
|
+
let reboundFrom;
|
|
4951
|
+
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);
|
|
4952
|
+
const newest = holders[0];
|
|
4953
|
+
if (newest) {
|
|
4954
|
+
const { binding, directory } = newest;
|
|
4955
|
+
const backendChanged = withoutUserinfo(binding.api_url) !== withoutUserinfo(options.apiBaseUrl);
|
|
4956
|
+
const previousApiUrl = withoutUserinfo(binding.api_url);
|
|
4957
|
+
reboundFrom = { server_name: binding.server_name, agent_id: binding.agent_id, api_url: previousApiUrl, bound_at: binding.bound_at, backend_changed: backendChanged };
|
|
4958
|
+
const beingReplaced = replacing?.superseded.some((entry) => entry.directory === directory) ?? false;
|
|
4959
|
+
log(
|
|
4960
|
+
`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." : ".")
|
|
4961
|
+
);
|
|
4962
|
+
}
|
|
4550
4963
|
const localKey = generateKey();
|
|
4551
4964
|
const localApiKey = generateLocalApiKey();
|
|
4552
4965
|
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
@@ -4610,6 +5023,21 @@ async function executeConnect(options, deps, trace) {
|
|
|
4610
5023
|
warn: log
|
|
4611
5024
|
});
|
|
4612
5025
|
trace.directory = credentialPaths.directory;
|
|
5026
|
+
try {
|
|
5027
|
+
const bindingRecord = {
|
|
5028
|
+
version: 1,
|
|
5029
|
+
server_name: hostedNameForRun,
|
|
5030
|
+
signer_name: serverNamesFor(serverName).signer,
|
|
5031
|
+
agent_id: registration.agent_id,
|
|
5032
|
+
api_url: withoutUserinfo(options.apiBaseUrl),
|
|
5033
|
+
// never persist `user:pass@` (#3154 doc review r2)
|
|
5034
|
+
...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
|
|
5035
|
+
bound_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5036
|
+
};
|
|
5037
|
+
await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
|
|
5038
|
+
} catch {
|
|
5039
|
+
log("Could not record the MCP server-name binding locally (non-fatal; the next setup cannot see this binding).");
|
|
5040
|
+
}
|
|
4613
5041
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
4614
5042
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
4615
5043
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -4689,6 +5117,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4689
5117
|
replacedBy: registration.agent_id
|
|
4690
5118
|
});
|
|
4691
5119
|
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
5120
|
+
await clearMcpServerBinding(entry.directory);
|
|
4692
5121
|
retiredAgentIds.push(entry.agentId);
|
|
4693
5122
|
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
4694
5123
|
} catch (err) {
|
|
@@ -4758,6 +5187,8 @@ async function executeConnect(options, deps, trace) {
|
|
|
4758
5187
|
supersededAgentIds,
|
|
4759
5188
|
supersededAgentsRetiredLocally,
|
|
4760
5189
|
...replacing ? { retiredAgentIds } : {},
|
|
5190
|
+
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
5191
|
+
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
4761
5192
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4762
5193
|
approvalRequired: registration.agent_status === "pending_approval",
|
|
4763
5194
|
approvalUrl: registration.approval_url
|
|
@@ -4822,6 +5253,10 @@ function completionOutcome(input) {
|
|
|
4822
5253
|
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
4823
5254
|
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
4824
5255
|
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
5256
|
+
// #3122: always emitted on a completed run (empty list included), for the
|
|
5257
|
+
// same reason as superseded_agent_ids above.
|
|
5258
|
+
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
5259
|
+
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
4825
5260
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4826
5261
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
4827
5262
|
};
|
|
@@ -5006,7 +5441,7 @@ function describeWaitBound(timeoutMs) {
|
|
|
5006
5441
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
5007
5442
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
5008
5443
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
5009
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
5444
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
|
|
5010
5445
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
5011
5446
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
5012
5447
|
let waitingAnnounced = false;
|
|
@@ -5097,6 +5532,32 @@ function activationInstructionWithWhy(profile) {
|
|
|
5097
5532
|
return profile.activationInstruction;
|
|
5098
5533
|
}
|
|
5099
5534
|
var RERUN_HINT = connectorRerunCommand();
|
|
5535
|
+
async function listExistingKeyedAgents(baseDir) {
|
|
5536
|
+
const root = defaultCredentialRoot(baseDir);
|
|
5537
|
+
let entries = [];
|
|
5538
|
+
try {
|
|
5539
|
+
entries = await readdir(root);
|
|
5540
|
+
} catch {
|
|
5541
|
+
return [];
|
|
5542
|
+
}
|
|
5543
|
+
const out = [];
|
|
5544
|
+
for (const entry of entries) {
|
|
5545
|
+
const directory = join(root, entry);
|
|
5546
|
+
try {
|
|
5547
|
+
const identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
5548
|
+
if (typeof identity.api_key !== "string" || identity.api_key.length === 0) continue;
|
|
5549
|
+
let agent = {};
|
|
5550
|
+
try {
|
|
5551
|
+
agent = JSON.parse(await readFile(join(directory, "agent.json"), "utf8"));
|
|
5552
|
+
} catch {
|
|
5553
|
+
}
|
|
5554
|
+
const { accountAddress } = readStoredAccountAddress(identity, agent);
|
|
5555
|
+
out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
|
|
5556
|
+
} catch {
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
return out;
|
|
5560
|
+
}
|
|
5100
5561
|
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
5101
5562
|
const root = defaultCredentialRoot(baseDir);
|
|
5102
5563
|
let entries = [];
|
|
@@ -5145,6 +5606,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
5145
5606
|
let tombstoneReason;
|
|
5146
5607
|
let tombstoneReplacedBy;
|
|
5147
5608
|
let unwire;
|
|
5609
|
+
let destroyKeyMaterial = false;
|
|
5610
|
+
let pruneSignerRuntimes2 = false;
|
|
5611
|
+
let dryRun = false;
|
|
5148
5612
|
let unwireDir;
|
|
5149
5613
|
let replace = false;
|
|
5150
5614
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -5172,6 +5636,12 @@ function parseArgs(argv, env = process.env) {
|
|
|
5172
5636
|
unwireDir = next;
|
|
5173
5637
|
i += 1;
|
|
5174
5638
|
}
|
|
5639
|
+
} else if (arg === "--destroy-key-material") {
|
|
5640
|
+
destroyKeyMaterial = true;
|
|
5641
|
+
} else if (arg === "--prune-signer-runtimes") {
|
|
5642
|
+
pruneSignerRuntimes2 = true;
|
|
5643
|
+
} else if (arg === "--dry-run") {
|
|
5644
|
+
dryRun = true;
|
|
5175
5645
|
} else if (arg === "--reason") {
|
|
5176
5646
|
tombstoneReason = requireValue(argv, ++i, arg);
|
|
5177
5647
|
} else if (arg === "--replaced-by") {
|
|
@@ -5214,7 +5684,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
5214
5684
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5215
5685
|
}
|
|
5216
5686
|
if (replace) {
|
|
5217
|
-
if (rekeyPhase || tombstoneDir || unwire || doctor || repair) {
|
|
5687
|
+
if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
|
|
5218
5688
|
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.");
|
|
5219
5689
|
}
|
|
5220
5690
|
if (options.serverName) {
|
|
@@ -5224,6 +5694,22 @@ function parseArgs(argv, env = process.env) {
|
|
|
5224
5694
|
}
|
|
5225
5695
|
options.replaceExistingWiring = true;
|
|
5226
5696
|
}
|
|
5697
|
+
if (destroyKeyMaterial && !unwire) {
|
|
5698
|
+
throw new Error("--destroy-key-material only applies to --unwire.");
|
|
5699
|
+
}
|
|
5700
|
+
if (destroyKeyMaterial && unwire) unwire = { ...unwire, destroyKeyMaterial: true };
|
|
5701
|
+
if (dryRun && !pruneSignerRuntimes2) {
|
|
5702
|
+
throw new Error("--dry-run only applies to --prune-signer-runtimes.");
|
|
5703
|
+
}
|
|
5704
|
+
if (pruneSignerRuntimes2) {
|
|
5705
|
+
if (unwire || tombstoneDir || rekeyPhase || doctor || repair) {
|
|
5706
|
+
throw new Error("--prune-signer-runtimes is its own operation; run it alone.");
|
|
5707
|
+
}
|
|
5708
|
+
if (options.setupToken) {
|
|
5709
|
+
throw new Error("--prune-signer-runtimes takes no --setup token; it reads stored state only.");
|
|
5710
|
+
}
|
|
5711
|
+
return { options, help, json, doctor, repair, tombstone, rekey, unwire, unwireDir, pruneSignerRuntimes: { dryRun } };
|
|
5712
|
+
}
|
|
5227
5713
|
if (rekey) {
|
|
5228
5714
|
if (options.setupToken) {
|
|
5229
5715
|
throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
|
|
@@ -5318,7 +5804,7 @@ function helpText() {
|
|
|
5318
5804
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
5319
5805
|
" --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
|
|
5320
5806
|
" --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
|
|
5321
|
-
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on
|
|
5807
|
+
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
|
|
5322
5808
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
5323
5809
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
5324
5810
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
@@ -5345,6 +5831,12 @@ function helpText() {
|
|
|
5345
5831
|
" API key are removed locally (record kept via the #2155 tombstone mirror) and",
|
|
5346
5832
|
" nothing is ever revoked on the backend \u2014 that stays an owner action on the",
|
|
5347
5833
|
" Haven agent page.",
|
|
5834
|
+
" --destroy-key-material With --unwire: destroy the signer key + stored API key even when the agent",
|
|
5835
|
+
" is still active or cannot be verified (#3123). Local sweep recovery ends.",
|
|
5836
|
+
" --prune-signer-runtimes Remove ~/.haven/signer-runtime directories no credential directory references",
|
|
5837
|
+
" (version- and override-keyed alike); --dry-run lists them. Exits 1 only on a",
|
|
5838
|
+
" removal that failed (#3123).",
|
|
5839
|
+
" --dry-run With --prune-signer-runtimes: report, remove nothing.",
|
|
5348
5840
|
" --reason <text> Reason recorded in the tombstone (with --tombstone or --unwire).",
|
|
5349
5841
|
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone or --unwire).",
|
|
5350
5842
|
" --help Show this help.",
|
|
@@ -5387,6 +5879,13 @@ function failSubcommand(io, json, err, envelope, fallback) {
|
|
|
5387
5879
|
}
|
|
5388
5880
|
return 1;
|
|
5389
5881
|
}
|
|
5882
|
+
function levelMarker(level) {
|
|
5883
|
+
return level === "ok" ? "\u2713" : level === "advisory" ? "!" : "\u2717";
|
|
5884
|
+
}
|
|
5885
|
+
function advisoryCount(report) {
|
|
5886
|
+
const others = report.agents.filter((agent) => agent.classification === "wired" && agent.directory !== report.credentialDirectory).flatMap((agent) => agent.checks);
|
|
5887
|
+
return [...report.checks, ...others].filter((check) => check.level === "advisory").length;
|
|
5888
|
+
}
|
|
5390
5889
|
async function runCli(argv, io = {
|
|
5391
5890
|
stdout: (message) => process.stdout.write(message),
|
|
5392
5891
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -5414,13 +5913,13 @@ async function runCli(argv, io = {
|
|
|
5414
5913
|
}
|
|
5415
5914
|
if (parsed.tombstone) {
|
|
5416
5915
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
5417
|
-
const { readFile:
|
|
5418
|
-
const { join:
|
|
5916
|
+
const { readFile: readFile15 } = await import('fs/promises');
|
|
5917
|
+
const { join: join13 } = await import('path');
|
|
5419
5918
|
try {
|
|
5420
5919
|
let agentId = "unknown";
|
|
5421
5920
|
try {
|
|
5422
5921
|
const identity = JSON.parse(
|
|
5423
|
-
await
|
|
5922
|
+
await readFile15(join13(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
5424
5923
|
);
|
|
5425
5924
|
agentId = identity.agent_id ?? "unknown";
|
|
5426
5925
|
} catch {
|
|
@@ -5458,20 +5957,22 @@ async function runCli(argv, io = {
|
|
|
5458
5957
|
}
|
|
5459
5958
|
if (parsed.unwire) {
|
|
5460
5959
|
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
5461
|
-
const { homedir:
|
|
5462
|
-
const { join:
|
|
5463
|
-
const homeDir =
|
|
5464
|
-
const root = parsed.options.credentialsDir ??
|
|
5465
|
-
const directory = parsed.unwireDir ?? (parsed.options.serverName ?
|
|
5960
|
+
const { homedir: homedir11 } = await import('os');
|
|
5961
|
+
const { join: join13 } = await import('path');
|
|
5962
|
+
const homeDir = homedir11();
|
|
5963
|
+
const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
|
|
5964
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join13(root, parsed.options.serverName) : root);
|
|
5466
5965
|
try {
|
|
5467
5966
|
const result = await unwireAgent2({
|
|
5468
5967
|
directory,
|
|
5469
5968
|
slug: parsed.options.serverName,
|
|
5470
5969
|
reason: parsed.unwire.reason,
|
|
5471
5970
|
replacedBy: parsed.unwire.replacedBy,
|
|
5971
|
+
destroyKeyMaterial: parsed.unwire.destroyKeyMaterial,
|
|
5472
5972
|
homeDir
|
|
5473
5973
|
});
|
|
5474
5974
|
const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
|
|
5975
|
+
const retained = result.teardown.status === "retained";
|
|
5475
5976
|
if (parsed.json) {
|
|
5476
5977
|
io.stdout(
|
|
5477
5978
|
`${redactSecrets(
|
|
@@ -5486,7 +5987,16 @@ async function runCli(argv, io = {
|
|
|
5486
5987
|
label: r.label,
|
|
5487
5988
|
status: r.status,
|
|
5488
5989
|
...r.detail ? { detail: r.detail } : {}
|
|
5489
|
-
}))
|
|
5990
|
+
})),
|
|
5991
|
+
// #3122: additive — whether the local server-name binding record was released.
|
|
5992
|
+
binding_released: result.bindingReleased,
|
|
5993
|
+
// #3123: additive — what happened to the key material and why.
|
|
5994
|
+
teardown: {
|
|
5995
|
+
status: result.teardown.status,
|
|
5996
|
+
probe: result.teardown.probe,
|
|
5997
|
+
detail: result.teardown.detail,
|
|
5998
|
+
...result.teardown.remedy ? { remedy: result.teardown.remedy } : {}
|
|
5999
|
+
}
|
|
5490
6000
|
})
|
|
5491
6001
|
)}
|
|
5492
6002
|
`
|
|
@@ -5502,14 +6012,20 @@ async function runCli(argv, io = {
|
|
|
5502
6012
|
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
5503
6013
|
`));
|
|
5504
6014
|
}
|
|
6015
|
+
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");
|
|
6016
|
+
const t = result.teardown;
|
|
6017
|
+
io.stdout(redactSecrets(` ${t.status === "retained" ? "\u2717" : t.status === "forced" ? "!" : "\u2713"} Key material: ${t.status} (probe: ${t.probe}) \u2014 ${t.detail}
|
|
6018
|
+
`));
|
|
6019
|
+
if (t.remedy) io.stdout(redactSecrets(` \u21B3 ${t.remedy}
|
|
6020
|
+
`));
|
|
5505
6021
|
io.stdout(
|
|
5506
|
-
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"
|
|
6022
|
+
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"
|
|
5507
6023
|
);
|
|
5508
6024
|
io.stdout(
|
|
5509
|
-
"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\
|
|
6025
|
+
"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"
|
|
5510
6026
|
);
|
|
5511
6027
|
}
|
|
5512
|
-
return failures.length > 0 ? 1 : 0;
|
|
6028
|
+
return failures.length > 0 || retained ? 1 : 0;
|
|
5513
6029
|
} catch (err) {
|
|
5514
6030
|
return failSubcommand(io, parsed.json, err, { unwired: false }, {
|
|
5515
6031
|
code: "unwire_failed",
|
|
@@ -5517,6 +6033,57 @@ async function runCli(argv, io = {
|
|
|
5517
6033
|
});
|
|
5518
6034
|
}
|
|
5519
6035
|
}
|
|
6036
|
+
if (parsed.pruneSignerRuntimes) {
|
|
6037
|
+
const { pruneSignerRuntimes: pruneSignerRuntimes2 } = await Promise.resolve().then(() => (init_prune_runtimes(), prune_runtimes_exports));
|
|
6038
|
+
try {
|
|
6039
|
+
const report = await pruneSignerRuntimes2(
|
|
6040
|
+
{ dryRun: parsed.pruneSignerRuntimes.dryRun },
|
|
6041
|
+
{ credentialsDir: parsed.options.credentialsDir }
|
|
6042
|
+
);
|
|
6043
|
+
if (parsed.json) {
|
|
6044
|
+
io.stdout(`${redactSecrets(JSON.stringify({
|
|
6045
|
+
pruned: true,
|
|
6046
|
+
version: report.version,
|
|
6047
|
+
dry_run: report.dryRun,
|
|
6048
|
+
level: report.level,
|
|
6049
|
+
root: report.root,
|
|
6050
|
+
removed: report.removed,
|
|
6051
|
+
reclaimed_bytes: report.reclaimedBytes,
|
|
6052
|
+
entries: report.entries.map((e) => ({
|
|
6053
|
+
key: e.key,
|
|
6054
|
+
kind: e.kind,
|
|
6055
|
+
bytes: e.bytes,
|
|
6056
|
+
action: e.action,
|
|
6057
|
+
level: e.level,
|
|
6058
|
+
referenced_by: e.referencedBy,
|
|
6059
|
+
detail: e.detail
|
|
6060
|
+
}))
|
|
6061
|
+
}))}
|
|
6062
|
+
`);
|
|
6063
|
+
} else {
|
|
6064
|
+
io.stdout(`Signer runtimes under ${report.root}${report.dryRun ? " (dry run \u2014 nothing removed)" : ""}:
|
|
6065
|
+
`);
|
|
6066
|
+
if (report.entries.length === 0) io.stdout(" (none)\n");
|
|
6067
|
+
for (const e of report.entries) {
|
|
6068
|
+
const mark = e.level === "failed" ? "\u2717" : e.level === "advisory" ? "!" : e.action === "removed" ? "\u2713" : "\u2022";
|
|
6069
|
+
const size = e.bytes > 0 ? `${Math.round(e.bytes / 1024 / 1024)} MB` : e.action === "kept" ? "not sized" : "0 MB";
|
|
6070
|
+
io.stdout(redactSecrets(` ${mark} ${e.key} (${e.kind}, ${size}): ${e.detail}
|
|
6071
|
+
`));
|
|
6072
|
+
}
|
|
6073
|
+
io.stdout(
|
|
6074
|
+
report.dryRun ? `Would remove ${report.entries.filter((e) => e.action === "would_remove").length} director(y/ies); re-run without --dry-run to reclaim.
|
|
6075
|
+
` : `Removed ${report.removed} director(y/ies), reclaimed ${Math.round(report.reclaimedBytes / 1024 / 1024)} MB.
|
|
6076
|
+
`
|
|
6077
|
+
);
|
|
6078
|
+
}
|
|
6079
|
+
return report.level === "failed" ? 1 : 0;
|
|
6080
|
+
} catch (err) {
|
|
6081
|
+
return failSubcommand(io, parsed.json, err, { pruned: false }, {
|
|
6082
|
+
code: "prune_failed",
|
|
6083
|
+
nextAction: "review_the_error_and_rerun_prune_which_is_idempotent"
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
5520
6087
|
if (parsed.rekey) {
|
|
5521
6088
|
const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
|
|
5522
6089
|
const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
|
|
@@ -5593,7 +6160,7 @@ async function runCli(argv, io = {
|
|
|
5593
6160
|
`);
|
|
5594
6161
|
} else {
|
|
5595
6162
|
for (const check of report.checks) {
|
|
5596
|
-
io.stdout(redactSecrets(`${check.
|
|
6163
|
+
io.stdout(redactSecrets(`${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5597
6164
|
`));
|
|
5598
6165
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5599
6166
|
`));
|
|
@@ -5603,21 +6170,25 @@ async function runCli(argv, io = {
|
|
|
5603
6170
|
io.stdout("\nOther agents on this machine:\n");
|
|
5604
6171
|
for (const agent of otherAgents) {
|
|
5605
6172
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
5606
|
-
const failed = agent.checks.filter((check) =>
|
|
5607
|
-
const
|
|
5608
|
-
|
|
6173
|
+
const failed = agent.checks.filter((check) => check.level === "failed");
|
|
6174
|
+
const advised = agent.checks.filter((check) => check.level === "advisory");
|
|
6175
|
+
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;
|
|
6176
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
|
|
5609
6177
|
`));
|
|
5610
|
-
for (const check of failed) {
|
|
5611
|
-
io.stdout(redactSecrets(`
|
|
6178
|
+
for (const check of [...failed, ...advised]) {
|
|
6179
|
+
io.stdout(redactSecrets(` ${levelMarker(check.level)} ${check.label}: ${check.detail}
|
|
5612
6180
|
`));
|
|
5613
6181
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
5614
6182
|
`));
|
|
5615
6183
|
}
|
|
5616
6184
|
}
|
|
5617
6185
|
}
|
|
5618
|
-
io.stdout(
|
|
6186
|
+
io.stdout(
|
|
6187
|
+
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.
|
|
6188
|
+
` : "All checks passed.\n"
|
|
6189
|
+
);
|
|
5619
6190
|
}
|
|
5620
|
-
return report.
|
|
6191
|
+
return report.level === "failed" ? 1 : 0;
|
|
5621
6192
|
} catch (err) {
|
|
5622
6193
|
return failSubcommand(io, parsed.json, err, { doctor: "failed" }, {
|
|
5623
6194
|
code: "doctor_failed",
|