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