@haven_ai/connect 0.0.0-dev.202609250737.3bd5a51 → 0.0.0-dev.202609251409.98ed67a
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/dist/cli.cjs +2162 -2148
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2166 -2152
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2166 -2152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -5
- package/dist/index.d.ts +12 -5
- package/dist/index.js +2149 -2133
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24,10 +24,11 @@ var __export = (target, all) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// src/api.ts
|
|
27
|
-
function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
27
|
+
function createConnectApiClient(baseUrl, fetchImpl = fetch, clientIdentity) {
|
|
28
28
|
const root = baseUrl.replace(/\/+$/, "");
|
|
29
|
+
const send = (url, init) => request(fetchImpl, url, clientIdentity ? { ...init, headers: { ...init.headers, "X-Haven-Client": clientIdentity } } : init);
|
|
29
30
|
return {
|
|
30
|
-
resolveSetup: (input) =>
|
|
31
|
+
resolveSetup: (input) => send(`${root}/agent-connection-setups/resolve`, {
|
|
31
32
|
method: "POST",
|
|
32
33
|
body: JSON.stringify({
|
|
33
34
|
setup_token: input.setupToken,
|
|
@@ -35,7 +36,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
35
36
|
runtime: input.runtime
|
|
36
37
|
})
|
|
37
38
|
}),
|
|
38
|
-
registerSetup: (input) =>
|
|
39
|
+
registerSetup: (input) => send(`${root}/agent-connection-setups/register`, {
|
|
39
40
|
method: "POST",
|
|
40
41
|
body: JSON.stringify({
|
|
41
42
|
setup_token: input.setupToken,
|
|
@@ -55,16 +56,16 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
55
56
|
}
|
|
56
57
|
})
|
|
57
58
|
}),
|
|
58
|
-
getAgentIdentity: (apiKey) =>
|
|
59
|
+
getAgentIdentity: (apiKey) => send(`${root}/machine-payments/agent`, {
|
|
59
60
|
method: "GET",
|
|
60
61
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
61
62
|
}),
|
|
62
|
-
getConnectorStatus: (setupId, apiKey) =>
|
|
63
|
+
getConnectorStatus: (setupId, apiKey) => send(`${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
|
|
63
64
|
method: "GET",
|
|
64
65
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
65
66
|
}),
|
|
66
67
|
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
67
|
-
await
|
|
68
|
+
await send(`${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
68
69
|
method: "POST",
|
|
69
70
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
70
71
|
body: JSON.stringify({
|
|
@@ -611,9 +612,9 @@ var init_runtime_manifest = __esm({
|
|
|
611
612
|
mcpPackage: "@haven_ai/mcp",
|
|
612
613
|
mcpVersion: MCP_VERSION,
|
|
613
614
|
sdkPackage: "@haven_ai/sdk",
|
|
614
|
-
sdkVersion: "0.0.0-dev.
|
|
615
|
+
sdkVersion: "0.0.0-dev.202609251409.98ed67a",
|
|
615
616
|
signerPackage: "@haven_ai/signer",
|
|
616
|
-
signerVersion: "0.0.0-dev.
|
|
617
|
+
signerVersion: "0.0.0-dev.202609251409.98ed67a",
|
|
617
618
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
618
619
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
619
620
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -2988,6 +2989,185 @@ var init_runtime_install = __esm({
|
|
|
2988
2989
|
execFileAsync3 = promisify(execFile);
|
|
2989
2990
|
}
|
|
2990
2991
|
});
|
|
2992
|
+
function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env = process.env) {
|
|
2993
|
+
const targets = [
|
|
2994
|
+
{
|
|
2995
|
+
runtime: "claude-code",
|
|
2996
|
+
label: "Claude Code",
|
|
2997
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2998
|
+
// not by writing a file this module owns — so its evidence is the
|
|
2999
|
+
// client directory, never a config path.
|
|
3000
|
+
configPath: null,
|
|
3001
|
+
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
runtime: "codex-cli",
|
|
3005
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
3006
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
3007
|
+
// whose answers are the same write.
|
|
3008
|
+
label: "Codex (CLI or Desktop)",
|
|
3009
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
3010
|
+
markers: [join(homeDir, ".codex")]
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
runtime: "cursor",
|
|
3014
|
+
label: "Cursor",
|
|
3015
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
3016
|
+
markers: [join(homeDir, ".cursor")]
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
runtime: "vscode",
|
|
3020
|
+
label: "VS Code",
|
|
3021
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3022
|
+
markers: [resolve(cwd, ".vscode")]
|
|
3023
|
+
},
|
|
3024
|
+
{
|
|
3025
|
+
runtime: "vscode-insiders",
|
|
3026
|
+
label: "VS Code Insiders",
|
|
3027
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3028
|
+
markers: []
|
|
3029
|
+
},
|
|
3030
|
+
{
|
|
3031
|
+
runtime: "claude-desktop",
|
|
3032
|
+
label: "Claude Desktop",
|
|
3033
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3034
|
+
markers: []
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
runtime: "hermes",
|
|
3038
|
+
label: "Hermes Agent",
|
|
3039
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3040
|
+
markers: [env.HERMES_HOME ?? join(homeDir, ".hermes")]
|
|
3041
|
+
}
|
|
3042
|
+
];
|
|
3043
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3044
|
+
}
|
|
3045
|
+
async function scanInstalledClients(options = {}) {
|
|
3046
|
+
const exists = options.exists ?? pathExists;
|
|
3047
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3048
|
+
const found = [];
|
|
3049
|
+
for (const target of targets) {
|
|
3050
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3051
|
+
found.push({
|
|
3052
|
+
runtime: target.runtime,
|
|
3053
|
+
label: target.label,
|
|
3054
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3055
|
+
configPath: target.configPath,
|
|
3056
|
+
evidence: "config-file"
|
|
3057
|
+
});
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
for (const marker of target.markers) {
|
|
3061
|
+
if (!await exists(marker)) continue;
|
|
3062
|
+
found.push({
|
|
3063
|
+
runtime: target.runtime,
|
|
3064
|
+
label: target.label,
|
|
3065
|
+
detail: `installed (${marker})`,
|
|
3066
|
+
configPath: target.configPath,
|
|
3067
|
+
evidence: "client-directory"
|
|
3068
|
+
});
|
|
3069
|
+
break;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return found.sort((a, b) => {
|
|
3073
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3074
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
function installedClientHint(candidates) {
|
|
3078
|
+
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3079
|
+
if (candidates.length === 1) {
|
|
3080
|
+
return { installedClients, suggestedRuntime: candidates[0].runtime };
|
|
3081
|
+
}
|
|
3082
|
+
const configured = candidates.filter((candidate) => candidate.evidence === "config-file");
|
|
3083
|
+
return configured.length === 1 ? { installedClients, suggestedRuntime: configured[0].runtime } : { installedClients };
|
|
3084
|
+
}
|
|
3085
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3086
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3087
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3088
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3089
|
+
candidates.forEach((candidate, index) => {
|
|
3090
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3091
|
+
`);
|
|
3092
|
+
});
|
|
3093
|
+
io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
|
|
3094
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3095
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3096
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3097
|
+
const trimmed = answer.trim();
|
|
3098
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3099
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3100
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3101
|
+
return candidates[picked - 1].runtime;
|
|
3102
|
+
}
|
|
3103
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3104
|
+
`);
|
|
3105
|
+
}
|
|
3106
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3107
|
+
}
|
|
3108
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3109
|
+
const candidates = await scanInstalledClients(options);
|
|
3110
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3111
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3112
|
+
}
|
|
3113
|
+
function noInstalledClientsError() {
|
|
3114
|
+
return new ConnectError(
|
|
3115
|
+
"runtime_no_installed_clients",
|
|
3116
|
+
"Could not determine the agent runtime: nothing was detected in this environment, and no agent client Haven can configure is installed on this machine. Re-run with --runtime <name> naming the client you want configured, or --runtime other to store credentials and finish the MCP setup by hand.",
|
|
3117
|
+
"rerun_connect_with_explicit_runtime"
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
function promptAbortedError(reason) {
|
|
3121
|
+
return new ConnectError(
|
|
3122
|
+
"runtime_prompt_aborted",
|
|
3123
|
+
`Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the connector command again, or pass --runtime <name> to skip the prompt.`,
|
|
3124
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
async function pathExists(path) {
|
|
3128
|
+
try {
|
|
3129
|
+
await access(path);
|
|
3130
|
+
return true;
|
|
3131
|
+
} catch {
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function defaultPromptIo() {
|
|
3136
|
+
return {
|
|
3137
|
+
write: (text) => process.stdout.write(text),
|
|
3138
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3139
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3140
|
+
let settled = false;
|
|
3141
|
+
const settle = (value) => {
|
|
3142
|
+
if (settled) return;
|
|
3143
|
+
settled = true;
|
|
3144
|
+
rl.close();
|
|
3145
|
+
resolvePromise(value);
|
|
3146
|
+
};
|
|
3147
|
+
rl.once("SIGINT", () => settle(null));
|
|
3148
|
+
rl.once("close", () => settle(null));
|
|
3149
|
+
rl.question(query, (answer) => settle(answer));
|
|
3150
|
+
})
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
var SCAN_ORDER, MAX_PROMPT_ATTEMPTS;
|
|
3154
|
+
var init_installed_clients = __esm({
|
|
3155
|
+
"src/installed-clients.ts"() {
|
|
3156
|
+
init_connect_error();
|
|
3157
|
+
init_config_writers();
|
|
3158
|
+
init_runtime_registry();
|
|
3159
|
+
SCAN_ORDER = [
|
|
3160
|
+
"claude-code",
|
|
3161
|
+
"codex-cli",
|
|
3162
|
+
"cursor",
|
|
3163
|
+
"vscode",
|
|
3164
|
+
"vscode-insiders",
|
|
3165
|
+
"claude-desktop",
|
|
3166
|
+
"hermes"
|
|
3167
|
+
];
|
|
3168
|
+
MAX_PROMPT_ATTEMPTS = 3;
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
2991
3171
|
|
|
2992
3172
|
// src/tombstone.ts
|
|
2993
3173
|
var tombstone_exports = {};
|
|
@@ -3120,49 +3300,160 @@ var init_tombstone = __esm({
|
|
|
3120
3300
|
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
3121
3301
|
}
|
|
3122
3302
|
});
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3303
|
+
async function detectWiringCollision(input) {
|
|
3304
|
+
const root = defaultCredentialRoot(input.credentialsDir);
|
|
3305
|
+
let entries = [];
|
|
3306
|
+
try {
|
|
3307
|
+
entries = await readdir(root);
|
|
3308
|
+
} catch {
|
|
3309
|
+
return null;
|
|
3310
|
+
}
|
|
3311
|
+
const superseded = [];
|
|
3312
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3313
|
+
for (const entry of entries) {
|
|
3314
|
+
const directory = join(root, entry);
|
|
3315
|
+
let identityRaw;
|
|
3316
|
+
try {
|
|
3317
|
+
identityRaw = await readFile(join(directory, "identity.json"), "utf8");
|
|
3318
|
+
} catch {
|
|
3319
|
+
continue;
|
|
3320
|
+
}
|
|
3321
|
+
taken.add(entry);
|
|
3322
|
+
let identity;
|
|
3323
|
+
try {
|
|
3324
|
+
identity = JSON.parse(identityRaw);
|
|
3325
|
+
} catch {
|
|
3326
|
+
identity = void 0;
|
|
3327
|
+
}
|
|
3328
|
+
if (!identity?.api_key) continue;
|
|
3329
|
+
if (await pathExists2(join(directory, TOMBSTONE_FILENAME))) continue;
|
|
3330
|
+
const sidecar = await readRuntimeSidecar(directory);
|
|
3331
|
+
if (sidecar?.server_name) continue;
|
|
3332
|
+
superseded.push({ directory, agentId: identity.agent_id ?? entry });
|
|
3143
3333
|
}
|
|
3334
|
+
if (superseded.length === 0) return null;
|
|
3335
|
+
return {
|
|
3336
|
+
superseded,
|
|
3337
|
+
suggestedServerName: proposeServerSlug(input.agentName, taken)
|
|
3338
|
+
};
|
|
3144
3339
|
}
|
|
3145
|
-
function
|
|
3146
|
-
|
|
3147
|
-
if (!
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3340
|
+
function proposeServerSlug(agentName, taken) {
|
|
3341
|
+
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
|
|
3342
|
+
if (!slugIsValid(base)) base = "agent";
|
|
3343
|
+
if (!taken.has(base)) return base;
|
|
3344
|
+
for (let n = 2; n < 1e3; n += 1) {
|
|
3345
|
+
const suffix = `-${n}`;
|
|
3346
|
+
const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
3347
|
+
if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
|
|
3152
3348
|
}
|
|
3153
|
-
|
|
3349
|
+
throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
|
|
3154
3350
|
}
|
|
3155
|
-
|
|
3351
|
+
function slugIsValid(slug) {
|
|
3156
3352
|
try {
|
|
3157
|
-
|
|
3353
|
+
assertValidServerSlug(slug);
|
|
3354
|
+
return true;
|
|
3158
3355
|
} catch {
|
|
3159
|
-
return
|
|
3356
|
+
return false;
|
|
3160
3357
|
}
|
|
3161
3358
|
}
|
|
3162
|
-
async function
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
3359
|
+
async function promptWiringCollisionResolution(collision, agentName, io) {
|
|
3360
|
+
const ids = collision.superseded.map((entry) => entry.agentId).join(", ");
|
|
3361
|
+
io.write(`This machine is already wired to a Haven agent with a live key: ${ids}.
|
|
3362
|
+
`);
|
|
3363
|
+
io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
|
|
3364
|
+
`);
|
|
3365
|
+
io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
|
|
3366
|
+
io.write(" (tombstoned, local key files removed; you still revoke it on the Haven agent page)\n");
|
|
3367
|
+
io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
|
|
3368
|
+
`);
|
|
3369
|
+
io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
|
|
3370
|
+
`);
|
|
3371
|
+
io.write(" q) quit \u2014 nothing is written and the setup token stays unused\n");
|
|
3372
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS2; attempt += 1) {
|
|
3373
|
+
const answer = await io.question("Replace, install alongside, or quit? [r/a/q]: ");
|
|
3374
|
+
if (answer === null) return { action: "abort" };
|
|
3375
|
+
const trimmed = answer.trim().toLowerCase();
|
|
3376
|
+
if (trimmed === "r" || trimmed === "replace") return { action: "replace" };
|
|
3377
|
+
if (trimmed === "q" || trimmed === "quit") return { action: "abort" };
|
|
3378
|
+
if (trimmed === "a" || trimmed === "alongside") {
|
|
3379
|
+
const typed = await io.question(`Server name [${collision.suggestedServerName}]: `);
|
|
3380
|
+
if (typed === null) return { action: "abort" };
|
|
3381
|
+
const serverName = typed.trim() === "" ? collision.suggestedServerName : typed.trim();
|
|
3382
|
+
try {
|
|
3383
|
+
assertValidServerSlug(serverName);
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
io.write(`${err instanceof Error ? err.message : String(err)}
|
|
3386
|
+
`);
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
return { action: "alongside", serverName };
|
|
3390
|
+
}
|
|
3391
|
+
io.write(`"${trimmed}" is not one of r, a, q.
|
|
3392
|
+
`);
|
|
3393
|
+
}
|
|
3394
|
+
return { action: "abort" };
|
|
3395
|
+
}
|
|
3396
|
+
async function pathExists2(path) {
|
|
3397
|
+
try {
|
|
3398
|
+
await stat(path);
|
|
3399
|
+
return true;
|
|
3400
|
+
} catch {
|
|
3401
|
+
return false;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
var MAX_PROMPT_ATTEMPTS2;
|
|
3405
|
+
var init_wiring_collision = __esm({
|
|
3406
|
+
"src/wiring-collision.ts"() {
|
|
3407
|
+
init_server_names();
|
|
3408
|
+
init_signer_runtime();
|
|
3409
|
+
init_storage();
|
|
3410
|
+
init_tombstone();
|
|
3411
|
+
MAX_PROMPT_ATTEMPTS2 = 3;
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
// src/unwire.ts
|
|
3416
|
+
var unwire_exports = {};
|
|
3417
|
+
__export(unwire_exports, {
|
|
3418
|
+
readIdentityFile: () => readIdentityFile,
|
|
3419
|
+
teardownLocalKeyMaterial: () => teardownLocalKeyMaterial,
|
|
3420
|
+
tombstoneDirectoryIfAbsent: () => tombstoneDirectoryIfAbsent,
|
|
3421
|
+
unwireAgent: () => unwireAgent
|
|
3422
|
+
});
|
|
3423
|
+
function identityAt(directory) {
|
|
3424
|
+
return readFile(join(directory, "identity.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
|
|
3425
|
+
}
|
|
3426
|
+
function removeForModel(text, model, names, path) {
|
|
3427
|
+
switch (model.kind) {
|
|
3428
|
+
case "yaml":
|
|
3429
|
+
return removeHermesYaml(text, names, path);
|
|
3430
|
+
case "toml":
|
|
3431
|
+
return removeCodexToml(text, names);
|
|
3432
|
+
case "json":
|
|
3433
|
+
return removeJsonMcpConfig(text, model.serverRoot ?? "mcpServers", names, path);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function envLineValue(envText, envKey) {
|
|
3437
|
+
const line = envText.split(/\r?\n/).find((candidate) => new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(candidate));
|
|
3438
|
+
if (!line) return void 0;
|
|
3439
|
+
let value = line.slice(line.indexOf("=") + 1);
|
|
3440
|
+
value = value.trim();
|
|
3441
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3442
|
+
value = value.slice(1, -1);
|
|
3443
|
+
}
|
|
3444
|
+
return value;
|
|
3445
|
+
}
|
|
3446
|
+
async function readOptionalText(path) {
|
|
3447
|
+
try {
|
|
3448
|
+
return await readFile(path, "utf8");
|
|
3449
|
+
} catch {
|
|
3450
|
+
return null;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
async function unwireAgent(input) {
|
|
3454
|
+
const homeDir = input.homeDir ?? homedir();
|
|
3455
|
+
const [identity, sidecar] = await Promise.all([identityAt(input.directory), readRuntimeSidecar(input.directory)]);
|
|
3456
|
+
const agentId = identity?.agent_id ?? "unknown";
|
|
3166
3457
|
const slug = input.slug ?? sidecar?.server_name;
|
|
3167
3458
|
const names = serverNamesFor(slug);
|
|
3168
3459
|
const runtimes = [];
|
|
@@ -3352,2294 +3643,2016 @@ var init_unwire = __esm({
|
|
|
3352
3643
|
DESTROY_FLAG = "--destroy-key-material";
|
|
3353
3644
|
}
|
|
3354
3645
|
});
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
"src/rekey-messages.ts"() {
|
|
3360
|
-
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
3646
|
+
function failureOutcomeFor(runtimeHint, error) {
|
|
3647
|
+
if (error !== null && typeof error === "object") {
|
|
3648
|
+
const recorded = failureOutcomesByError.get(error);
|
|
3649
|
+
if (recorded) return recorded;
|
|
3361
3650
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
// src/prune-runtimes.ts
|
|
3365
|
-
var prune_runtimes_exports = {};
|
|
3366
|
-
__export(prune_runtimes_exports, {
|
|
3367
|
-
normalizeRuntimePath: () => normalizeRuntimePath,
|
|
3368
|
-
pruneSignerRuntimes: () => pruneSignerRuntimes,
|
|
3369
|
-
referencedRuntimeDirectories: () => referencedRuntimeDirectories,
|
|
3370
|
-
rollUpPruneLevel: () => rollUpPruneLevel,
|
|
3371
|
-
signerRuntimeRoot: () => signerRuntimeRoot
|
|
3372
|
-
});
|
|
3373
|
-
function signerRuntimeRoot(homeDir) {
|
|
3374
|
-
return join(homeDir, ".haven", "signer-runtime");
|
|
3375
|
-
}
|
|
3376
|
-
function classifyKey(key) {
|
|
3377
|
-
if (key.startsWith("override-")) return "override";
|
|
3378
|
-
if (key === MCP_RUNTIME_MANIFEST.signerVersion || /^\d+\.\d+\.\d+/.test(key)) return "version";
|
|
3379
|
-
return "unknown";
|
|
3651
|
+
return failedConnectOutcome(runtimeHint, error);
|
|
3380
3652
|
}
|
|
3381
|
-
async function
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
const path = join(dir, entry.name);
|
|
3392
|
-
if (entry.isDirectory()) await walk(path);
|
|
3393
|
-
else if (entry.isFile()) {
|
|
3394
|
-
try {
|
|
3395
|
-
total += (await stat(path)).size;
|
|
3396
|
-
} catch {
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
};
|
|
3401
|
-
await walk(directory);
|
|
3402
|
-
return total;
|
|
3653
|
+
async function runConnect(options, deps = {}) {
|
|
3654
|
+
const trace = {};
|
|
3655
|
+
try {
|
|
3656
|
+
return await executeConnect(options, deps, trace);
|
|
3657
|
+
} catch (err) {
|
|
3658
|
+
const outcome = failedConnectOutcome(trace.runtime ?? options.runtime, err);
|
|
3659
|
+
if (err !== null && typeof err === "object") failureOutcomesByError.set(err, outcome);
|
|
3660
|
+
if (trace.directory) await recordConnectOutcome(deps, trace.directory, outcome);
|
|
3661
|
+
throw err;
|
|
3662
|
+
}
|
|
3403
3663
|
}
|
|
3404
|
-
async function
|
|
3405
|
-
const resolved = resolve(path).replace(/[\\/]+$/, "");
|
|
3664
|
+
async function recordConnectOutcome(deps, directory, outcome) {
|
|
3406
3665
|
try {
|
|
3407
|
-
return await
|
|
3666
|
+
return await (deps.writeOutcomeRecord ?? writeConnectOutcomeRecord)(directory, outcome);
|
|
3408
3667
|
} catch {
|
|
3409
|
-
return
|
|
3668
|
+
return void 0;
|
|
3410
3669
|
}
|
|
3411
3670
|
}
|
|
3412
|
-
function
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
const
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
}
|
|
3671
|
+
async function executeConnect(options, deps, trace) {
|
|
3672
|
+
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
3673
|
+
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
3674
|
+
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl, void 0, CONNECTOR_CLIENT_IDENTITY);
|
|
3675
|
+
const log = secureLogger(
|
|
3676
|
+
deps.log ?? ((message) => process.stdout.write(`${message}
|
|
3677
|
+
`)),
|
|
3678
|
+
deps.redactPaths === true
|
|
3679
|
+
);
|
|
3680
|
+
const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
|
|
3681
|
+
const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
|
|
3682
|
+
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
3683
|
+
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
3684
|
+
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
3685
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3686
|
+
env: deps.env ?? process.env,
|
|
3687
|
+
selfReported: options.runtimeSelfReport,
|
|
3688
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3689
|
+
});
|
|
3690
|
+
if (!selection.runtime) {
|
|
3691
|
+
const hint = await installedClientHintFor(deps);
|
|
3692
|
+
throw new ConnectError(
|
|
3693
|
+
"runtime_undetermined",
|
|
3694
|
+
`Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. ` + installedClientProse(hint) + "Nothing was written and the Haven setup token is still unused.",
|
|
3695
|
+
"rerun_connect_with_explicit_runtime",
|
|
3696
|
+
// #2091: the values must ride structurally too. The backend's setup
|
|
3697
|
+
// prompt permits a retry only with "one of the values that refusal
|
|
3698
|
+
// lists" — and --json discards prose, so a prose-only list deadlocked
|
|
3699
|
+
// every automation run in an undetected runtime (Codex in the field:
|
|
3700
|
+
// npx needs network, Codex runs network commands unsandboxed, and the
|
|
3701
|
+
// unsandboxed path carries none of the CODEX_* detection vars).
|
|
3702
|
+
//
|
|
3703
|
+
// #2174: the list alone still leaves the retry a nine-value guess. The
|
|
3704
|
+
// scan below narrows it to what is actually on this machine — as a
|
|
3705
|
+
// HINT. It does not select, and this stays a refusal: see
|
|
3706
|
+
// `installedClientHint`.
|
|
3707
|
+
{ allowedRuntimes: RUNTIME_FLAG_VALUE_LIST, ...hint }
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3710
|
+
const runtime = selection.runtime;
|
|
3711
|
+
trace.runtime = runtime;
|
|
3712
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
3713
|
+
if (options.localMcp) {
|
|
3714
|
+
if (!supportsLocalMcp(runtime)) {
|
|
3715
|
+
throw new Error(
|
|
3716
|
+
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(runtime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
|
|
3717
|
+
);
|
|
3449
3718
|
}
|
|
3450
3719
|
}
|
|
3451
|
-
|
|
3452
|
-
}
|
|
3453
|
-
function rollUpPruneLevel(entries) {
|
|
3454
|
-
if (entries.some((e) => e.level === "failed")) return "failed";
|
|
3455
|
-
if (entries.some((e) => e.level === "advisory")) return "advisory";
|
|
3456
|
-
return "ok";
|
|
3457
|
-
}
|
|
3458
|
-
async function pruneSignerRuntimes(input, deps = {}) {
|
|
3459
|
-
const homeDir = deps.homeDir ?? homedir();
|
|
3460
|
-
const root = signerRuntimeRoot(homeDir);
|
|
3461
|
-
const measure = input.measure ?? true;
|
|
3462
|
-
const referenced = await referencedRuntimeDirectories(homeDir, deps.credentialsDir);
|
|
3463
|
-
const pin = await normalizeRuntimePath(join(root, MCP_RUNTIME_MANIFEST.signerVersion));
|
|
3464
|
-
const remove = deps.rm ?? (async (directory) => rm(directory, { recursive: true, force: false }));
|
|
3465
|
-
const entries = [];
|
|
3466
|
-
let keys = [];
|
|
3467
|
-
try {
|
|
3468
|
-
keys = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
3469
|
-
} catch {
|
|
3470
|
-
return { version: 1, root, dryRun: input.dryRun, entries, removed: 0, reclaimedBytes: 0, level: "ok" };
|
|
3720
|
+
if (selection.overrodeHint) {
|
|
3721
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3471
3722
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
for (const key of keys) {
|
|
3475
|
-
const directory = join(root, key);
|
|
3476
|
-
const normalized = await normalizeRuntimePath(directory);
|
|
3477
|
-
const referencedBy = referenced.get(normalized) ?? [];
|
|
3478
|
-
const kind = classifyKey(key);
|
|
3479
|
-
if (referencedBy.length > 0 || normalized === pin) {
|
|
3480
|
-
entries.push({
|
|
3481
|
-
directory,
|
|
3482
|
-
key,
|
|
3483
|
-
kind,
|
|
3484
|
-
bytes: 0,
|
|
3485
|
-
referencedBy,
|
|
3486
|
-
action: "kept",
|
|
3487
|
-
level: "ok",
|
|
3488
|
-
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)"
|
|
3489
|
-
});
|
|
3490
|
-
continue;
|
|
3491
|
-
}
|
|
3492
|
-
const bytes = measure ? await directoryBytes(directory) : 0;
|
|
3493
|
-
if (input.dryRun) {
|
|
3494
|
-
entries.push({
|
|
3495
|
-
directory,
|
|
3496
|
-
key,
|
|
3497
|
-
kind,
|
|
3498
|
-
bytes,
|
|
3499
|
-
referencedBy,
|
|
3500
|
-
action: "would_remove",
|
|
3501
|
-
level: "advisory",
|
|
3502
|
-
detail: `would remove \u2014 no credential directory names it (${kind}-keyed)`
|
|
3503
|
-
});
|
|
3504
|
-
continue;
|
|
3505
|
-
}
|
|
3506
|
-
try {
|
|
3507
|
-
await remove(directory);
|
|
3508
|
-
removed += 1;
|
|
3509
|
-
reclaimedBytes += bytes;
|
|
3510
|
-
entries.push({ directory, key, kind, bytes, referencedBy, action: "removed", level: "ok", detail: `removed \u2014 no credential directory named it (${kind}-keyed)` });
|
|
3511
|
-
} catch (err) {
|
|
3512
|
-
entries.push({
|
|
3513
|
-
directory,
|
|
3514
|
-
key,
|
|
3515
|
-
kind,
|
|
3516
|
-
bytes,
|
|
3517
|
-
referencedBy,
|
|
3518
|
-
action: "failed",
|
|
3519
|
-
level: "failed",
|
|
3520
|
-
detail: `removal failed: ${err instanceof Error ? err.message : String(err)} \u2014 a signer process may still hold it open; stop it and re-run`
|
|
3521
|
-
});
|
|
3522
|
-
}
|
|
3723
|
+
if (selection.discardedHint) {
|
|
3724
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${RUNTIME_FLAG_VALUES})`);
|
|
3523
3725
|
}
|
|
3524
|
-
|
|
3525
|
-
}
|
|
3526
|
-
var init_prune_runtimes = __esm({
|
|
3527
|
-
"src/prune-runtimes.ts"() {
|
|
3528
|
-
init_runtime_manifest();
|
|
3726
|
+
if (selection.source === "prompted") {
|
|
3727
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3529
3728
|
}
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
});
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const stored = await readStoredCredentials(
|
|
3541
|
-
options.serverName,
|
|
3542
|
-
options.agentId,
|
|
3543
|
-
options.credentialsDir
|
|
3544
|
-
);
|
|
3545
|
-
const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
|
|
3546
|
-
const identity = await probeIdentity(api, stored.apiKey, "current");
|
|
3547
|
-
assertRekeyable(identity, stored);
|
|
3548
|
-
const key = (deps.generateKey ?? generateDelegateKey)();
|
|
3549
|
-
const startedAt = new Date(now()).toISOString();
|
|
3550
|
-
const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
|
|
3551
|
-
await writeRekeyPending(stored.directory, {
|
|
3552
|
-
agent_id: stored.agentId,
|
|
3553
|
-
new_delegate_address: key.address,
|
|
3554
|
-
new_delegate_key: key.privateKey,
|
|
3555
|
-
started_at: startedAt,
|
|
3556
|
-
expires_at: expiresAt
|
|
3557
|
-
});
|
|
3558
|
-
const finishCommand = [
|
|
3559
|
-
connectorRerunCommand("--rekey-finish"),
|
|
3560
|
-
options.serverName ? `--name ${options.serverName}` : void 0,
|
|
3561
|
-
"--api-key <the key the dashboard showed you>",
|
|
3562
|
-
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
3563
|
-
].filter(Boolean).join(" ");
|
|
3564
|
-
return {
|
|
3565
|
-
started: true,
|
|
3566
|
-
agentId: stored.agentId,
|
|
3567
|
-
directory: stored.directory,
|
|
3568
|
-
newDelegateAddress: key.address,
|
|
3569
|
-
expiresAt,
|
|
3570
|
-
messages: [
|
|
3571
|
-
`New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
|
|
3572
|
-
"",
|
|
3573
|
-
` New signing address: ${key.address}`,
|
|
3574
|
-
"",
|
|
3575
|
-
"The private half stays here \u2014 Haven never receives it, and there is no way to move it",
|
|
3576
|
-
"between machines. That is what keeps the account non-custodial.",
|
|
3577
|
-
"",
|
|
3578
|
-
'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
|
|
3579
|
-
"When it finishes it shows a new API key ONCE. Come back here and run:",
|
|
3580
|
-
"",
|
|
3581
|
-
` ${finishCommand}`,
|
|
3582
|
-
"",
|
|
3583
|
-
`Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
|
|
3584
|
-
`This pending re-key expires ${expiresAt}.`
|
|
3585
|
-
]
|
|
3586
|
-
};
|
|
3587
|
-
}
|
|
3588
|
-
async function finishRekey(options, deps = {}) {
|
|
3589
|
-
const now = deps.now ?? (() => Date.now());
|
|
3590
|
-
if (!options.newApiKey) {
|
|
3591
|
-
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
3729
|
+
log("Warming up your connection to Haven\u2026");
|
|
3730
|
+
let setup;
|
|
3731
|
+
try {
|
|
3732
|
+
setup = await api.resolveSetup({
|
|
3733
|
+
setupToken: options.setupToken,
|
|
3734
|
+
connectorVersion,
|
|
3735
|
+
runtime
|
|
3736
|
+
});
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
throw deadSetupTokenError(err) ?? err;
|
|
3592
3739
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
if (pending.agent_id !== stored.agentId) {
|
|
3600
|
-
throw new Error(
|
|
3601
|
-
`The pending re-key at ${stored.directory} belongs to agent ${pending.agent_id}, but that directory now holds ${stored.agentId}. Refusing to write a key into the wrong agent.`
|
|
3602
|
-
);
|
|
3740
|
+
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
3741
|
+
printSetupSummary(setup, log);
|
|
3742
|
+
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3743
|
+
if (options.serverName) {
|
|
3744
|
+
assertValidServerSlug(options.serverName);
|
|
3745
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3603
3746
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3747
|
+
log("Checked local credential storage \u2014 all clear.");
|
|
3748
|
+
let serverName = options.serverName;
|
|
3749
|
+
let replacing;
|
|
3750
|
+
if (!serverName) {
|
|
3751
|
+
const collision = await detectWiringCollision({
|
|
3752
|
+
credentialsDir: options.credentialsDir,
|
|
3753
|
+
agentName: setup.agent.name
|
|
3754
|
+
});
|
|
3755
|
+
if (collision) {
|
|
3756
|
+
const resolution = await resolveWiringCollision(collision, setup.agent.name, options, deps);
|
|
3757
|
+
if (resolution.action === "replace") {
|
|
3758
|
+
replacing = collision;
|
|
3759
|
+
log(
|
|
3760
|
+
`Replacing the existing haven / haven-signer wiring (previous agent(s): ${supersededIds(collision)}). The previous directory is retired locally once the new wiring is written; nothing is revoked.`
|
|
3761
|
+
);
|
|
3762
|
+
} else {
|
|
3763
|
+
serverName = resolution.serverName;
|
|
3764
|
+
assertValidServerSlug(serverName);
|
|
3765
|
+
await assertServerSlugAvailable(serverName, options.credentialsDir);
|
|
3766
|
+
log(`Installing alongside the existing wiring as a named agent: ${serverNamesFor(serverName).hosted} / ${serverNamesFor(serverName).signer}.`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
const existingAgents = await listExistingKeyedAgents(options.credentialsDir);
|
|
3771
|
+
if (existingAgents.length > 0) {
|
|
3772
|
+
log("");
|
|
3773
|
+
log(
|
|
3774
|
+
`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."
|
|
3609
3775
|
);
|
|
3610
3776
|
}
|
|
3611
|
-
const
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3777
|
+
const hostedNameForRun = serverNamesFor(serverName).hosted;
|
|
3778
|
+
let reboundFrom;
|
|
3779
|
+
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);
|
|
3780
|
+
const newest = holders[0];
|
|
3781
|
+
if (newest) {
|
|
3782
|
+
const { binding, directory } = newest;
|
|
3783
|
+
const backendChanged = withoutUserinfo(binding.api_url) !== withoutUserinfo(options.apiBaseUrl);
|
|
3784
|
+
const previousApiUrl = withoutUserinfo(binding.api_url);
|
|
3785
|
+
reboundFrom = { server_name: binding.server_name, agent_id: binding.agent_id, api_url: previousApiUrl, bound_at: binding.bound_at, backend_changed: backendChanged };
|
|
3786
|
+
const beingReplaced = replacing?.superseded.some((entry) => entry.directory === directory) ?? false;
|
|
3787
|
+
log(
|
|
3788
|
+
`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." : ".")
|
|
3616
3789
|
);
|
|
3617
3790
|
}
|
|
3618
|
-
|
|
3791
|
+
const localKey = generateKey();
|
|
3792
|
+
const localApiKey = generateLocalApiKey();
|
|
3793
|
+
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
3794
|
+
const proofSignature = await localKey.signChallenge(setup.challenge.message);
|
|
3795
|
+
log("Introducing your agent to Haven\u2026");
|
|
3796
|
+
let registration;
|
|
3797
|
+
try {
|
|
3798
|
+
registration = await api.registerSetup({
|
|
3799
|
+
setupToken: options.setupToken,
|
|
3800
|
+
connectorVersion,
|
|
3801
|
+
runtime,
|
|
3802
|
+
challengeId: setup.challenge.id,
|
|
3803
|
+
delegateAddress: localKey.address,
|
|
3804
|
+
proofSignature,
|
|
3805
|
+
apiKeyHash: hashAgentApiKey(localApiKey),
|
|
3806
|
+
apiKeyPrefix: agentApiKeyPrefix(localApiKey),
|
|
3807
|
+
// #1878: report the pair we are ACTUALLY wiring, bare pair included, so
|
|
3808
|
+
// the dashboard can name it. Derived here rather than sent as the raw
|
|
3809
|
+
// slug — `serverNamesFor` is the one place the naming rule lives, and
|
|
3810
|
+
// the hosted name is what a user pastes into an MCP config.
|
|
3811
|
+
mcpServerName: serverNamesFor(serverName).hosted,
|
|
3812
|
+
// #2528: 'prose' is the default because it is what a caller who says
|
|
3813
|
+
// nothing is doing — the library entry point is not a --json run.
|
|
3814
|
+
runMode: options.runMode ?? "prose",
|
|
3815
|
+
connectorContext: {
|
|
3816
|
+
environment_label: options.environmentLabel ?? "Local workspace",
|
|
3817
|
+
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
3818
|
+
},
|
|
3819
|
+
installCapabilities
|
|
3820
|
+
});
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
const dead = deadSetupTokenError(err);
|
|
3823
|
+
if (dead) throw dead;
|
|
3824
|
+
if (isExpiredSetupChallenge(err)) {
|
|
3825
|
+
throw new Error(
|
|
3826
|
+
"The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new connector command. Do not reuse or paste credentials."
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
throw err;
|
|
3830
|
+
}
|
|
3831
|
+
log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
|
|
3832
|
+
log("Tucking your credentials away safely on disk\u2026");
|
|
3833
|
+
const credentialPaths = await writeCredentials({
|
|
3619
3834
|
baseDir: options.credentialsDir,
|
|
3620
|
-
agentId:
|
|
3621
|
-
serverName
|
|
3622
|
-
apiKey:
|
|
3623
|
-
delegateKey:
|
|
3624
|
-
delegateAddress:
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3835
|
+
agentId: registration.agent_id,
|
|
3836
|
+
serverName,
|
|
3837
|
+
apiKey: localApiKey,
|
|
3838
|
+
delegateKey: localKey.privateKey,
|
|
3839
|
+
delegateAddress: localKey.address,
|
|
3840
|
+
accountAddress: setup.haven_wallet.address,
|
|
3841
|
+
chainId: setup.haven_wallet.chain_id,
|
|
3842
|
+
network: setup.haven_wallet.network,
|
|
3843
|
+
agentBudget: setup.agent_budget.map((budget) => ({
|
|
3844
|
+
token_symbol: budget.token_symbol,
|
|
3845
|
+
allowance_amount: budget.allowance_amount,
|
|
3846
|
+
reset_period_min: budget.reset_period_min
|
|
3847
|
+
})),
|
|
3848
|
+
apiUrl: options.apiBaseUrl,
|
|
3849
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
3850
|
+
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
3851
|
+
warn: log
|
|
3636
3852
|
});
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
identityPath: `${stored.directory}/identity.json`,
|
|
3662
|
-
signerPath: `${stored.directory}/signer.json`,
|
|
3663
|
-
credentialDirectory: stored.directory,
|
|
3664
|
-
serverName: options.serverName
|
|
3665
|
-
},
|
|
3666
|
-
{ command: prepared.command, args: prepared.args }
|
|
3853
|
+
trace.directory = credentialPaths.directory;
|
|
3854
|
+
try {
|
|
3855
|
+
const bindingRecord = {
|
|
3856
|
+
version: 1,
|
|
3857
|
+
server_name: hostedNameForRun,
|
|
3858
|
+
signer_name: serverNamesFor(serverName).signer,
|
|
3859
|
+
agent_id: registration.agent_id,
|
|
3860
|
+
api_url: withoutUserinfo(options.apiBaseUrl),
|
|
3861
|
+
// never persist `user:pass@` (#3154 doc review r2)
|
|
3862
|
+
...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
|
|
3863
|
+
bound_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3864
|
+
};
|
|
3865
|
+
await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
|
|
3866
|
+
} catch {
|
|
3867
|
+
log("Could not record the MCP server-name binding locally (non-fatal; the next setup cannot see this binding).");
|
|
3868
|
+
}
|
|
3869
|
+
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
3870
|
+
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
3871
|
+
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
3872
|
+
if (setup.x402_binding_signer) {
|
|
3873
|
+
log("Configured x402 binding signer for the local signer.");
|
|
3874
|
+
} else {
|
|
3875
|
+
log(
|
|
3876
|
+
"Warning: Haven did not provide an x402 binding signer, so x402 payments will not sign until HAVEN_X402_BINDING_SIGNER is set for the signer. Non-x402 payments are unaffected."
|
|
3667
3877
|
);
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3878
|
+
}
|
|
3879
|
+
const runtimeInstall = await runRuntimeInstall({
|
|
3880
|
+
runtime,
|
|
3881
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
3882
|
+
apiKey: localApiKey,
|
|
3883
|
+
signerPath: credentialPaths.signerPath,
|
|
3884
|
+
identityPath: credentialPaths.identityPath,
|
|
3885
|
+
credentialDirectory: credentialPaths.directory,
|
|
3886
|
+
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
3887
|
+
ackSigner: options.ackSigner,
|
|
3888
|
+
ackLocalTools: options.ackLocalTools,
|
|
3889
|
+
localMcp: options.localMcp,
|
|
3890
|
+
serverName
|
|
3891
|
+
}, {
|
|
3892
|
+
onProgress: log,
|
|
3893
|
+
// #1543: report "runtime configured" the moment the config write settles,
|
|
3894
|
+
// so the dashboard can expose its budget-approval controls without
|
|
3895
|
+
// waiting on the probes and skill install — a tail that approval does not
|
|
3896
|
+
// depend on. Silent and best-effort: the complete report below remains
|
|
3897
|
+
// authoritative (it overwrites these keys, adding the probe verdicts and
|
|
3898
|
+
// skill state), so an early failure costs nothing but the head start.
|
|
3899
|
+
onRuntimeConfigured: async (early) => {
|
|
3900
|
+
try {
|
|
3901
|
+
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
3902
|
+
runtime: early.runtime,
|
|
3903
|
+
connectorVersion,
|
|
3904
|
+
runtimeMcpMode: early.runtimeMcpMode,
|
|
3905
|
+
hostedMcpConfigured: early.hostedMcpConfigured,
|
|
3906
|
+
localSignerConfigured: early.localSignerConfigured,
|
|
3907
|
+
localMcpConfigured: early.localMcpConfigured,
|
|
3908
|
+
credentialFilesWritten: true,
|
|
3909
|
+
signerAcknowledged: early.signerAcknowledged,
|
|
3910
|
+
localMcpAcknowledged: early.localMcpAcknowledged,
|
|
3911
|
+
restartRequired: early.restartRequired,
|
|
3912
|
+
nextUserAction: early.nextUserAction,
|
|
3913
|
+
errorCode: early.errorCode ?? null,
|
|
3914
|
+
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
3915
|
+
});
|
|
3916
|
+
if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
|
|
3917
|
+
log(approveBudgetCta(setup, registration.approval_url));
|
|
3918
|
+
}
|
|
3919
|
+
} catch {
|
|
3920
|
+
}
|
|
3673
3921
|
}
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3922
|
+
});
|
|
3923
|
+
printRuntimeInstall(runtimeInstall, log);
|
|
3924
|
+
if (runtimeInstall.errorCode) {
|
|
3925
|
+
log("Haven setup needs a couple more steps on this machine \u2014 see the notes above.");
|
|
3926
|
+
} else {
|
|
3927
|
+
log("Haven setup on this machine is complete.");
|
|
3928
|
+
}
|
|
3929
|
+
let supersededAgentsRetiredLocally;
|
|
3930
|
+
const retiredAgentIds = [];
|
|
3931
|
+
const retirementMirrorErrors = {};
|
|
3932
|
+
if (replacing) {
|
|
3933
|
+
if (runtimeInstall.errorCode) {
|
|
3934
|
+
supersededAgentsRetiredLocally = false;
|
|
3935
|
+
log(
|
|
3936
|
+
`Previous agent(s) ${supersededIds(replacing)} were NOT retired: the runtime install did not complete, so their wiring may still be the only working one. Resolve the install, then retire them with ${RERUN_HINT} --unwire <dir>.`
|
|
3682
3937
|
);
|
|
3938
|
+
} else {
|
|
3939
|
+
supersededAgentsRetiredLocally = true;
|
|
3940
|
+
for (const entry of replacing.superseded) {
|
|
3941
|
+
try {
|
|
3942
|
+
const { mirrorError } = await tombstoneDirectoryIfAbsent({
|
|
3943
|
+
directory: entry.directory,
|
|
3944
|
+
agentId: entry.agentId,
|
|
3945
|
+
reason: "replaced by a new setup (--replace)",
|
|
3946
|
+
replacedBy: registration.agent_id,
|
|
3947
|
+
// #3251: the ledger follows this run's credential root (default
|
|
3948
|
+
// root → ~/.haven/tombstones, as before; any other → <root>/.tombstones).
|
|
3949
|
+
tombstonesDir: tombstonesDirForCredentialRoot(options.credentialsDir)
|
|
3950
|
+
});
|
|
3951
|
+
if (mirrorError !== void 0) {
|
|
3952
|
+
retirementMirrorErrors[entry.agentId] = mirrorError;
|
|
3953
|
+
log(
|
|
3954
|
+
`Warning: the surviving tombstone record for ${entry.agentId} could not be written to ${tombstonesDirForCredentialRoot(options.credentialsDir)} (${mirrorError}). The in-place TOMBSTONE.json stands; --doctor will not list this retirement once the directory is deleted.`
|
|
3955
|
+
);
|
|
3956
|
+
}
|
|
3957
|
+
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
3958
|
+
await clearMcpServerBinding(entry.directory);
|
|
3959
|
+
retiredAgentIds.push(entry.agentId);
|
|
3960
|
+
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
3961
|
+
} catch (err) {
|
|
3962
|
+
supersededAgentsRetiredLocally = false;
|
|
3963
|
+
log(`Could not retire previous agent ${entry.agentId} locally: ${err instanceof Error ? err.message : String(err)}`);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3683
3966
|
}
|
|
3684
|
-
} else {
|
|
3685
|
-
messages.push(
|
|
3686
|
-
"",
|
|
3687
|
-
"NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
|
|
3688
|
-
"wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
|
|
3689
|
-
);
|
|
3690
3967
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
agentId: stored.agentId,
|
|
3694
|
-
directory: stored.directory,
|
|
3695
|
-
newDelegateAddress: pending.new_delegate_address,
|
|
3696
|
-
serverNames: { hosted: names.hosted, signer: names.signer },
|
|
3697
|
-
configRewritten,
|
|
3698
|
-
messages
|
|
3699
|
-
};
|
|
3700
|
-
}
|
|
3701
|
-
async function probeIdentity(api, apiKey, which) {
|
|
3968
|
+
let supersededScan = null;
|
|
3969
|
+
let supersededAgentIds = [];
|
|
3702
3970
|
try {
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3971
|
+
supersededScan = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3972
|
+
supersededAgentIds = supersededScan ?? [];
|
|
3973
|
+
if (supersededAgentsRetiredLocally === true) {
|
|
3974
|
+
log("");
|
|
3975
|
+
log(
|
|
3976
|
+
`Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014 revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors): each holds the MCP wiring snapshot from its own start time, and the tombstone speaks only when a stale host next probes the old path.`
|
|
3977
|
+
);
|
|
3978
|
+
} else if (supersededAgentIds.length > 0) {
|
|
3979
|
+
log("");
|
|
3980
|
+
log(
|
|
3981
|
+
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededAgentIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3982
|
+
);
|
|
3983
|
+
log(
|
|
3984
|
+
`If you meant to replace them: revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
|
|
3709
3985
|
);
|
|
3710
3986
|
}
|
|
3711
|
-
|
|
3712
|
-
`Could not reach Haven to check this agent (${redactSecrets(
|
|
3713
|
-
err instanceof Error ? err.message : String(err)
|
|
3714
|
-
)}). Nothing was changed.`
|
|
3715
|
-
);
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
function assertRekeyable(identity, stored) {
|
|
3719
|
-
if (identity.id !== stored.agentId) {
|
|
3720
|
-
throw new Error(
|
|
3721
|
-
`The credentials at ${stored.directory} say agent ${stored.agentId}, but Haven says that key belongs to ${identity.id}. Refusing to re-key an agent this directory does not own.`
|
|
3722
|
-
);
|
|
3723
|
-
}
|
|
3724
|
-
if (identity.execution_rail === "legacy") {
|
|
3725
|
-
throw new Error(
|
|
3726
|
-
`Agent ${identity.name || identity.id} is on the legacy rail, which cannot be re-keyed \u2014 its authority is per-token Safe allowances, not a signed delegation there is anything to re-issue. Re-onboard the agent on the delegation rail instead.`
|
|
3727
|
-
);
|
|
3728
|
-
}
|
|
3729
|
-
if (identity.status === "revoked") {
|
|
3730
|
-
throw new Error(
|
|
3731
|
-
`Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
|
|
3732
|
-
);
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
var init_rekey = __esm({
|
|
3736
|
-
"src/rekey.ts"() {
|
|
3737
|
-
init_api();
|
|
3738
|
-
init_runtime_install();
|
|
3739
|
-
init_signer_runtime();
|
|
3740
|
-
init_runtime_spec_override();
|
|
3741
|
-
init_key();
|
|
3742
|
-
init_redact();
|
|
3743
|
-
init_rekey_messages();
|
|
3744
|
-
init_server_names();
|
|
3745
|
-
init_storage();
|
|
3987
|
+
} catch {
|
|
3746
3988
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3989
|
+
try {
|
|
3990
|
+
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
3991
|
+
runtime: runtimeInstall.runtime,
|
|
3992
|
+
connectorVersion,
|
|
3993
|
+
runtimeMcpMode: runtimeInstall.runtimeMcpMode,
|
|
3994
|
+
hostedMcpConfigured: runtimeInstall.hostedMcpConfigured,
|
|
3995
|
+
localSignerConfigured: runtimeInstall.localSignerConfigured,
|
|
3996
|
+
localMcpConfigured: runtimeInstall.localMcpConfigured,
|
|
3997
|
+
credentialFilesWritten: true,
|
|
3998
|
+
signerAcknowledged: runtimeInstall.signerAcknowledged,
|
|
3999
|
+
localMcpAcknowledged: runtimeInstall.localMcpAcknowledged,
|
|
4000
|
+
activationCommandAvailable: Boolean(runtimeInstall.activationCommand),
|
|
4001
|
+
skillInstalled: runtimeInstall.skillInstalled,
|
|
4002
|
+
probeResult: runtimeInstall.probeResult,
|
|
4003
|
+
restartRequired: runtimeInstall.restartRequired,
|
|
4004
|
+
nextUserAction: runtimeInstall.nextUserAction,
|
|
4005
|
+
errorCode: runtimeInstall.errorCode,
|
|
4006
|
+
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
4007
|
+
// The raw scan result, `null` and all — NOT the flattened
|
|
4008
|
+
// `supersededAgentIds` the outcome carries. Flattening here would hand
|
|
4009
|
+
// the dashboard the same ambiguity this field exists to remove (#2561).
|
|
4010
|
+
supersededAgentIds: supersededScan
|
|
4011
|
+
});
|
|
4012
|
+
} catch (err) {
|
|
4013
|
+
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
3766
4014
|
}
|
|
3767
|
-
|
|
3768
|
-
|
|
4015
|
+
let approval;
|
|
4016
|
+
const narrateApprovalWait = options.waitForApproval === true || options.waitForApproval === void 0 && (deps.isStdoutTty ?? Boolean(process.stdout.isTTY));
|
|
4017
|
+
if (narrateApprovalWait && !runtimeInstall.errorCode) {
|
|
4018
|
+
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
4019
|
+
}
|
|
4020
|
+
printNextSteps(runtimeInstall, log, approval, registration.approval_url);
|
|
4021
|
+
const outcome = completionOutcome({
|
|
4022
|
+
runtimeInstall,
|
|
4023
|
+
delegateAddress: registration.delegate_address,
|
|
4024
|
+
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4025
|
+
supersededAgentIds,
|
|
4026
|
+
supersededAgentsRetiredLocally,
|
|
4027
|
+
...replacing ? { retiredAgentIds } : {},
|
|
4028
|
+
...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
|
|
4029
|
+
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
4030
|
+
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
4031
|
+
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4032
|
+
approvalRequired: registration.agent_status === "pending_approval",
|
|
4033
|
+
approvalUrl: registration.approval_url
|
|
4034
|
+
});
|
|
4035
|
+
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4036
|
+
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
4037
|
+
}
|
|
4038
|
+
return {
|
|
4039
|
+
setupId: registration.setup_id,
|
|
4040
|
+
agentId: registration.agent_id,
|
|
4041
|
+
delegateAddress: registration.delegate_address,
|
|
4042
|
+
credentialPaths,
|
|
4043
|
+
outcome
|
|
4044
|
+
};
|
|
3769
4045
|
}
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
"
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
}
|
|
4046
|
+
function completionOutcome(input) {
|
|
4047
|
+
const { runtimeInstall } = input;
|
|
4048
|
+
const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
|
|
4049
|
+
const nextAction2 = runtimeInstall.nextUserAction;
|
|
4050
|
+
const outcome = {
|
|
4051
|
+
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
4052
|
+
outcome: runtimeInstall.errorCode ? "action_required" : "complete",
|
|
4053
|
+
runtime: runtimeInstall.runtime,
|
|
4054
|
+
topology: runtimeInstall.runtimeMcpMode,
|
|
4055
|
+
configuration: {
|
|
4056
|
+
hosted_mcp: runtimeInstall.hostedMcpConfigured,
|
|
4057
|
+
local_signer: runtimeInstall.localSignerConfigured,
|
|
4058
|
+
local_mcp: runtimeInstall.localMcpConfigured
|
|
4059
|
+
},
|
|
4060
|
+
probe: { result: runtimeInstall.probeResult },
|
|
4061
|
+
activation: {
|
|
4062
|
+
restart_required: runtimeInstall.restartRequired,
|
|
4063
|
+
instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
|
|
4064
|
+
},
|
|
4065
|
+
next_action: nextAction2,
|
|
4066
|
+
approval: {
|
|
4067
|
+
required: input.approvalRequired,
|
|
4068
|
+
expires_at: null,
|
|
4069
|
+
// Only when there is something to approve AND the backend supplied a
|
|
4070
|
+
// link. Never synthesised here: the connector does not know the
|
|
4071
|
+
// dashboard's origin, and a guessed URL is worse than none — it is the
|
|
4072
|
+
// "do not invent one" rule the agent runbook states, applied to the
|
|
4073
|
+
// tool rather than the agent.
|
|
4074
|
+
...input.approvalRequired && input.approvalUrl ? { url: input.approvalUrl } : {}
|
|
4075
|
+
},
|
|
4076
|
+
verification: {
|
|
4077
|
+
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
4078
|
+
instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
|
|
4079
|
+
},
|
|
4080
|
+
// The backend contract supplies a 20-byte address. If an unexpected
|
|
4081
|
+
// malformed value arrives, do not echo it into an automation-facing
|
|
4082
|
+
// record; the human log has already been redacted separately.
|
|
4083
|
+
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
4084
|
+
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
4085
|
+
// same string that already sits in the user's own config file. Passed
|
|
4086
|
+
// through the secret filter anyway: this is the automation contract, the
|
|
4087
|
+
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
4088
|
+
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
4089
|
+
// Always emitted on a completed run, empty list included: "no superseded
|
|
4090
|
+
// agents" is a fact a caller needs, and an omitted key would be
|
|
4091
|
+
// indistinguishable from an older connector that never reported it.
|
|
4092
|
+
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
4093
|
+
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
4094
|
+
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
4095
|
+
...input.retirementMirrorErrors ? { retirement_mirror_errors: input.retirementMirrorErrors } : {},
|
|
4096
|
+
// #3122: always emitted on a completed run (empty list included), for the
|
|
4097
|
+
// same reason as superseded_agent_ids above.
|
|
4098
|
+
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
4099
|
+
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
4100
|
+
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4101
|
+
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
4102
|
+
};
|
|
4103
|
+
return outcome;
|
|
4104
|
+
}
|
|
4105
|
+
async function installedClientHintFor(deps) {
|
|
4106
|
+
try {
|
|
4107
|
+
const candidates = await (deps.scanInstalledClients ?? scanInstalledClients)({ env: deps.env });
|
|
4108
|
+
const hint = installedClientHint(candidates);
|
|
4109
|
+
return hint.installedClients.length > 0 ? hint : {};
|
|
4110
|
+
} catch {
|
|
4111
|
+
return {};
|
|
3808
4112
|
}
|
|
3809
|
-
});
|
|
3810
|
-
|
|
3811
|
-
// src/doctor.ts
|
|
3812
|
-
var doctor_exports = {};
|
|
3813
|
-
__export(doctor_exports, {
|
|
3814
|
-
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
3815
|
-
rollUpLevel: () => rollUpLevel,
|
|
3816
|
-
runDoctor: () => runDoctor,
|
|
3817
|
-
runRepair: () => runRepair
|
|
3818
|
-
});
|
|
3819
|
-
function finalizeCheck(check) {
|
|
3820
|
-
return { ...check, ok: check.level !== "failed" };
|
|
3821
4113
|
}
|
|
3822
|
-
function
|
|
3823
|
-
|
|
3824
|
-
if (
|
|
3825
|
-
|
|
4114
|
+
function installedClientProse(hint) {
|
|
4115
|
+
const found = hint.installedClients ?? [];
|
|
4116
|
+
if (found.length === 0) return "";
|
|
4117
|
+
const suggestion = hint.suggestedRuntime ? ` The likeliest is ${hint.suggestedRuntime}, but Haven will not choose for you \u2014 pass it yourself if it is right.` : " Haven will not choose between them for you.";
|
|
4118
|
+
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
3826
4119
|
}
|
|
3827
|
-
function
|
|
3828
|
-
|
|
4120
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
4121
|
+
if (!interactivePromptAllowed(options, deps)) return void 0;
|
|
4122
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3829
4123
|
}
|
|
3830
|
-
function
|
|
3831
|
-
if (
|
|
3832
|
-
return
|
|
4124
|
+
function interactivePromptAllowed(options, deps) {
|
|
4125
|
+
if (options.interactive !== true) return false;
|
|
4126
|
+
return deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
3833
4127
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
4128
|
+
function supersededIds(collision) {
|
|
4129
|
+
return collision.superseded.map((entry) => entry.agentId).join(", ");
|
|
4130
|
+
}
|
|
4131
|
+
async function resolveWiringCollision(collision, agentName, options, deps) {
|
|
4132
|
+
if (options.replaceExistingWiring) return { action: "replace" };
|
|
4133
|
+
if (interactivePromptAllowed(options, deps)) {
|
|
4134
|
+
const prompt = deps.promptWiringCollision ?? ((c, name) => promptWiringCollisionResolution(c, name, defaultPromptIo()));
|
|
4135
|
+
const resolution = await prompt(collision, agentName);
|
|
4136
|
+
if (resolution.action === "abort") {
|
|
4137
|
+
throw new ConnectError(
|
|
4138
|
+
"wiring_collision_declined",
|
|
4139
|
+
`Setup stopped at your request: this machine is already wired to ${supersededIds(collision)} and you chose neither to replace that wiring nor to install alongside it. Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the connector command again with --replace to re-point haven / haven-signer at the new agent, or with --name <slug> (e.g. --name ${collision.suggestedServerName}) to install alongside.`,
|
|
4140
|
+
"rerun_connect_with_replace_or_name",
|
|
4141
|
+
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4142
|
+
);
|
|
3841
4143
|
}
|
|
4144
|
+
return resolution;
|
|
3842
4145
|
}
|
|
3843
|
-
|
|
4146
|
+
throw new ConnectError(
|
|
4147
|
+
"wiring_collision",
|
|
4148
|
+
`This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still revoke the old agent on the Haven agent page), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
|
|
4149
|
+
"relay_wiring_collision_to_user",
|
|
4150
|
+
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4151
|
+
);
|
|
3844
4152
|
}
|
|
3845
|
-
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
} catch {
|
|
3851
|
-
return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
|
|
3852
|
-
}
|
|
3853
|
-
const candidates = [];
|
|
3854
|
-
const tombstonedOnly = [];
|
|
3855
|
-
const parkedOnly = [];
|
|
3856
|
-
for (const entry of entries) {
|
|
3857
|
-
const directory = join(root, entry);
|
|
3858
|
-
try {
|
|
3859
|
-
const s = await stat(join(directory, "identity.json"));
|
|
3860
|
-
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
3861
|
-
} catch {
|
|
3862
|
-
try {
|
|
3863
|
-
await stat(join(directory, TOMBSTONE_FILENAME));
|
|
3864
|
-
tombstonedOnly.push(directory);
|
|
3865
|
-
} catch {
|
|
3866
|
-
try {
|
|
3867
|
-
await stat(join(directory, REKEY_PENDING_FILENAME));
|
|
3868
|
-
parkedOnly.push(directory);
|
|
3869
|
-
} catch {
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
}
|
|
3874
|
-
const parkedOnlySet = new Set(parkedOnly);
|
|
3875
|
-
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
3876
|
-
if (explicit) {
|
|
3877
|
-
return {
|
|
3878
|
-
directory: explicit,
|
|
3879
|
-
others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
|
|
3880
|
-
parkedOnly: parkedOnlySet
|
|
3881
|
-
};
|
|
3882
|
-
}
|
|
3883
|
-
if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
|
|
3884
|
-
return { others: [], parkedOnly: parkedOnlySet };
|
|
3885
|
-
}
|
|
4153
|
+
function failedConnectOutcome(runtimeHint, error) {
|
|
4154
|
+
const message = error instanceof Error ? error.message : "";
|
|
4155
|
+
const code = error instanceof ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
4156
|
+
const runtime = normalizeRuntime(runtimeHint);
|
|
4157
|
+
const nextAction2 = error instanceof ConnectError ? error.nextAction : code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
|
|
3886
4158
|
return {
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4159
|
+
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
4160
|
+
outcome: "failed",
|
|
4161
|
+
runtime,
|
|
4162
|
+
topology: "unknown",
|
|
4163
|
+
configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
|
|
4164
|
+
probe: { result: "not_run" },
|
|
4165
|
+
activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
|
|
4166
|
+
next_action: nextAction2,
|
|
4167
|
+
approval: { required: false, expires_at: null },
|
|
4168
|
+
verification: {
|
|
4169
|
+
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
4170
|
+
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
4171
|
+
},
|
|
4172
|
+
error: {
|
|
4173
|
+
code,
|
|
4174
|
+
next_action: nextAction2,
|
|
4175
|
+
// Only a ConnectError's message enters the JSON record: the vocabulary's
|
|
4176
|
+
// prose is connector-authored and safe to serialize, while a plain
|
|
4177
|
+
// Error can carry arbitrary server or filesystem detail (that stance is
|
|
4178
|
+
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
4179
|
+
// runs still get their redacted message on stderr via the CLI mirror.
|
|
4180
|
+
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4181
|
+
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4182
|
+
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4183
|
+
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {},
|
|
4184
|
+
...error instanceof ConnectError && error.details.supersededAgentIds ? { superseded_agent_ids: error.details.supersededAgentIds } : {},
|
|
4185
|
+
...error instanceof ConnectError && error.details.suggestedServerName ? { suggested_name: error.details.suggestedServerName } : {}
|
|
4186
|
+
}
|
|
3890
4187
|
};
|
|
3891
4188
|
}
|
|
3892
|
-
function
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
4189
|
+
function printSetupSummary(setup, log) {
|
|
4190
|
+
log(`Fetched Haven setup for ${setup.agent.name}.`);
|
|
4191
|
+
log(`Haven wallet: ${setup.haven_wallet.name} on ${setup.haven_wallet.network}.`);
|
|
4192
|
+
if (setup.agent_budget.length > 0) {
|
|
4193
|
+
for (const budget of setup.agent_budget) {
|
|
4194
|
+
log(
|
|
4195
|
+
`Agent budget: ${budget.allowance_amount} atomic ${budget.token_symbol} / ${budget.reset_period_min} minute reset.`
|
|
4196
|
+
);
|
|
3897
4197
|
}
|
|
3898
|
-
return false;
|
|
3899
4198
|
}
|
|
3900
|
-
|
|
3901
|
-
if (bareOwnerExists) return false;
|
|
3902
|
-
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
3903
|
-
}
|
|
3904
|
-
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
3905
|
-
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
4199
|
+
log(`Setup challenge expires at ${setup.challenge.expires_at}. If it expires, return to Haven for a fresh setup and rerun Connect \u2014 do not reuse or paste credentials.`);
|
|
3906
4200
|
}
|
|
3907
|
-
function
|
|
3908
|
-
const
|
|
3909
|
-
const
|
|
3910
|
-
if (
|
|
3911
|
-
return {
|
|
3912
|
-
id: "rekey_pending",
|
|
3913
|
-
label,
|
|
3914
|
-
level: "failed",
|
|
3915
|
-
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.`,
|
|
3916
|
-
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
3917
|
-
};
|
|
3918
|
-
}
|
|
3919
|
-
const started = status.startedAt ?? "an unknown time";
|
|
3920
|
-
const address = status.newDelegateAddress ?? "unknown";
|
|
3921
|
-
const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
|
|
3922
|
-
if (completedOnHaven) {
|
|
3923
|
-
return {
|
|
3924
|
-
id: "rekey_pending",
|
|
3925
|
-
label,
|
|
3926
|
-
level: "failed",
|
|
3927
|
-
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." : ""),
|
|
3928
|
-
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)}`
|
|
3929
|
-
};
|
|
3930
|
-
}
|
|
3931
|
-
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.";
|
|
3932
|
-
if (status.state === "expired") {
|
|
3933
|
-
return {
|
|
3934
|
-
id: "rekey_pending",
|
|
3935
|
-
label,
|
|
3936
|
-
level: "failed",
|
|
3937
|
-
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,
|
|
3938
|
-
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.`
|
|
3939
|
-
};
|
|
4201
|
+
function approveBudgetCta(setup, approvalUrl) {
|
|
4202
|
+
const where = approvalUrl ? `at ${approvalUrl}` : "in the Haven dashboard";
|
|
4203
|
+
const budgetPhrase = setup.agent_budget.length > 0 ? setup.agent_budget.map((budget) => `up to ${describeSetupBudget(budget)}`).join(", ") : void 0;
|
|
4204
|
+
if (budgetPhrase) {
|
|
4205
|
+
return `\u2192 Action needed: approve this agent's budget \u2014 ${budgetPhrase} from ${setup.haven_wallet.name} on ${setup.haven_wallet.network} \u2014 ${where}. The approval button is live now; setup continues here in the meantime.`;
|
|
3940
4206
|
}
|
|
3941
|
-
return {
|
|
3942
|
-
id: "rekey_pending",
|
|
3943
|
-
label,
|
|
3944
|
-
level: "ok",
|
|
3945
|
-
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
|
|
3946
|
-
};
|
|
4207
|
+
return `\u2192 Action needed: approve this agent's budget ${where} \u2014 the approval button is live now. Setup continues here in the meantime.`;
|
|
3947
4208
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
);
|
|
3955
|
-
}
|
|
3956
|
-
const mcpInstalled = await readMcpSidecarOverride(directory);
|
|
3957
|
-
if (mcpInstalled) {
|
|
3958
|
-
facts.push(
|
|
3959
|
-
`local MCP runtime installed under ${describeRuntimeSpecOverride(mcpInstalled.specs)} (resolved: ${mcpInstalled.resolved_specs.join(" ")}; directory key ${mcpInstalled.directory_key})`
|
|
3960
|
-
);
|
|
3961
|
-
}
|
|
3962
|
-
let shell;
|
|
3963
|
-
try {
|
|
3964
|
-
const active = resolveRuntimeSpecOverride(env);
|
|
3965
|
-
if (active) shell = `set in this shell: ${describeRuntimeSpecOverride(active)} \u2014 a --repair from here installs these, not the pin`;
|
|
3966
|
-
} catch (err) {
|
|
3967
|
-
shell = err instanceof RuntimeSpecOverrideError ? `set in this shell but REFUSED: ${err.message}` : `set in this shell but unreadable: ${err instanceof Error ? err.message : String(err)}`;
|
|
3968
|
-
}
|
|
3969
|
-
if (shell) facts.push(shell);
|
|
3970
|
-
if (facts.length === 0) return void 0;
|
|
3971
|
-
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3972
|
-
return finalizeCheck({
|
|
3973
|
-
id: "runtime_spec_override",
|
|
3974
|
-
label: "Runtime spec override",
|
|
3975
|
-
level: "failed",
|
|
3976
|
-
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(". ")}.`,
|
|
3977
|
-
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.`
|
|
4209
|
+
function describeSetupBudget(budget) {
|
|
4210
|
+
return describeApprovedBudget({
|
|
4211
|
+
token_symbol: budget.token_symbol,
|
|
4212
|
+
token_address: budget.token_address,
|
|
4213
|
+
amount: budget.allowance_amount,
|
|
4214
|
+
reset_period_min: budget.reset_period_min
|
|
3978
4215
|
});
|
|
3979
4216
|
}
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
}
|
|
4217
|
+
function assertSetupChallengeIsUsable(expiresAt) {
|
|
4218
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
4219
|
+
if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
|
|
4220
|
+
throw new Error(
|
|
4221
|
+
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
4222
|
+
);
|
|
3987
4223
|
}
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
4224
|
+
function deadSetupTokenError(err) {
|
|
4225
|
+
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
4226
|
+
return new ConnectError(
|
|
4227
|
+
"setup_challenge_expired_or_invalid",
|
|
4228
|
+
"This Haven setup token is expired or invalid \u2014 tokens are single-use and expire 30 minutes after the dashboard issues them, and a mistyped token reads the same way. Return to Haven, start a fresh connection, and run its new connector command. No local credentials were written.",
|
|
4229
|
+
"return_to_haven_for_fresh_setup"
|
|
4230
|
+
);
|
|
3994
4231
|
}
|
|
3995
|
-
function
|
|
3996
|
-
|
|
3997
|
-
const has = (key) => files.some((f) => typeof f?.[key] === "string" && f[key].trim() !== "");
|
|
3998
|
-
if (has("account_address")) return "account address stored as account_address";
|
|
3999
|
-
if (has("safe_address")) {
|
|
4000
|
-
return "account address stored under the pre-#2908 name safe_address \u2014 still read; the next --rekey or setup rewrites it as account_address";
|
|
4001
|
-
}
|
|
4002
|
-
return "no account address stored";
|
|
4232
|
+
function isExpiredSetupChallenge(err) {
|
|
4233
|
+
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
4003
4234
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
checks: verdicts.checks.map(finalizeCheck),
|
|
4008
|
-
...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
|
|
4235
|
+
function secureLogger(log, redactPaths = false) {
|
|
4236
|
+
return (message) => {
|
|
4237
|
+
log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
|
|
4009
4238
|
};
|
|
4010
4239
|
}
|
|
4011
|
-
|
|
4012
|
-
const
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
4018
|
-
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
4019
|
-
} catch {
|
|
4020
|
-
signerFile = void 0;
|
|
4021
|
-
}
|
|
4022
|
-
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
4023
|
-
checks.push({
|
|
4024
|
-
id: "credentials",
|
|
4025
|
-
label: "Agent credentials",
|
|
4026
|
-
level: credentialsOk ? "ok" : "failed",
|
|
4027
|
-
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.",
|
|
4028
|
-
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
4029
|
-
});
|
|
4030
|
-
if (!sidecar) {
|
|
4031
|
-
checks.push({
|
|
4032
|
-
id: "signer_runtime",
|
|
4033
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
4034
|
-
level: "failed",
|
|
4035
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
4036
|
-
repair: repairCommandFor(input.runtime)
|
|
4037
|
-
});
|
|
4038
|
-
} else if (sidecar.runtime_spec_override) {
|
|
4039
|
-
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
4040
|
-
signerVersion: sidecar.signer_version,
|
|
4041
|
-
sdkVersion: sidecar.sdk_version
|
|
4042
|
-
});
|
|
4043
|
-
checks.push({
|
|
4044
|
-
id: "signer_runtime",
|
|
4045
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
4046
|
-
level: matches ? "ok" : "failed",
|
|
4047
|
-
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.`,
|
|
4048
|
-
...matches ? {} : { repair: repairCommandFor(input.runtime, " with the same HAVEN_*_SPEC variables set.") }
|
|
4049
|
-
});
|
|
4240
|
+
function printRuntimeInstall(result, log) {
|
|
4241
|
+
for (const message of result.messages) log(message);
|
|
4242
|
+
if (result.localMcpConfigured) {
|
|
4243
|
+
log("Configured local Haven MCP tools.");
|
|
4244
|
+
} else if (result.hostedMcpConfigured) {
|
|
4245
|
+
log("Configured hosted Haven MCP identity.");
|
|
4050
4246
|
} else {
|
|
4051
|
-
|
|
4052
|
-
signerVersion: sidecar.signer_version,
|
|
4053
|
-
sdkVersion: sidecar.sdk_version
|
|
4054
|
-
});
|
|
4055
|
-
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
4056
|
-
const ok = intact && versionOk;
|
|
4057
|
-
checks.push({
|
|
4058
|
-
id: "signer_runtime",
|
|
4059
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
4060
|
-
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
4061
|
-
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.`,
|
|
4062
|
-
...ok ? {} : { repair: repairCommandFor(input.runtime) }
|
|
4063
|
-
});
|
|
4247
|
+
log("Haven MCP tools still need runtime setup.");
|
|
4064
4248
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
4068
|
-
if (identity?.api_key && hostedUrl) {
|
|
4069
|
-
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
4070
|
-
checks.push({
|
|
4071
|
-
id: "hosted_mcp",
|
|
4072
|
-
label: "Hosted Haven MCP",
|
|
4073
|
-
level: probe.status === "ok" ? "ok" : "failed",
|
|
4074
|
-
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
4075
|
-
...probe.status === "ok" ? {} : {
|
|
4076
|
-
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
4077
|
-
}
|
|
4078
|
-
});
|
|
4249
|
+
if (result.localSignerConfigured) {
|
|
4250
|
+
log("Configured local Haven signer.");
|
|
4079
4251
|
} else {
|
|
4080
|
-
|
|
4081
|
-
id: "hosted_mcp",
|
|
4082
|
-
label: "Hosted Haven MCP",
|
|
4083
|
-
level: "failed",
|
|
4084
|
-
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
4085
|
-
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
4086
|
-
});
|
|
4087
|
-
}
|
|
4088
|
-
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
4089
|
-
let hostedDelegateAddress;
|
|
4090
|
-
if (identity?.api_key && identity.api_url) {
|
|
4091
|
-
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
4092
|
-
identity.api_key,
|
|
4093
|
-
identity.api_url,
|
|
4094
|
-
deps.fetch
|
|
4095
|
-
);
|
|
4096
|
-
if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
|
|
4097
|
-
if (probe.status !== "ok") {
|
|
4098
|
-
checks.push({
|
|
4099
|
-
id: "identity_match",
|
|
4100
|
-
label: "Hosted identity matches the local signing key",
|
|
4101
|
-
level: "failed",
|
|
4102
|
-
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.`,
|
|
4103
|
-
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)}`
|
|
4104
|
-
});
|
|
4105
|
-
} else if (!localDelegate) {
|
|
4106
|
-
checks.push({
|
|
4107
|
-
id: "identity_match",
|
|
4108
|
-
label: "Hosted identity matches the local signing key",
|
|
4109
|
-
level: "failed",
|
|
4110
|
-
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
4111
|
-
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
4112
|
-
});
|
|
4113
|
-
} else {
|
|
4114
|
-
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
4115
|
-
checks.push({
|
|
4116
|
-
id: "identity_match",
|
|
4117
|
-
label: "Hosted identity matches the local signing key",
|
|
4118
|
-
level: same ? "ok" : "failed",
|
|
4119
|
-
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.`,
|
|
4120
|
-
...same ? {} : {
|
|
4121
|
-
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.`
|
|
4122
|
-
}
|
|
4123
|
-
});
|
|
4124
|
-
}
|
|
4125
|
-
}
|
|
4126
|
-
const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
|
|
4127
|
-
if (pending) {
|
|
4128
|
-
checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
|
|
4252
|
+
log("Local Haven signer still needs runtime setup.");
|
|
4129
4253
|
}
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
4155
|
-
...probe.status === "ok" ? {} : { repair: repairCommandFor(input.runtime) }
|
|
4156
|
-
});
|
|
4157
|
-
}
|
|
4158
|
-
} else {
|
|
4159
|
-
checks.push({
|
|
4160
|
-
id: "signer_process",
|
|
4161
|
-
label: "Signer stdio handshake",
|
|
4162
|
-
level: "failed",
|
|
4163
|
-
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
4164
|
-
repair: repairCommandFor(input.runtime)
|
|
4165
|
-
});
|
|
4254
|
+
}
|
|
4255
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
4256
|
+
const s = atomic.toString().padStart(decimals + 1, "0");
|
|
4257
|
+
const intPart = s.slice(0, s.length - decimals) || "0";
|
|
4258
|
+
const fracPart = s.slice(s.length - decimals).replace(/0+$/, "");
|
|
4259
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
4260
|
+
}
|
|
4261
|
+
function describeResetPeriod(resetPeriodMin) {
|
|
4262
|
+
if (resetPeriodMin === 1440) return "per day";
|
|
4263
|
+
if (resetPeriodMin === 10080) return "per week";
|
|
4264
|
+
if (resetPeriodMin === 43200) return "per month";
|
|
4265
|
+
if (resetPeriodMin === 60) return "per hour";
|
|
4266
|
+
if (resetPeriodMin === 0) return "in total";
|
|
4267
|
+
return `every ${resetPeriodMin} minutes`;
|
|
4268
|
+
}
|
|
4269
|
+
function describeApprovedBudget(budget) {
|
|
4270
|
+
const token = resolveTokenFromAddress(budget.token_address);
|
|
4271
|
+
const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
|
|
4272
|
+
return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
|
|
4273
|
+
}
|
|
4274
|
+
function describeWaitBound(timeoutMs) {
|
|
4275
|
+
if (timeoutMs >= 12e4) {
|
|
4276
|
+
const minutes = Math.round(timeoutMs / 6e4);
|
|
4277
|
+
return `${minutes} minutes`;
|
|
4166
4278
|
}
|
|
4167
|
-
return {
|
|
4279
|
+
return `${Math.round(timeoutMs / 1e3)}s`;
|
|
4168
4280
|
}
|
|
4169
|
-
async function
|
|
4170
|
-
const
|
|
4171
|
-
const
|
|
4172
|
-
|
|
4173
|
-
const
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
const
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4281
|
+
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
4282
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
4283
|
+
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
4284
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
4285
|
+
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
4286
|
+
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
4287
|
+
let waitingAnnounced = false;
|
|
4288
|
+
const announceWaiting = () => {
|
|
4289
|
+
if (waitingAnnounced) return;
|
|
4290
|
+
waitingAnnounced = true;
|
|
4291
|
+
log("Now waiting for you to approve the budget in the Haven dashboard \u2014 approval is what unlocks the agent's Haven tools\u2026");
|
|
4292
|
+
};
|
|
4293
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
4294
|
+
if (i > 0) await sleep(intervalMs);
|
|
4295
|
+
let status;
|
|
4181
4296
|
try {
|
|
4182
|
-
|
|
4183
|
-
} catch {
|
|
4184
|
-
|
|
4297
|
+
status = await api.getConnectorStatus(setupId, apiKey);
|
|
4298
|
+
} catch (err) {
|
|
4299
|
+
if (err instanceof ConnectRequestError && (err.status === 401 || err.status === 404)) {
|
|
4300
|
+
log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
|
|
4301
|
+
return "ended";
|
|
4302
|
+
}
|
|
4303
|
+
announceWaiting();
|
|
4304
|
+
continue;
|
|
4185
4305
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
4192
|
-
bareOwnerExists = true;
|
|
4193
|
-
break;
|
|
4306
|
+
if (status.status === "active") {
|
|
4307
|
+
log(
|
|
4308
|
+
status.approved_budget ? `Budget approved \u{1F389} \u2014 I can now spend up to ${describeApprovedBudget(status.approved_budget)} from your Haven wallet.` : "Budget approved \u{1F389} \u2014 the agent can now spend within its Haven rules."
|
|
4309
|
+
);
|
|
4310
|
+
return "approved";
|
|
4194
4311
|
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
4199
|
-
for (const dir of allDirectories) {
|
|
4200
|
-
const identity = await readIdentity(dir);
|
|
4201
|
-
const sidecar = await readRuntimeSidecar(dir);
|
|
4202
|
-
const tombstone = await readAgentTombstone(dir);
|
|
4203
|
-
const slug = sidecar?.server_name;
|
|
4204
|
-
const names = serverNamesFor(slug);
|
|
4205
|
-
const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
|
|
4206
|
-
if (!identity?.api_key) {
|
|
4207
|
-
const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
|
|
4208
|
-
inventory.push({
|
|
4209
|
-
...slug ? { slug } : {},
|
|
4210
|
-
...agentId ? { agentId } : {},
|
|
4211
|
-
directory: dir,
|
|
4212
|
-
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
4213
|
-
// a retired directory that also holds a parked key stays `retired`.
|
|
4214
|
-
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
4215
|
-
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
|
|
4216
|
-
...rekeyPending ? { rekeyPending } : {}
|
|
4217
|
-
});
|
|
4218
|
-
continue;
|
|
4312
|
+
if (status.status === "cancelled" || status.status === "expired" || status.status === "failed") {
|
|
4313
|
+
log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
|
|
4314
|
+
return "ended";
|
|
4219
4315
|
}
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
checks: [],
|
|
4227
|
-
...rekeyPending ? { rekeyPending } : {}
|
|
4228
|
-
};
|
|
4229
|
-
if (wired) {
|
|
4230
|
-
const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
|
|
4231
|
-
entry.checks = result.checks;
|
|
4232
|
-
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
4233
|
-
} else if (rekeyPending) {
|
|
4234
|
-
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
|
|
4316
|
+
announceWaiting();
|
|
4317
|
+
if (i > 0 && i % remindEvery === 0) {
|
|
4318
|
+
const elapsedSeconds = Math.round(i * intervalMs / 1e3);
|
|
4319
|
+
log(
|
|
4320
|
+
`Still waiting for budget approval in Haven \u2014 ${elapsedSeconds}s elapsed, checking every ${Math.round(intervalMs / 1e3)}s. I'll stop waiting after ${describeWaitBound(timeoutMs)}; you can approve later and the tools unlock then.`
|
|
4321
|
+
);
|
|
4235
4322
|
}
|
|
4236
|
-
inventory.push(entry);
|
|
4237
4323
|
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
}
|
|
4245
|
-
if (
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
});
|
|
4253
|
-
} else {
|
|
4254
|
-
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
4255
|
-
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
4256
|
-
if (!primaryChecksById.has("credentials")) {
|
|
4257
|
-
const result = await checksForAgent(
|
|
4258
|
-
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
4259
|
-
input2,
|
|
4260
|
-
deps
|
|
4261
|
-
);
|
|
4262
|
-
signerCapabilities = result.signerCapabilities;
|
|
4263
|
-
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
4264
|
-
}
|
|
4265
|
-
for (const id of ["credentials", "signer_runtime", "runtime_spec_override"]) {
|
|
4266
|
-
const check = primaryChecksById.get(id);
|
|
4267
|
-
if (check) checks.push(check);
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
4271
|
-
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
4272
|
-
checks.push({
|
|
4273
|
-
id: "runtime_config",
|
|
4274
|
-
label: "Runtime MCP config",
|
|
4275
|
-
level: "failed",
|
|
4276
|
-
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(", ")}.`,
|
|
4277
|
-
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4278
|
-
});
|
|
4279
|
-
} else if (configPath === null && input2.runtime === "") {
|
|
4280
|
-
checks.push({
|
|
4281
|
-
id: "runtime_config",
|
|
4282
|
-
label: "Runtime MCP config",
|
|
4283
|
-
level: "failed",
|
|
4284
|
-
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(", ")}.`,
|
|
4285
|
-
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4286
|
-
});
|
|
4287
|
-
} else if (configPath === null) {
|
|
4288
|
-
checks.push({
|
|
4289
|
-
id: "runtime_config",
|
|
4290
|
-
label: "Runtime MCP config",
|
|
4291
|
-
level: "ok",
|
|
4292
|
-
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.`
|
|
4293
|
-
});
|
|
4294
|
-
} else if (configText === null) {
|
|
4295
|
-
checks.push({
|
|
4296
|
-
id: "runtime_config",
|
|
4297
|
-
label: "Runtime MCP config",
|
|
4298
|
-
level: "failed",
|
|
4299
|
-
detail: `No runtime config at ${configPath}.`,
|
|
4300
|
-
repair: repairCommandFor(input2.runtime)
|
|
4301
|
-
});
|
|
4302
|
-
} else {
|
|
4303
|
-
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
4304
|
-
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
4305
|
-
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
4306
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
4307
|
-
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
4308
|
-
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
4309
|
-
checks.push({
|
|
4310
|
-
id: "runtime_config",
|
|
4311
|
-
label: "Runtime MCP config",
|
|
4312
|
-
level: ok ? "ok" : "failed",
|
|
4313
|
-
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)" : ""}.`,
|
|
4314
|
-
...ok ? {} : { repair: repairCommandFor(input2.runtime) }
|
|
4315
|
-
});
|
|
4324
|
+
log(
|
|
4325
|
+
"Budget approval is still pending in Haven. Approve it in the dashboard whenever you are ready \u2014 the agent tools unlock the moment you do. Verify later with the read-only haven_get_agent tool."
|
|
4326
|
+
);
|
|
4327
|
+
return "pending";
|
|
4328
|
+
}
|
|
4329
|
+
function completionHandoffLines(result, approval, approvalUrl) {
|
|
4330
|
+
const approveStep = approvalUrl ? `Approve the budget at ${approvalUrl}. Approval \u2014 not restarting \u2014 unlocks Haven tools.` : "Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.";
|
|
4331
|
+
if (result.errorCode === "manual_runtime_setup_required") {
|
|
4332
|
+
return [
|
|
4333
|
+
"Next steps:",
|
|
4334
|
+
`1. ${approveStep}`,
|
|
4335
|
+
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
4336
|
+
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
4337
|
+
];
|
|
4316
4338
|
}
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4339
|
+
if (result.errorCode) {
|
|
4340
|
+
return [
|
|
4341
|
+
"Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new connector command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
|
|
4342
|
+
];
|
|
4320
4343
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
const unverifiable = [];
|
|
4326
|
-
const retired = [];
|
|
4327
|
-
for (const entry of otherEntries) {
|
|
4328
|
-
const identity = await readIdentity(entry.directory);
|
|
4329
|
-
const tombstone = await readAgentTombstone(entry.directory);
|
|
4330
|
-
const otherAgent = entry.agentId ?? basename(entry.directory);
|
|
4331
|
-
if (!identity?.api_key || !identity.api_url) {
|
|
4332
|
-
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
4333
|
-
else unverifiable.push(`${otherAgent} (no stored key/API URL to probe)`);
|
|
4334
|
-
continue;
|
|
4335
|
-
}
|
|
4336
|
-
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
4337
|
-
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
4338
|
-
identity.api_key,
|
|
4339
|
-
identity.api_url,
|
|
4340
|
-
deps.fetch
|
|
4341
|
-
);
|
|
4342
|
-
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
4343
|
-
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
4344
|
-
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
4345
|
-
}
|
|
4346
|
-
const parts = [];
|
|
4347
|
-
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
4348
|
-
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
4349
|
-
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
4350
|
-
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
4351
|
-
const knownIds = new Set([...inventory].map((e) => e.agentId ?? basename(e.directory)));
|
|
4352
|
-
const ledgerDir = input.credentialsDir ? tombstonesDirForAgentDirectory(input.credentialsDir, homeDir) : join(homeDir, ".haven", "tombstones");
|
|
4353
|
-
const ghostRecords = (await readTombstoneRecords(ledgerDir)).filter(
|
|
4354
|
-
(rec) => !knownIds.has(rec.agent_id)
|
|
4355
|
-
);
|
|
4356
|
-
if (ghostRecords.length > 0) {
|
|
4357
|
-
parts.push(
|
|
4358
|
-
`retired records (dir removed): ${ghostRecords.map((rec) => `${rec.agent_id} (${rec.reason})`).join(", ")}`
|
|
4359
|
-
);
|
|
4360
|
-
}
|
|
4361
|
-
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
4362
|
-
const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
|
|
4363
|
-
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
4364
|
-
checks.push({
|
|
4365
|
-
id: "superseded_agents",
|
|
4366
|
-
label: "Superseded agent credentials",
|
|
4367
|
-
level: supersededLevel,
|
|
4368
|
-
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("; ")}.`,
|
|
4369
|
-
...supersededLevel === "failed" ? {
|
|
4370
|
-
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.`
|
|
4371
|
-
} : supersededLevel === "advisory" ? {
|
|
4372
|
-
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.`
|
|
4373
|
-
} : {}
|
|
4374
|
-
});
|
|
4344
|
+
if (approval === "ended") {
|
|
4345
|
+
return [
|
|
4346
|
+
"Next step: this setup ended in Haven before the budget was approved, so there is nothing left to approve on it \u2014 start a fresh connection from the dashboard when ready."
|
|
4347
|
+
];
|
|
4375
4348
|
}
|
|
4376
|
-
const
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4349
|
+
const profile = runtimeProfile(result.runtime);
|
|
4350
|
+
const activation = activationInstructionWithWhy(profile);
|
|
4351
|
+
if (approval === "approved") {
|
|
4352
|
+
return [
|
|
4353
|
+
"Next steps \u2014 the budget is already approved, so there is nothing more to do in Haven:",
|
|
4354
|
+
`1. ${activation}`,
|
|
4355
|
+
`2. ${runtimeVerificationInstruction(result.runtime)}`
|
|
4356
|
+
];
|
|
4384
4357
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
level: "advisory",
|
|
4396
|
-
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.`,
|
|
4397
|
-
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
4398
|
-
});
|
|
4358
|
+
return [
|
|
4359
|
+
"Next steps:",
|
|
4360
|
+
`1. ${approveStep}`,
|
|
4361
|
+
`2. ${activation}`,
|
|
4362
|
+
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
4363
|
+
];
|
|
4364
|
+
}
|
|
4365
|
+
function activationInstructionWithWhy(profile) {
|
|
4366
|
+
if (profile.restartMode === "restart-session") {
|
|
4367
|
+
return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config when a session starts, which is why this step is still needed.)`;
|
|
4399
4368
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
4403
|
-
const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
|
|
4404
|
-
checks.push({
|
|
4405
|
-
id: "rekey_pending_elsewhere",
|
|
4406
|
-
label: "Parked re-keys in other credential directories",
|
|
4407
|
-
level: abandoned.length === 0 ? "ok" : "failed",
|
|
4408
|
-
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(", ")}.`,
|
|
4409
|
-
...abandoned.length > 0 ? {
|
|
4410
|
-
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."
|
|
4411
|
-
} : {}
|
|
4412
|
-
});
|
|
4369
|
+
if (profile.restartMode === "restart-app") {
|
|
4370
|
+
return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config at app launch, which is why this step is still needed.)`;
|
|
4413
4371
|
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
const
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
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).`,
|
|
4424
|
-
repair: `Run: ${RERUN} --prune-signer-runtimes (add --dry-run to list only).`
|
|
4425
|
-
});
|
|
4372
|
+
return profile.activationInstruction;
|
|
4373
|
+
}
|
|
4374
|
+
async function listExistingKeyedAgents(baseDir) {
|
|
4375
|
+
const root = defaultCredentialRoot(baseDir);
|
|
4376
|
+
let entries = [];
|
|
4377
|
+
try {
|
|
4378
|
+
entries = await readdir(root);
|
|
4379
|
+
} catch {
|
|
4380
|
+
return [];
|
|
4426
4381
|
}
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4382
|
+
const out = [];
|
|
4383
|
+
for (const entry of entries) {
|
|
4384
|
+
const directory = join(root, entry);
|
|
4385
|
+
try {
|
|
4386
|
+
const identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
4387
|
+
if (typeof identity.api_key !== "string" || identity.api_key.length === 0) continue;
|
|
4388
|
+
let agent = {};
|
|
4389
|
+
try {
|
|
4390
|
+
agent = JSON.parse(await readFile(join(directory, "agent.json"), "utf8"));
|
|
4391
|
+
} catch {
|
|
4392
|
+
}
|
|
4393
|
+
const { accountAddress } = readStoredAccountAddress(identity, agent);
|
|
4394
|
+
out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
|
|
4395
|
+
} catch {
|
|
4431
4396
|
}
|
|
4432
4397
|
}
|
|
4433
|
-
|
|
4434
|
-
checks.push({
|
|
4435
|
-
id: "restart",
|
|
4436
|
-
label: "Runtime restart",
|
|
4437
|
-
level: "ok",
|
|
4438
|
-
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."
|
|
4439
|
-
});
|
|
4440
|
-
const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
|
|
4441
|
-
const finalChecks = checks.map(finalizeCheck);
|
|
4442
|
-
const level = rollUpLevel([...finalChecks, ...wiredChecks]);
|
|
4443
|
-
return {
|
|
4444
|
-
version: 1,
|
|
4445
|
-
ok: level !== "failed",
|
|
4446
|
-
level,
|
|
4447
|
-
runtime: input2.runtime,
|
|
4448
|
-
credentialDirectory: primaryDirectory,
|
|
4449
|
-
checks: finalChecks,
|
|
4450
|
-
agents: inventory,
|
|
4451
|
-
...signerCapabilities ? { signerCapabilities } : {}
|
|
4452
|
-
};
|
|
4398
|
+
return out;
|
|
4453
4399
|
}
|
|
4454
|
-
async function
|
|
4455
|
-
const
|
|
4456
|
-
|
|
4457
|
-
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
4458
|
-
if (others.length > 0) {
|
|
4459
|
-
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
4460
|
-
}
|
|
4461
|
-
if (!directory) {
|
|
4462
|
-
return {
|
|
4463
|
-
ok: false,
|
|
4464
|
-
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
4465
|
-
};
|
|
4466
|
-
}
|
|
4467
|
-
const resolution = await resolveDoctorRuntime(input, directory);
|
|
4468
|
-
if (resolution.origin === "record") {
|
|
4469
|
-
messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${join(directory, CONNECT_OUTCOME_FILENAME)}.`);
|
|
4470
|
-
}
|
|
4471
|
-
const runtime = resolution.runtime;
|
|
4472
|
-
const input2 = { ...input, runtime };
|
|
4473
|
-
if (runtime === "") {
|
|
4474
|
-
return {
|
|
4475
|
-
ok: false,
|
|
4476
|
-
messages: [
|
|
4477
|
-
"Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
|
|
4478
|
-
`Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
4479
|
-
]
|
|
4480
|
-
};
|
|
4481
|
-
}
|
|
4482
|
-
let identity;
|
|
4400
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
4401
|
+
const root = defaultCredentialRoot(baseDir);
|
|
4402
|
+
let entries = [];
|
|
4483
4403
|
try {
|
|
4484
|
-
|
|
4404
|
+
entries = await readdir(root);
|
|
4485
4405
|
} catch {
|
|
4486
|
-
return
|
|
4487
|
-
}
|
|
4488
|
-
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
4489
|
-
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
4406
|
+
return null;
|
|
4490
4407
|
}
|
|
4491
|
-
const
|
|
4492
|
-
|
|
4408
|
+
const ids = [];
|
|
4409
|
+
for (const entry of entries) {
|
|
4410
|
+
if (join(root, entry) === currentDirectory) continue;
|
|
4411
|
+
const identityPath = join(root, entry, "identity.json");
|
|
4493
4412
|
try {
|
|
4494
|
-
|
|
4495
|
-
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
4496
|
-
return {
|
|
4497
|
-
ok: false,
|
|
4498
|
-
messages: [
|
|
4499
|
-
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
4500
|
-
"Re-run your original connector command (with --local) to repair a local-stdio install."
|
|
4501
|
-
]
|
|
4502
|
-
};
|
|
4503
|
-
}
|
|
4413
|
+
await stat(identityPath);
|
|
4504
4414
|
} catch {
|
|
4415
|
+
continue;
|
|
4416
|
+
}
|
|
4417
|
+
try {
|
|
4418
|
+
const identity = JSON.parse(await readFile(identityPath, "utf8"));
|
|
4419
|
+
ids.push(identity.agent_id ?? entry);
|
|
4420
|
+
} catch {
|
|
4421
|
+
ids.push(entry);
|
|
4505
4422
|
}
|
|
4506
4423
|
}
|
|
4507
|
-
|
|
4508
|
-
const serverName = existingSidecar?.server_name;
|
|
4509
|
-
const signerPath = join(directory, "signer.json");
|
|
4510
|
-
const prepared = await prepareSignerRuntime(
|
|
4511
|
-
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
4512
|
-
{ runCommand: deps.runCommand, env: deps.env }
|
|
4513
|
-
);
|
|
4514
|
-
messages.push(...prepared.messages);
|
|
4515
|
-
const names = serverNamesFor(serverName);
|
|
4516
|
-
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
4517
|
-
const configResult = await writeRuntimeConfig({
|
|
4518
|
-
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
4519
|
-
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
4520
|
-
// — a repair that reported success while writing nothing.
|
|
4521
|
-
runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
|
|
4522
|
-
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
4523
|
-
apiKey: identity.api_key,
|
|
4524
|
-
identityPath: join(directory, "identity.json"),
|
|
4525
|
-
signerPath,
|
|
4526
|
-
credentialDirectory: directory,
|
|
4527
|
-
signerCommand: { command: prepared.command, args: prepared.args },
|
|
4528
|
-
homeDir,
|
|
4529
|
-
mode: "hosted",
|
|
4530
|
-
serverName
|
|
4531
|
-
});
|
|
4532
|
-
messages.push(...configResult.messages);
|
|
4533
|
-
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
4534
|
-
return { ok: true, messages };
|
|
4424
|
+
return ids;
|
|
4535
4425
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
init_runtime_registry();
|
|
4546
|
-
init_signer_consent();
|
|
4547
|
-
init_tombstone();
|
|
4426
|
+
function printNextSteps(result, log, approval, approvalUrl) {
|
|
4427
|
+
for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
|
|
4428
|
+
}
|
|
4429
|
+
var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4430
|
+
var init_runtime = __esm({
|
|
4431
|
+
"src/runtime.ts"() {
|
|
4432
|
+
init_api();
|
|
4433
|
+
init_key();
|
|
4434
|
+
init_redact();
|
|
4548
4435
|
init_server_names();
|
|
4549
4436
|
init_storage();
|
|
4550
|
-
|
|
4551
|
-
|
|
4437
|
+
init_runtime_install();
|
|
4438
|
+
init_runtime_registry();
|
|
4439
|
+
init_connect_error();
|
|
4440
|
+
init_installed_clients();
|
|
4441
|
+
init_wiring_collision();
|
|
4442
|
+
init_unwire();
|
|
4443
|
+
init_tombstone();
|
|
4444
|
+
init_local_mcp_runtime();
|
|
4445
|
+
init_runtime_manifest();
|
|
4446
|
+
CONNECTOR_VERSION = "0.0.0-dev.202609251409.98ed67a";
|
|
4447
|
+
CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
|
|
4448
|
+
CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4449
|
+
failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4450
|
+
RERUN_HINT = connectorRerunCommand();
|
|
4552
4451
|
}
|
|
4553
4452
|
});
|
|
4554
4453
|
|
|
4555
|
-
// src/
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
init_runtime_registry();
|
|
4563
|
-
init_connect_error();
|
|
4454
|
+
// src/rekey-messages.ts
|
|
4455
|
+
var REKEY_FINISH_NEEDS_API_KEY;
|
|
4456
|
+
var init_rekey_messages = __esm({
|
|
4457
|
+
"src/rekey-messages.ts"() {
|
|
4458
|
+
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
4459
|
+
}
|
|
4460
|
+
});
|
|
4564
4461
|
|
|
4565
|
-
// src/
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
"
|
|
4576
|
-
"hermes"
|
|
4577
|
-
];
|
|
4578
|
-
function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env = process.env) {
|
|
4579
|
-
const targets = [
|
|
4580
|
-
{
|
|
4581
|
-
runtime: "claude-code",
|
|
4582
|
-
label: "Claude Code",
|
|
4583
|
-
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
4584
|
-
// not by writing a file this module owns — so its evidence is the
|
|
4585
|
-
// client directory, never a config path.
|
|
4586
|
-
configPath: null,
|
|
4587
|
-
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
4588
|
-
},
|
|
4589
|
-
{
|
|
4590
|
-
runtime: "codex-cli",
|
|
4591
|
-
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
4592
|
-
// ONE candidate. Splitting them would ask the user to answer a question
|
|
4593
|
-
// whose answers are the same write.
|
|
4594
|
-
label: "Codex (CLI or Desktop)",
|
|
4595
|
-
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
4596
|
-
markers: [join(homeDir, ".codex")]
|
|
4597
|
-
},
|
|
4598
|
-
{
|
|
4599
|
-
runtime: "cursor",
|
|
4600
|
-
label: "Cursor",
|
|
4601
|
-
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
4602
|
-
markers: [join(homeDir, ".cursor")]
|
|
4603
|
-
},
|
|
4604
|
-
{
|
|
4605
|
-
runtime: "vscode",
|
|
4606
|
-
label: "VS Code",
|
|
4607
|
-
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
4608
|
-
markers: [resolve(cwd, ".vscode")]
|
|
4609
|
-
},
|
|
4610
|
-
{
|
|
4611
|
-
runtime: "vscode-insiders",
|
|
4612
|
-
label: "VS Code Insiders",
|
|
4613
|
-
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
4614
|
-
markers: []
|
|
4615
|
-
},
|
|
4616
|
-
{
|
|
4617
|
-
runtime: "claude-desktop",
|
|
4618
|
-
label: "Claude Desktop",
|
|
4619
|
-
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
4620
|
-
markers: []
|
|
4621
|
-
},
|
|
4622
|
-
{
|
|
4623
|
-
runtime: "hermes",
|
|
4624
|
-
label: "Hermes Agent",
|
|
4625
|
-
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
4626
|
-
markers: [env.HERMES_HOME ?? join(homeDir, ".hermes")]
|
|
4627
|
-
}
|
|
4628
|
-
];
|
|
4629
|
-
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
4462
|
+
// src/prune-runtimes.ts
|
|
4463
|
+
var prune_runtimes_exports = {};
|
|
4464
|
+
__export(prune_runtimes_exports, {
|
|
4465
|
+
normalizeRuntimePath: () => normalizeRuntimePath,
|
|
4466
|
+
pruneSignerRuntimes: () => pruneSignerRuntimes,
|
|
4467
|
+
referencedRuntimeDirectories: () => referencedRuntimeDirectories,
|
|
4468
|
+
rollUpPruneLevel: () => rollUpPruneLevel,
|
|
4469
|
+
signerRuntimeRoot: () => signerRuntimeRoot
|
|
4470
|
+
});
|
|
4471
|
+
function signerRuntimeRoot(homeDir) {
|
|
4472
|
+
return join(homeDir, ".haven", "signer-runtime");
|
|
4630
4473
|
}
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
continue;
|
|
4474
|
+
function classifyKey(key) {
|
|
4475
|
+
if (key.startsWith("override-")) return "override";
|
|
4476
|
+
if (key === MCP_RUNTIME_MANIFEST.signerVersion || /^\d+\.\d+\.\d+/.test(key)) return "version";
|
|
4477
|
+
return "unknown";
|
|
4478
|
+
}
|
|
4479
|
+
async function directoryBytes(directory) {
|
|
4480
|
+
let total = 0;
|
|
4481
|
+
const walk = async (dir) => {
|
|
4482
|
+
let entries = [];
|
|
4483
|
+
try {
|
|
4484
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
4485
|
+
} catch {
|
|
4486
|
+
return;
|
|
4645
4487
|
}
|
|
4646
|
-
for (const
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
}
|
|
4655
|
-
break;
|
|
4488
|
+
for (const entry of entries) {
|
|
4489
|
+
const path = join(dir, entry.name);
|
|
4490
|
+
if (entry.isDirectory()) await walk(path);
|
|
4491
|
+
else if (entry.isFile()) {
|
|
4492
|
+
try {
|
|
4493
|
+
total += (await stat(path)).size;
|
|
4494
|
+
} catch {
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4656
4497
|
}
|
|
4657
|
-
}
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
4661
|
-
});
|
|
4498
|
+
};
|
|
4499
|
+
await walk(directory);
|
|
4500
|
+
return total;
|
|
4662
4501
|
}
|
|
4663
|
-
function
|
|
4664
|
-
const
|
|
4665
|
-
if (candidates.length === 1) {
|
|
4666
|
-
return { installedClients, suggestedRuntime: candidates[0].runtime };
|
|
4667
|
-
}
|
|
4668
|
-
const configured = candidates.filter((candidate) => candidate.evidence === "config-file");
|
|
4669
|
-
return configured.length === 1 ? { installedClients, suggestedRuntime: configured[0].runtime } : { installedClients };
|
|
4670
|
-
}
|
|
4671
|
-
var MAX_PROMPT_ATTEMPTS = 3;
|
|
4672
|
-
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
4673
|
-
if (candidates.length === 0) throw noInstalledClientsError();
|
|
4674
|
-
io.write("Haven could not detect which agent runtime this is.\n");
|
|
4675
|
-
io.write("These agent clients are installed on this machine:\n");
|
|
4676
|
-
candidates.forEach((candidate, index) => {
|
|
4677
|
-
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
4678
|
-
`);
|
|
4679
|
-
});
|
|
4680
|
-
io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
|
|
4681
|
-
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
4682
|
-
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
4683
|
-
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
4684
|
-
const trimmed = answer.trim();
|
|
4685
|
-
if (trimmed === "") return candidates[0].runtime;
|
|
4686
|
-
const picked = Number.parseInt(trimmed, 10);
|
|
4687
|
-
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
4688
|
-
return candidates[picked - 1].runtime;
|
|
4689
|
-
}
|
|
4690
|
-
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
4691
|
-
`);
|
|
4692
|
-
}
|
|
4693
|
-
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
4694
|
-
}
|
|
4695
|
-
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
4696
|
-
const candidates = await scanInstalledClients(options);
|
|
4697
|
-
if (candidates.length === 0) throw noInstalledClientsError();
|
|
4698
|
-
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
4699
|
-
}
|
|
4700
|
-
function noInstalledClientsError() {
|
|
4701
|
-
return new ConnectError(
|
|
4702
|
-
"runtime_no_installed_clients",
|
|
4703
|
-
"Could not determine the agent runtime: nothing was detected in this environment, and no agent client Haven can configure is installed on this machine. Re-run with --runtime <name> naming the client you want configured, or --runtime other to store credentials and finish the MCP setup by hand.",
|
|
4704
|
-
"rerun_connect_with_explicit_runtime"
|
|
4705
|
-
);
|
|
4706
|
-
}
|
|
4707
|
-
function promptAbortedError(reason) {
|
|
4708
|
-
return new ConnectError(
|
|
4709
|
-
"runtime_prompt_aborted",
|
|
4710
|
-
`Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the connector command again, or pass --runtime <name> to skip the prompt.`,
|
|
4711
|
-
"rerun_connect_and_choose_a_runtime"
|
|
4712
|
-
);
|
|
4713
|
-
}
|
|
4714
|
-
async function pathExists(path) {
|
|
4502
|
+
async function normalizeRuntimePath(path) {
|
|
4503
|
+
const resolved = resolve(path).replace(/[\\/]+$/, "");
|
|
4715
4504
|
try {
|
|
4716
|
-
await
|
|
4717
|
-
return true;
|
|
4505
|
+
return await realpath(resolved);
|
|
4718
4506
|
} catch {
|
|
4719
|
-
return
|
|
4507
|
+
return resolved;
|
|
4720
4508
|
}
|
|
4721
4509
|
}
|
|
4722
|
-
function
|
|
4723
|
-
return {
|
|
4724
|
-
write: (text) => process.stdout.write(text),
|
|
4725
|
-
question: (query) => new Promise((resolvePromise) => {
|
|
4726
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4727
|
-
let settled = false;
|
|
4728
|
-
const settle = (value) => {
|
|
4729
|
-
if (settled) return;
|
|
4730
|
-
settled = true;
|
|
4731
|
-
rl.close();
|
|
4732
|
-
resolvePromise(value);
|
|
4733
|
-
};
|
|
4734
|
-
rl.once("SIGINT", () => settle(null));
|
|
4735
|
-
rl.once("close", () => settle(null));
|
|
4736
|
-
rl.question(query, (answer) => settle(answer));
|
|
4737
|
-
})
|
|
4738
|
-
};
|
|
4510
|
+
function escapeRegExp(text) {
|
|
4511
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4739
4512
|
}
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
const
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
const taken = /* @__PURE__ */ new Set();
|
|
4756
|
-
for (const entry of entries) {
|
|
4757
|
-
const directory = join(root, entry);
|
|
4758
|
-
let identityRaw;
|
|
4513
|
+
async function referencedRuntimeDirectories(homeDir, credentialsDir) {
|
|
4514
|
+
const roots = /* @__PURE__ */ new Set([join(homeDir, ".haven", "agents")]);
|
|
4515
|
+
if (credentialsDir) roots.add(dirname(resolve(credentialsDir)));
|
|
4516
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
4517
|
+
const runtimeRoot = await normalizeRuntimePath(signerRuntimeRoot(homeDir));
|
|
4518
|
+
const rootSpellings = [.../* @__PURE__ */ new Set([resolve(signerRuntimeRoot(homeDir)), runtimeRoot])];
|
|
4519
|
+
const wrapperRe = new RegExp(`(?:${rootSpellings.map(escapeRegExp).join("|")})[\\/]+([^\\/'"\\s]+)`, "g");
|
|
4520
|
+
const add = async (runtimeDirectory, by) => {
|
|
4521
|
+
const key = await normalizeRuntimePath(runtimeDirectory);
|
|
4522
|
+
const list = referenced.get(key) ?? [];
|
|
4523
|
+
if (!list.includes(by)) list.push(by);
|
|
4524
|
+
referenced.set(key, list);
|
|
4525
|
+
};
|
|
4526
|
+
for (const root of roots) {
|
|
4527
|
+
let entries = [];
|
|
4759
4528
|
try {
|
|
4760
|
-
|
|
4529
|
+
entries = await readdir(root);
|
|
4761
4530
|
} catch {
|
|
4762
4531
|
continue;
|
|
4763
4532
|
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4533
|
+
for (const entry of entries) {
|
|
4534
|
+
const directory = join(root, entry);
|
|
4535
|
+
try {
|
|
4536
|
+
const sidecar = JSON.parse(await readFile(join(directory, "signer-runtime.json"), "utf8"));
|
|
4537
|
+
if (typeof sidecar.runtime_directory === "string" && sidecar.runtime_directory.length > 0) {
|
|
4538
|
+
await add(sidecar.runtime_directory, directory);
|
|
4539
|
+
}
|
|
4540
|
+
} catch {
|
|
4541
|
+
}
|
|
4542
|
+
try {
|
|
4543
|
+
const wrapper = await readFile(join(directory, "bin", "haven-signer.mjs"), "utf8");
|
|
4544
|
+
for (const match of wrapper.matchAll(wrapperRe)) await add(join(runtimeRoot, match[1]), directory);
|
|
4545
|
+
} catch {
|
|
4546
|
+
}
|
|
4770
4547
|
}
|
|
4771
|
-
if (!identity?.api_key) continue;
|
|
4772
|
-
if (await pathExists2(join(directory, TOMBSTONE_FILENAME))) continue;
|
|
4773
|
-
const sidecar = await readRuntimeSidecar(directory);
|
|
4774
|
-
if (sidecar?.server_name) continue;
|
|
4775
|
-
superseded.push({ directory, agentId: identity.agent_id ?? entry });
|
|
4776
4548
|
}
|
|
4777
|
-
|
|
4778
|
-
return {
|
|
4779
|
-
superseded,
|
|
4780
|
-
suggestedServerName: proposeServerSlug(input.agentName, taken)
|
|
4781
|
-
};
|
|
4549
|
+
return referenced;
|
|
4782
4550
|
}
|
|
4783
|
-
function
|
|
4784
|
-
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
for (let n = 2; n < 1e3; n += 1) {
|
|
4788
|
-
const suffix = `-${n}`;
|
|
4789
|
-
const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
4790
|
-
if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
|
|
4791
|
-
}
|
|
4792
|
-
throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
|
|
4551
|
+
function rollUpPruneLevel(entries) {
|
|
4552
|
+
if (entries.some((e) => e.level === "failed")) return "failed";
|
|
4553
|
+
if (entries.some((e) => e.level === "advisory")) return "advisory";
|
|
4554
|
+
return "ok";
|
|
4793
4555
|
}
|
|
4794
|
-
function
|
|
4556
|
+
async function pruneSignerRuntimes(input, deps = {}) {
|
|
4557
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
4558
|
+
const root = signerRuntimeRoot(homeDir);
|
|
4559
|
+
const measure = input.measure ?? true;
|
|
4560
|
+
const referenced = await referencedRuntimeDirectories(homeDir, deps.credentialsDir);
|
|
4561
|
+
const pin = await normalizeRuntimePath(join(root, MCP_RUNTIME_MANIFEST.signerVersion));
|
|
4562
|
+
const remove = deps.rm ?? (async (directory) => rm(directory, { recursive: true, force: false }));
|
|
4563
|
+
const entries = [];
|
|
4564
|
+
let keys = [];
|
|
4795
4565
|
try {
|
|
4796
|
-
|
|
4797
|
-
return true;
|
|
4566
|
+
keys = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
4798
4567
|
} catch {
|
|
4799
|
-
return
|
|
4568
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed: 0, reclaimedBytes: 0, level: "ok" };
|
|
4800
4569
|
}
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
if (
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
}
|
|
4833
|
-
|
|
4570
|
+
let removed = 0;
|
|
4571
|
+
let reclaimedBytes = 0;
|
|
4572
|
+
for (const key of keys) {
|
|
4573
|
+
const directory = join(root, key);
|
|
4574
|
+
const normalized = await normalizeRuntimePath(directory);
|
|
4575
|
+
const referencedBy = referenced.get(normalized) ?? [];
|
|
4576
|
+
const kind = classifyKey(key);
|
|
4577
|
+
if (referencedBy.length > 0 || normalized === pin) {
|
|
4578
|
+
entries.push({
|
|
4579
|
+
directory,
|
|
4580
|
+
key,
|
|
4581
|
+
kind,
|
|
4582
|
+
bytes: 0,
|
|
4583
|
+
referencedBy,
|
|
4584
|
+
action: "kept",
|
|
4585
|
+
level: "ok",
|
|
4586
|
+
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)"
|
|
4587
|
+
});
|
|
4588
|
+
continue;
|
|
4589
|
+
}
|
|
4590
|
+
const bytes = measure ? await directoryBytes(directory) : 0;
|
|
4591
|
+
if (input.dryRun) {
|
|
4592
|
+
entries.push({
|
|
4593
|
+
directory,
|
|
4594
|
+
key,
|
|
4595
|
+
kind,
|
|
4596
|
+
bytes,
|
|
4597
|
+
referencedBy,
|
|
4598
|
+
action: "would_remove",
|
|
4599
|
+
level: "advisory",
|
|
4600
|
+
detail: `would remove \u2014 no credential directory names it (${kind}-keyed)`
|
|
4601
|
+
});
|
|
4602
|
+
continue;
|
|
4603
|
+
}
|
|
4604
|
+
try {
|
|
4605
|
+
await remove(directory);
|
|
4606
|
+
removed += 1;
|
|
4607
|
+
reclaimedBytes += bytes;
|
|
4608
|
+
entries.push({ directory, key, kind, bytes, referencedBy, action: "removed", level: "ok", detail: `removed \u2014 no credential directory named it (${kind}-keyed)` });
|
|
4609
|
+
} catch (err) {
|
|
4610
|
+
entries.push({
|
|
4611
|
+
directory,
|
|
4612
|
+
key,
|
|
4613
|
+
kind,
|
|
4614
|
+
bytes,
|
|
4615
|
+
referencedBy,
|
|
4616
|
+
action: "failed",
|
|
4617
|
+
level: "failed",
|
|
4618
|
+
detail: `removal failed: ${err instanceof Error ? err.message : String(err)} \u2014 a signer process may still hold it open; stop it and re-run`
|
|
4619
|
+
});
|
|
4834
4620
|
}
|
|
4835
|
-
io.write(`"${trimmed}" is not one of r, a, q.
|
|
4836
|
-
`);
|
|
4837
4621
|
}
|
|
4838
|
-
return {
|
|
4622
|
+
return { version: 1, root, dryRun: input.dryRun, entries, removed, reclaimedBytes, level: rollUpPruneLevel(entries) };
|
|
4839
4623
|
}
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
return true;
|
|
4844
|
-
} catch {
|
|
4845
|
-
return false;
|
|
4624
|
+
var init_prune_runtimes = __esm({
|
|
4625
|
+
"src/prune-runtimes.ts"() {
|
|
4626
|
+
init_runtime_manifest();
|
|
4846
4627
|
}
|
|
4847
|
-
}
|
|
4628
|
+
});
|
|
4848
4629
|
|
|
4849
|
-
// src/
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4630
|
+
// src/rekey.ts
|
|
4631
|
+
var rekey_exports = {};
|
|
4632
|
+
__export(rekey_exports, {
|
|
4633
|
+
finishRekey: () => finishRekey,
|
|
4634
|
+
startRekey: () => startRekey
|
|
4635
|
+
});
|
|
4636
|
+
async function startRekey(options, deps = {}) {
|
|
4637
|
+
const now = deps.now ?? (() => Date.now());
|
|
4638
|
+
const stored = await readStoredCredentials(
|
|
4639
|
+
options.serverName,
|
|
4640
|
+
options.agentId,
|
|
4641
|
+
options.credentialsDir
|
|
4642
|
+
);
|
|
4643
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url, void 0, CONNECTOR_CLIENT_IDENTITY)))(stored.apiUrl);
|
|
4644
|
+
const identity = await probeIdentity(api, stored.apiKey, "current");
|
|
4645
|
+
assertRekeyable(identity, stored);
|
|
4646
|
+
const key = (deps.generateKey ?? generateDelegateKey)();
|
|
4647
|
+
const startedAt = new Date(now()).toISOString();
|
|
4648
|
+
const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
|
|
4649
|
+
await writeRekeyPending(stored.directory, {
|
|
4650
|
+
agent_id: stored.agentId,
|
|
4651
|
+
new_delegate_address: key.address,
|
|
4652
|
+
new_delegate_key: key.privateKey,
|
|
4653
|
+
started_at: startedAt,
|
|
4654
|
+
expires_at: expiresAt
|
|
4655
|
+
});
|
|
4656
|
+
const finishCommand = [
|
|
4657
|
+
connectorRerunCommand("--rekey-finish"),
|
|
4658
|
+
options.serverName ? `--name ${options.serverName}` : void 0,
|
|
4659
|
+
"--api-key <the key the dashboard showed you>",
|
|
4660
|
+
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
4661
|
+
].filter(Boolean).join(" ");
|
|
4662
|
+
return {
|
|
4663
|
+
started: true,
|
|
4664
|
+
agentId: stored.agentId,
|
|
4665
|
+
directory: stored.directory,
|
|
4666
|
+
newDelegateAddress: key.address,
|
|
4667
|
+
expiresAt,
|
|
4668
|
+
messages: [
|
|
4669
|
+
`New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
|
|
4670
|
+
"",
|
|
4671
|
+
` New signing address: ${key.address}`,
|
|
4672
|
+
"",
|
|
4673
|
+
"The private half stays here \u2014 Haven never receives it, and there is no way to move it",
|
|
4674
|
+
"between machines. That is what keeps the account non-custodial.",
|
|
4675
|
+
"",
|
|
4676
|
+
'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
|
|
4677
|
+
"When it finishes it shows a new API key ONCE. Come back here and run:",
|
|
4678
|
+
"",
|
|
4679
|
+
` ${finishCommand}`,
|
|
4680
|
+
"",
|
|
4681
|
+
`Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
|
|
4682
|
+
`This pending re-key expires ${expiresAt}.`
|
|
4683
|
+
]
|
|
4684
|
+
};
|
|
4874
4685
|
}
|
|
4875
|
-
async function
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
return void 0;
|
|
4686
|
+
async function finishRekey(options, deps = {}) {
|
|
4687
|
+
const now = deps.now ?? (() => Date.now());
|
|
4688
|
+
if (!options.newApiKey) {
|
|
4689
|
+
throw new Error(REKEY_FINISH_NEEDS_API_KEY);
|
|
4880
4690
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
4886
|
-
const log = secureLogger(
|
|
4887
|
-
deps.log ?? ((message) => process.stdout.write(`${message}
|
|
4888
|
-
`)),
|
|
4889
|
-
deps.redactPaths === true
|
|
4691
|
+
const stored = await readStoredCredentials(
|
|
4692
|
+
options.serverName,
|
|
4693
|
+
options.agentId,
|
|
4694
|
+
options.credentialsDir
|
|
4890
4695
|
);
|
|
4891
|
-
const
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4696
|
+
const pending = await readRekeyPending(stored.directory, now());
|
|
4697
|
+
if (pending.agent_id !== stored.agentId) {
|
|
4698
|
+
throw new Error(
|
|
4699
|
+
`The pending re-key at ${stored.directory} belongs to agent ${pending.agent_id}, but that directory now holds ${stored.agentId}. Refusing to write a key into the wrong agent.`
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
const api = (deps.createApi ?? ((url) => createConnectApiClient(url, void 0, CONNECTOR_CLIENT_IDENTITY)))(stored.apiUrl);
|
|
4703
|
+
const identity = await probeIdentity(api, options.newApiKey, "new");
|
|
4704
|
+
if (identity.id !== stored.agentId) {
|
|
4705
|
+
throw new Error(
|
|
4706
|
+
`That API key belongs to agent ${identity.id}, not ${stored.agentId}. Nothing was changed.`
|
|
4707
|
+
);
|
|
4708
|
+
}
|
|
4709
|
+
const onChain = (identity.delegate_address ?? "").toLowerCase();
|
|
4710
|
+
const expected = pending.new_delegate_address.toLowerCase();
|
|
4711
|
+
if (onChain !== expected) {
|
|
4712
|
+
throw new Error(
|
|
4713
|
+
`Haven says this agent's signing address is ${identity.delegate_address ?? "unset"}, but this machine generated ${pending.new_delegate_address}. Nothing was changed. Either the re-key on the agent page used a different address, or it has not finished yet.`
|
|
4714
|
+
);
|
|
4715
|
+
}
|
|
4716
|
+
await rewriteCredentialFiles({
|
|
4717
|
+
baseDir: options.credentialsDir,
|
|
4718
|
+
agentId: stored.agentId,
|
|
4719
|
+
serverName: options.serverName,
|
|
4720
|
+
apiKey: options.newApiKey,
|
|
4721
|
+
delegateKey: pending.new_delegate_key,
|
|
4722
|
+
delegateAddress: pending.new_delegate_address,
|
|
4723
|
+
// The stored value (already new-name-first, from the credential-FILE
|
|
4724
|
+
// permanent fallback) wins; the hosted identity's `account_address` is a
|
|
4725
|
+
// LIVE server read, which as of #2914 emits only that name.
|
|
4726
|
+
accountAddress: stored.accountAddress ?? identity.account_address ?? void 0,
|
|
4727
|
+
chainId: stored.chainId ?? identity.chain_id ?? void 0,
|
|
4728
|
+
network: stored.network,
|
|
4729
|
+
agentBudget: stored.agentBudget,
|
|
4730
|
+
apiUrl: stored.apiUrl,
|
|
4731
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
4732
|
+
x402BindingSigner: stored.x402BindingSigner,
|
|
4733
|
+
warn: deps.log
|
|
4900
4734
|
});
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4735
|
+
await clearRekeyPending(stored.directory);
|
|
4736
|
+
const names = serverNamesFor(options.serverName);
|
|
4737
|
+
const messages = [
|
|
4738
|
+
`Agent ${identity.name || stored.agentId} is now on its new signing key.`,
|
|
4739
|
+
` Signing address: ${pending.new_delegate_address}`,
|
|
4740
|
+
` Credentials: ${stored.directory} (rewritten in place)`,
|
|
4741
|
+
` MCP servers: ${names.hosted} / ${names.signer} (unchanged names)`
|
|
4742
|
+
];
|
|
4743
|
+
let configRewritten = false;
|
|
4744
|
+
if (options.runtime) {
|
|
4745
|
+
const prepared = await (deps.prepareSigner ?? prepareSignerRuntime)(
|
|
4746
|
+
{
|
|
4747
|
+
credentialDirectory: stored.directory,
|
|
4748
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
4749
|
+
homeDir: options.homeDir
|
|
4750
|
+
},
|
|
4751
|
+
{ runCommand: deps.runCommand, env: deps.env }
|
|
4752
|
+
);
|
|
4753
|
+
const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
|
|
4754
|
+
{ runCommand: deps.runCommand, homeDir: options.homeDir },
|
|
4755
|
+
{
|
|
4756
|
+
runtime: options.runtime,
|
|
4757
|
+
hostedMcpUrl: stored.hostedMcpUrl,
|
|
4758
|
+
apiKey: options.newApiKey,
|
|
4759
|
+
identityPath: `${stored.directory}/identity.json`,
|
|
4760
|
+
signerPath: `${stored.directory}/signer.json`,
|
|
4761
|
+
credentialDirectory: stored.directory,
|
|
4762
|
+
serverName: options.serverName
|
|
4763
|
+
},
|
|
4764
|
+
{ command: prepared.command, args: prepared.args }
|
|
4765
|
+
);
|
|
4766
|
+
configRewritten = result.hostedConfigured;
|
|
4767
|
+
if (prepared.runtimeSpecOverride) {
|
|
4768
|
+
messages.push(
|
|
4769
|
+
` Signer runtime: RUNTIME SPEC OVERRIDE ACTIVE \u2014 ${describeRuntimeSpecOverride(prepared.runtimeSpecOverride.specs)} (NOT the pinned manifest; installed into ${prepared.runtimeDirectory})`
|
|
4770
|
+
);
|
|
4771
|
+
}
|
|
4772
|
+
messages.push(` Config: ${result.target}`);
|
|
4773
|
+
messages.push(...result.messages.map((line) => ` ${line}`));
|
|
4774
|
+
if (!configRewritten) {
|
|
4775
|
+
messages.push(
|
|
4776
|
+
"",
|
|
4777
|
+
"WARNING: the MCP config was NOT updated, so it still carries the OLD API key and every",
|
|
4778
|
+
`wired host will fail with 401. Fix the cause above and re-run with --runtime ${options.runtime},`,
|
|
4779
|
+
"or update the config by hand. The credential files on disk are already on the new key."
|
|
4780
|
+
);
|
|
4781
|
+
}
|
|
4782
|
+
} else {
|
|
4783
|
+
messages.push(
|
|
4784
|
+
"",
|
|
4785
|
+
"NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
|
|
4786
|
+
"wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
|
|
4919
4787
|
);
|
|
4920
4788
|
}
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4789
|
+
return {
|
|
4790
|
+
finished: true,
|
|
4791
|
+
agentId: stored.agentId,
|
|
4792
|
+
directory: stored.directory,
|
|
4793
|
+
newDelegateAddress: pending.new_delegate_address,
|
|
4794
|
+
serverNames: { hosted: names.hosted, signer: names.signer },
|
|
4795
|
+
configRewritten,
|
|
4796
|
+
messages
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
async function probeIdentity(api, apiKey, which) {
|
|
4800
|
+
try {
|
|
4801
|
+
return await api.getAgentIdentity(apiKey);
|
|
4802
|
+
} catch (err) {
|
|
4803
|
+
const status = err?.status;
|
|
4804
|
+
if (status === 401 || status === 403) {
|
|
4926
4805
|
throw new Error(
|
|
4927
|
-
|
|
4806
|
+
which === "current" ? "This machine's Haven API key is no longer accepted. If a re-key already finished elsewhere, run --rekey-finish with the key that re-key produced instead of starting a new one." : "Haven rejected that API key. Check you pasted the whole key from the agent page \u2014 nothing was changed."
|
|
4928
4807
|
);
|
|
4929
4808
|
}
|
|
4809
|
+
throw new Error(
|
|
4810
|
+
`Could not reach Haven to check this agent (${redactSecrets(
|
|
4811
|
+
err instanceof Error ? err.message : String(err)
|
|
4812
|
+
)}). Nothing was changed.`
|
|
4813
|
+
);
|
|
4930
4814
|
}
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4815
|
+
}
|
|
4816
|
+
function assertRekeyable(identity, stored) {
|
|
4817
|
+
if (identity.id !== stored.agentId) {
|
|
4818
|
+
throw new Error(
|
|
4819
|
+
`The credentials at ${stored.directory} say agent ${stored.agentId}, but Haven says that key belongs to ${identity.id}. Refusing to re-key an agent this directory does not own.`
|
|
4820
|
+
);
|
|
4936
4821
|
}
|
|
4937
|
-
if (
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
let setup;
|
|
4942
|
-
try {
|
|
4943
|
-
setup = await api.resolveSetup({
|
|
4944
|
-
setupToken: options.setupToken,
|
|
4945
|
-
connectorVersion,
|
|
4946
|
-
runtime
|
|
4947
|
-
});
|
|
4948
|
-
} catch (err) {
|
|
4949
|
-
throw deadSetupTokenError(err) ?? err;
|
|
4950
|
-
}
|
|
4951
|
-
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
4952
|
-
printSetupSummary(setup, log);
|
|
4953
|
-
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
4954
|
-
if (options.serverName) {
|
|
4955
|
-
assertValidServerSlug(options.serverName);
|
|
4956
|
-
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
4957
|
-
}
|
|
4958
|
-
log("Checked local credential storage \u2014 all clear.");
|
|
4959
|
-
let serverName = options.serverName;
|
|
4960
|
-
let replacing;
|
|
4961
|
-
if (!serverName) {
|
|
4962
|
-
const collision = await detectWiringCollision({
|
|
4963
|
-
credentialsDir: options.credentialsDir,
|
|
4964
|
-
agentName: setup.agent.name
|
|
4965
|
-
});
|
|
4966
|
-
if (collision) {
|
|
4967
|
-
const resolution = await resolveWiringCollision(collision, setup.agent.name, options, deps);
|
|
4968
|
-
if (resolution.action === "replace") {
|
|
4969
|
-
replacing = collision;
|
|
4970
|
-
log(
|
|
4971
|
-
`Replacing the existing haven / haven-signer wiring (previous agent(s): ${supersededIds(collision)}). The previous directory is retired locally once the new wiring is written; nothing is revoked.`
|
|
4972
|
-
);
|
|
4973
|
-
} else {
|
|
4974
|
-
serverName = resolution.serverName;
|
|
4975
|
-
assertValidServerSlug(serverName);
|
|
4976
|
-
await assertServerSlugAvailable(serverName, options.credentialsDir);
|
|
4977
|
-
log(`Installing alongside the existing wiring as a named agent: ${serverNamesFor(serverName).hosted} / ${serverNamesFor(serverName).signer}.`);
|
|
4978
|
-
}
|
|
4979
|
-
}
|
|
4822
|
+
if (identity.execution_rail === "legacy") {
|
|
4823
|
+
throw new Error(
|
|
4824
|
+
`Agent ${identity.name || identity.id} is on the legacy rail, which cannot be re-keyed \u2014 its authority is per-token Safe allowances, not a signed delegation there is anything to re-issue. Re-onboard the agent on the delegation rail instead.`
|
|
4825
|
+
);
|
|
4980
4826
|
}
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
log(
|
|
4985
|
-
`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."
|
|
4827
|
+
if (identity.status === "revoked") {
|
|
4828
|
+
throw new Error(
|
|
4829
|
+
`Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
|
|
4986
4830
|
);
|
|
4987
4831
|
}
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
4832
|
+
}
|
|
4833
|
+
var init_rekey = __esm({
|
|
4834
|
+
"src/rekey.ts"() {
|
|
4835
|
+
init_api();
|
|
4836
|
+
init_runtime_install();
|
|
4837
|
+
init_signer_runtime();
|
|
4838
|
+
init_runtime_spec_override();
|
|
4839
|
+
init_key();
|
|
4840
|
+
init_redact();
|
|
4841
|
+
init_rekey_messages();
|
|
4842
|
+
init_runtime();
|
|
4843
|
+
init_server_names();
|
|
4844
|
+
init_storage();
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
|
|
4848
|
+
// src/rekey-restart.ts
|
|
4849
|
+
var rekey_restart_exports = {};
|
|
4850
|
+
__export(rekey_restart_exports, {
|
|
4851
|
+
restartGuidance: () => restartGuidance
|
|
4852
|
+
});
|
|
4853
|
+
function restartGuidance(runtime) {
|
|
4854
|
+
const profile = runtime ? BY_RUNTIME[runtime] : void 0;
|
|
4855
|
+
const lines = ["The new key is only live in a process that started after now.", ""];
|
|
4856
|
+
if (profile) {
|
|
4857
|
+
lines.push(profile.how);
|
|
4858
|
+
for (const command of profile.commands) lines.push(` ${command}`);
|
|
4859
|
+
if (profile.commands.length > 0) lines.push("");
|
|
4860
|
+
} else {
|
|
4861
|
+
lines.push(
|
|
4862
|
+
runtime ? `No standard restart command is known for "${runtime}" \u2014 restart it the way you start it.` : "Pass --runtime <name> to get the restart command for a specific host."
|
|
5000
4863
|
);
|
|
4864
|
+
lines.push("");
|
|
5001
4865
|
}
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
challengeId: setup.challenge.id,
|
|
5014
|
-
delegateAddress: localKey.address,
|
|
5015
|
-
proofSignature,
|
|
5016
|
-
apiKeyHash: hashAgentApiKey(localApiKey),
|
|
5017
|
-
apiKeyPrefix: agentApiKeyPrefix(localApiKey),
|
|
5018
|
-
// #1878: report the pair we are ACTUALLY wiring, bare pair included, so
|
|
5019
|
-
// the dashboard can name it. Derived here rather than sent as the raw
|
|
5020
|
-
// slug — `serverNamesFor` is the one place the naming rule lives, and
|
|
5021
|
-
// the hosted name is what a user pastes into an MCP config.
|
|
5022
|
-
mcpServerName: serverNamesFor(serverName).hosted,
|
|
5023
|
-
// #2528: 'prose' is the default because it is what a caller who says
|
|
5024
|
-
// nothing is doing — the library entry point is not a --json run.
|
|
5025
|
-
runMode: options.runMode ?? "prose",
|
|
5026
|
-
connectorContext: {
|
|
5027
|
-
environment_label: options.environmentLabel ?? "Local workspace",
|
|
5028
|
-
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
4866
|
+
lines.push(SWEEP);
|
|
4867
|
+
return { commands: profile?.commands ?? [], lines };
|
|
4868
|
+
}
|
|
4869
|
+
var SWEEP, BY_RUNTIME;
|
|
4870
|
+
var init_rekey_restart = __esm({
|
|
4871
|
+
"src/rekey-restart.ts"() {
|
|
4872
|
+
SWEEP = "Then restart EVERY other long-lived MCP host on this machine \u2014 gateways, TUI workers, editors. Each holds the wiring snapshot from its own start time, so after a re-key each one is still presenting the OLD API key and will fail with 401 until it restarts.";
|
|
4873
|
+
BY_RUNTIME = {
|
|
4874
|
+
"claude-code": {
|
|
4875
|
+
commands: ["claude --continue"],
|
|
4876
|
+
how: "Exit this Claude Code session and start a new one:"
|
|
5029
4877
|
},
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
4878
|
+
"codex-cli": {
|
|
4879
|
+
commands: ["codex resume --last"],
|
|
4880
|
+
how: "Start a fresh Codex CLI session:"
|
|
4881
|
+
},
|
|
4882
|
+
"codex-desktop": {
|
|
4883
|
+
commands: [],
|
|
4884
|
+
how: "Quit Codex Desktop completely (not just the window) and reopen it."
|
|
4885
|
+
},
|
|
4886
|
+
"claude-desktop": {
|
|
4887
|
+
commands: [],
|
|
4888
|
+
how: "Quit Claude Desktop completely (not just the window) and reopen it."
|
|
4889
|
+
},
|
|
4890
|
+
hermes: {
|
|
4891
|
+
commands: ["systemctl --user restart hermes-gateway", "/restart"],
|
|
4892
|
+
how: "Restart the Hermes gateway, or run /restart inside a Hermes session. The first form is the one that matters if you run it as a user service:"
|
|
4893
|
+
},
|
|
4894
|
+
cursor: {
|
|
4895
|
+
commands: [],
|
|
4896
|
+
how: 'Cursor hot-reloads MCP config, so it will pick the new key up on its own. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
4897
|
+
},
|
|
4898
|
+
vscode: {
|
|
4899
|
+
commands: [],
|
|
4900
|
+
how: 'VS Code hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
4901
|
+
},
|
|
4902
|
+
"vscode-insiders": {
|
|
4903
|
+
commands: [],
|
|
4904
|
+
how: 'VS Code Insiders hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4908
|
+
});
|
|
4909
|
+
|
|
4910
|
+
// src/doctor.ts
|
|
4911
|
+
var doctor_exports = {};
|
|
4912
|
+
__export(doctor_exports, {
|
|
4913
|
+
describeAccountAddressKey: () => describeAccountAddressKey,
|
|
4914
|
+
rollUpLevel: () => rollUpLevel,
|
|
4915
|
+
runDoctor: () => runDoctor,
|
|
4916
|
+
runRepair: () => runRepair
|
|
4917
|
+
});
|
|
4918
|
+
function finalizeCheck(check) {
|
|
4919
|
+
return { ...check, ok: check.level !== "failed" };
|
|
4920
|
+
}
|
|
4921
|
+
function rollUpLevel(checks) {
|
|
4922
|
+
if (checks.some((check) => check.level === "failed")) return "failed";
|
|
4923
|
+
if (checks.some((check) => check.level === "advisory")) return "advisory";
|
|
4924
|
+
return "ok";
|
|
4925
|
+
}
|
|
4926
|
+
function runtimeFlagFor(runtime) {
|
|
4927
|
+
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
4928
|
+
}
|
|
4929
|
+
function repairCommandFor(runtime, suffix = "") {
|
|
4930
|
+
if (normalizeRuntimeName(runtime)) return `Run: ${RERUN} --doctor --repair --runtime ${runtime}${suffix}`;
|
|
4931
|
+
return `Run: ${RERUN} --doctor --repair --runtime <runtime>${suffix.replace(/\.$/, "")} \u2014 --repair needs the runtime named; one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`;
|
|
4932
|
+
}
|
|
4933
|
+
async function resolveDoctorRuntime(input, directory) {
|
|
4934
|
+
const explicit = input.runtime.trim();
|
|
4935
|
+
if (explicit) return { runtime: explicit, origin: "flag" };
|
|
4936
|
+
if (directory) {
|
|
4937
|
+
const recorded = await readConnectOutcomeRuntime(directory);
|
|
4938
|
+
if (recorded !== null && normalizeRuntimeName(recorded)) {
|
|
4939
|
+
return { runtime: recorded, origin: "record" };
|
|
5039
4940
|
}
|
|
5040
|
-
throw err;
|
|
5041
4941
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
serverName,
|
|
5048
|
-
apiKey: localApiKey,
|
|
5049
|
-
delegateKey: localKey.privateKey,
|
|
5050
|
-
delegateAddress: localKey.address,
|
|
5051
|
-
accountAddress: setup.haven_wallet.address,
|
|
5052
|
-
chainId: setup.haven_wallet.chain_id,
|
|
5053
|
-
network: setup.haven_wallet.network,
|
|
5054
|
-
agentBudget: setup.agent_budget.map((budget) => ({
|
|
5055
|
-
token_symbol: budget.token_symbol,
|
|
5056
|
-
allowance_amount: budget.allowance_amount,
|
|
5057
|
-
reset_period_min: budget.reset_period_min
|
|
5058
|
-
})),
|
|
5059
|
-
apiUrl: options.apiBaseUrl,
|
|
5060
|
-
hostedMcpUrl: registration.hosted_mcp_url,
|
|
5061
|
-
x402BindingSigner: setup.x402_binding_signer ?? void 0,
|
|
5062
|
-
warn: log
|
|
5063
|
-
});
|
|
5064
|
-
trace.directory = credentialPaths.directory;
|
|
4942
|
+
return { runtime: "", origin: "unknown" };
|
|
4943
|
+
}
|
|
4944
|
+
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
4945
|
+
const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
|
|
4946
|
+
let entries = [];
|
|
5065
4947
|
try {
|
|
5066
|
-
|
|
5067
|
-
version: 1,
|
|
5068
|
-
server_name: hostedNameForRun,
|
|
5069
|
-
signer_name: serverNamesFor(serverName).signer,
|
|
5070
|
-
agent_id: registration.agent_id,
|
|
5071
|
-
api_url: withoutUserinfo(options.apiBaseUrl),
|
|
5072
|
-
// never persist `user:pass@` (#3154 doc review r2)
|
|
5073
|
-
...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
|
|
5074
|
-
bound_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5075
|
-
};
|
|
5076
|
-
await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
|
|
4948
|
+
entries = await readdir(root);
|
|
5077
4949
|
} catch {
|
|
5078
|
-
|
|
4950
|
+
return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
|
|
5079
4951
|
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
}
|
|
5090
|
-
const runtimeInstall = await runRuntimeInstall({
|
|
5091
|
-
runtime,
|
|
5092
|
-
hostedMcpUrl: registration.hosted_mcp_url,
|
|
5093
|
-
apiKey: localApiKey,
|
|
5094
|
-
signerPath: credentialPaths.signerPath,
|
|
5095
|
-
identityPath: credentialPaths.identityPath,
|
|
5096
|
-
credentialDirectory: credentialPaths.directory,
|
|
5097
|
-
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
5098
|
-
ackSigner: options.ackSigner,
|
|
5099
|
-
ackLocalTools: options.ackLocalTools,
|
|
5100
|
-
localMcp: options.localMcp,
|
|
5101
|
-
serverName
|
|
5102
|
-
}, {
|
|
5103
|
-
onProgress: log,
|
|
5104
|
-
// #1543: report "runtime configured" the moment the config write settles,
|
|
5105
|
-
// so the dashboard can expose its budget-approval controls without
|
|
5106
|
-
// waiting on the probes and skill install — a tail that approval does not
|
|
5107
|
-
// depend on. Silent and best-effort: the complete report below remains
|
|
5108
|
-
// authoritative (it overwrites these keys, adding the probe verdicts and
|
|
5109
|
-
// skill state), so an early failure costs nothing but the head start.
|
|
5110
|
-
onRuntimeConfigured: async (early) => {
|
|
4952
|
+
const candidates = [];
|
|
4953
|
+
const tombstonedOnly = [];
|
|
4954
|
+
const parkedOnly = [];
|
|
4955
|
+
for (const entry of entries) {
|
|
4956
|
+
const directory = join(root, entry);
|
|
4957
|
+
try {
|
|
4958
|
+
const s = await stat(join(directory, "identity.json"));
|
|
4959
|
+
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
4960
|
+
} catch {
|
|
5111
4961
|
try {
|
|
5112
|
-
await
|
|
5113
|
-
|
|
5114
|
-
connectorVersion,
|
|
5115
|
-
runtimeMcpMode: early.runtimeMcpMode,
|
|
5116
|
-
hostedMcpConfigured: early.hostedMcpConfigured,
|
|
5117
|
-
localSignerConfigured: early.localSignerConfigured,
|
|
5118
|
-
localMcpConfigured: early.localMcpConfigured,
|
|
5119
|
-
credentialFilesWritten: true,
|
|
5120
|
-
signerAcknowledged: early.signerAcknowledged,
|
|
5121
|
-
localMcpAcknowledged: early.localMcpAcknowledged,
|
|
5122
|
-
restartRequired: early.restartRequired,
|
|
5123
|
-
nextUserAction: early.nextUserAction,
|
|
5124
|
-
errorCode: early.errorCode ?? null,
|
|
5125
|
-
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
5126
|
-
});
|
|
5127
|
-
if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
|
|
5128
|
-
log(approveBudgetCta(setup, registration.approval_url));
|
|
5129
|
-
}
|
|
4962
|
+
await stat(join(directory, TOMBSTONE_FILENAME));
|
|
4963
|
+
tombstonedOnly.push(directory);
|
|
5130
4964
|
} catch {
|
|
5131
|
-
}
|
|
5132
|
-
}
|
|
5133
|
-
});
|
|
5134
|
-
printRuntimeInstall(runtimeInstall, log);
|
|
5135
|
-
if (runtimeInstall.errorCode) {
|
|
5136
|
-
log("Haven setup needs a couple more steps on this machine \u2014 see the notes above.");
|
|
5137
|
-
} else {
|
|
5138
|
-
log("Haven setup on this machine is complete.");
|
|
5139
|
-
}
|
|
5140
|
-
let supersededAgentsRetiredLocally;
|
|
5141
|
-
const retiredAgentIds = [];
|
|
5142
|
-
const retirementMirrorErrors = {};
|
|
5143
|
-
if (replacing) {
|
|
5144
|
-
if (runtimeInstall.errorCode) {
|
|
5145
|
-
supersededAgentsRetiredLocally = false;
|
|
5146
|
-
log(
|
|
5147
|
-
`Previous agent(s) ${supersededIds(replacing)} were NOT retired: the runtime install did not complete, so their wiring may still be the only working one. Resolve the install, then retire them with ${RERUN_HINT} --unwire <dir>.`
|
|
5148
|
-
);
|
|
5149
|
-
} else {
|
|
5150
|
-
supersededAgentsRetiredLocally = true;
|
|
5151
|
-
for (const entry of replacing.superseded) {
|
|
5152
4965
|
try {
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
reason: "replaced by a new setup (--replace)",
|
|
5157
|
-
replacedBy: registration.agent_id,
|
|
5158
|
-
// #3251: the ledger follows this run's credential root (default
|
|
5159
|
-
// root → ~/.haven/tombstones, as before; any other → <root>/.tombstones).
|
|
5160
|
-
tombstonesDir: tombstonesDirForCredentialRoot(options.credentialsDir)
|
|
5161
|
-
});
|
|
5162
|
-
if (mirrorError !== void 0) {
|
|
5163
|
-
retirementMirrorErrors[entry.agentId] = mirrorError;
|
|
5164
|
-
log(
|
|
5165
|
-
`Warning: the surviving tombstone record for ${entry.agentId} could not be written to ${tombstonesDirForCredentialRoot(options.credentialsDir)} (${mirrorError}). The in-place TOMBSTONE.json stands; --doctor will not list this retirement once the directory is deleted.`
|
|
5166
|
-
);
|
|
5167
|
-
}
|
|
5168
|
-
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
5169
|
-
await clearMcpServerBinding(entry.directory);
|
|
5170
|
-
retiredAgentIds.push(entry.agentId);
|
|
5171
|
-
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
5172
|
-
} catch (err) {
|
|
5173
|
-
supersededAgentsRetiredLocally = false;
|
|
5174
|
-
log(`Could not retire previous agent ${entry.agentId} locally: ${err instanceof Error ? err.message : String(err)}`);
|
|
4966
|
+
await stat(join(directory, REKEY_PENDING_FILENAME));
|
|
4967
|
+
parkedOnly.push(directory);
|
|
4968
|
+
} catch {
|
|
5175
4969
|
}
|
|
5176
4970
|
}
|
|
5177
4971
|
}
|
|
5178
4972
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
4973
|
+
const parkedOnlySet = new Set(parkedOnly);
|
|
4974
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
4975
|
+
if (explicit) {
|
|
4976
|
+
return {
|
|
4977
|
+
directory: explicit,
|
|
4978
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
|
|
4979
|
+
parkedOnly: parkedOnlySet
|
|
4980
|
+
};
|
|
4981
|
+
}
|
|
4982
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
|
|
4983
|
+
return { others: [], parkedOnly: parkedOnlySet };
|
|
4984
|
+
}
|
|
4985
|
+
return {
|
|
4986
|
+
directory: candidates[0]?.directory,
|
|
4987
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
|
|
4988
|
+
parkedOnly: parkedOnlySet
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4991
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
4992
|
+
if (configText === null) return isPrimary;
|
|
4993
|
+
if (slug) {
|
|
4994
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
4995
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
5197
4996
|
}
|
|
5198
|
-
|
|
4997
|
+
return false;
|
|
5199
4998
|
}
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
// The raw scan result, `null` and all — NOT the flattened
|
|
5219
|
-
// `supersededAgentIds` the outcome carries. Flattening here would hand
|
|
5220
|
-
// the dashboard the same ambiguity this field exists to remove (#2561).
|
|
5221
|
-
supersededAgentIds: supersededScan
|
|
5222
|
-
});
|
|
5223
|
-
} catch (err) {
|
|
5224
|
-
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
4999
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
5000
|
+
if (bareOwnerExists) return false;
|
|
5001
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
5002
|
+
}
|
|
5003
|
+
function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
5004
|
+
return finalizeCheck(rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug));
|
|
5005
|
+
}
|
|
5006
|
+
function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
|
|
5007
|
+
const label = "Pending re-key";
|
|
5008
|
+
const nameFlag = slug ? ` --name ${slug}` : "";
|
|
5009
|
+
if (status.state === "unreadable") {
|
|
5010
|
+
return {
|
|
5011
|
+
id: "rekey_pending",
|
|
5012
|
+
label,
|
|
5013
|
+
level: "failed",
|
|
5014
|
+
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.`,
|
|
5015
|
+
repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
|
|
5016
|
+
};
|
|
5225
5017
|
}
|
|
5226
|
-
|
|
5227
|
-
const
|
|
5228
|
-
|
|
5229
|
-
|
|
5018
|
+
const started = status.startedAt ?? "an unknown time";
|
|
5019
|
+
const address = status.newDelegateAddress ?? "unknown";
|
|
5020
|
+
const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
|
|
5021
|
+
if (completedOnHaven) {
|
|
5022
|
+
return {
|
|
5023
|
+
id: "rekey_pending",
|
|
5024
|
+
label,
|
|
5025
|
+
level: "failed",
|
|
5026
|
+
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." : ""),
|
|
5027
|
+
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)}`
|
|
5028
|
+
};
|
|
5230
5029
|
}
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
5241
|
-
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
5242
|
-
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
5243
|
-
approvalRequired: registration.agent_status === "pending_approval",
|
|
5244
|
-
approvalUrl: registration.approval_url
|
|
5245
|
-
});
|
|
5246
|
-
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
5247
|
-
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
5030
|
+
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.";
|
|
5031
|
+
if (status.state === "expired") {
|
|
5032
|
+
return {
|
|
5033
|
+
id: "rekey_pending",
|
|
5034
|
+
label,
|
|
5035
|
+
level: "failed",
|
|
5036
|
+
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,
|
|
5037
|
+
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.`
|
|
5038
|
+
};
|
|
5248
5039
|
}
|
|
5249
5040
|
return {
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
outcome
|
|
5041
|
+
id: "rekey_pending",
|
|
5042
|
+
label,
|
|
5043
|
+
level: "ok",
|
|
5044
|
+
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
|
|
5255
5045
|
};
|
|
5256
5046
|
}
|
|
5257
|
-
function
|
|
5258
|
-
const
|
|
5259
|
-
const
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
},
|
|
5287
|
-
|
|
5288
|
-
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
5289
|
-
instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
|
|
5290
|
-
},
|
|
5291
|
-
// The backend contract supplies a 20-byte address. If an unexpected
|
|
5292
|
-
// malformed value arrives, do not echo it into an automation-facing
|
|
5293
|
-
// record; the human log has already been redacted separately.
|
|
5294
|
-
delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
|
|
5295
|
-
// The endpoint Connect wrote into the runtime's MCP config, verbatim — the
|
|
5296
|
-
// same string that already sits in the user's own config file. Passed
|
|
5297
|
-
// through the secret filter anyway: this is the automation contract, the
|
|
5298
|
-
// value is server-supplied, and belts are cheap (the #1589 stance).
|
|
5299
|
-
...input.hostedMcpUrl ? { hosted_mcp_url: redactSecrets(input.hostedMcpUrl) } : {},
|
|
5300
|
-
// Always emitted on a completed run, empty list included: "no superseded
|
|
5301
|
-
// agents" is a fact a caller needs, and an omitted key would be
|
|
5302
|
-
// indistinguishable from an older connector that never reported it.
|
|
5303
|
-
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
5304
|
-
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
5305
|
-
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
5306
|
-
...input.retirementMirrorErrors ? { retirement_mirror_errors: input.retirementMirrorErrors } : {},
|
|
5307
|
-
// #3122: always emitted on a completed run (empty list included), for the
|
|
5308
|
-
// same reason as superseded_agent_ids above.
|
|
5309
|
-
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
5310
|
-
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
5311
|
-
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
5312
|
-
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
5313
|
-
};
|
|
5314
|
-
return outcome;
|
|
5047
|
+
async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
5048
|
+
const facts = [];
|
|
5049
|
+
const installed = sidecar?.runtime_spec_override;
|
|
5050
|
+
if (installed) {
|
|
5051
|
+
facts.push(
|
|
5052
|
+
`signer runtime installed under ${describeRuntimeSpecOverride(installed.specs)} (resolved: ${installed.resolved_specs.join(" ")}; directory key ${installed.directory_key})`
|
|
5053
|
+
);
|
|
5054
|
+
}
|
|
5055
|
+
const mcpInstalled = await readMcpSidecarOverride(directory);
|
|
5056
|
+
if (mcpInstalled) {
|
|
5057
|
+
facts.push(
|
|
5058
|
+
`local MCP runtime installed under ${describeRuntimeSpecOverride(mcpInstalled.specs)} (resolved: ${mcpInstalled.resolved_specs.join(" ")}; directory key ${mcpInstalled.directory_key})`
|
|
5059
|
+
);
|
|
5060
|
+
}
|
|
5061
|
+
let shell;
|
|
5062
|
+
try {
|
|
5063
|
+
const active = resolveRuntimeSpecOverride(env);
|
|
5064
|
+
if (active) shell = `set in this shell: ${describeRuntimeSpecOverride(active)} \u2014 a --repair from here installs these, not the pin`;
|
|
5065
|
+
} catch (err) {
|
|
5066
|
+
shell = err instanceof RuntimeSpecOverrideError ? `set in this shell but REFUSED: ${err.message}` : `set in this shell but unreadable: ${err instanceof Error ? err.message : String(err)}`;
|
|
5067
|
+
}
|
|
5068
|
+
if (shell) facts.push(shell);
|
|
5069
|
+
if (facts.length === 0) return void 0;
|
|
5070
|
+
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
5071
|
+
return finalizeCheck({
|
|
5072
|
+
id: "runtime_spec_override",
|
|
5073
|
+
label: "Runtime spec override",
|
|
5074
|
+
level: "failed",
|
|
5075
|
+
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(". ")}.`,
|
|
5076
|
+
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.`
|
|
5077
|
+
});
|
|
5315
5078
|
}
|
|
5316
|
-
async function
|
|
5079
|
+
async function readMcpSidecarOverride(directory) {
|
|
5317
5080
|
try {
|
|
5318
|
-
const
|
|
5319
|
-
|
|
5320
|
-
return hint.installedClients.length > 0 ? hint : {};
|
|
5081
|
+
const parsed = JSON.parse(await readFile(join(directory, "mcp-runtime.json"), "utf8"));
|
|
5082
|
+
return parsed.runtime_spec_override;
|
|
5321
5083
|
} catch {
|
|
5322
|
-
return
|
|
5084
|
+
return void 0;
|
|
5323
5085
|
}
|
|
5324
5086
|
}
|
|
5325
|
-
function
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
}
|
|
5331
|
-
function runtimeSelectionPrompt(options, deps) {
|
|
5332
|
-
if (!interactivePromptAllowed(options, deps)) return void 0;
|
|
5333
|
-
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
5087
|
+
async function readIdentity(directory) {
|
|
5088
|
+
try {
|
|
5089
|
+
return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
5090
|
+
} catch {
|
|
5091
|
+
return void 0;
|
|
5092
|
+
}
|
|
5334
5093
|
}
|
|
5335
|
-
function
|
|
5336
|
-
|
|
5337
|
-
|
|
5094
|
+
function describeAccountAddressKey(identity, signerFile) {
|
|
5095
|
+
const files = [identity, signerFile];
|
|
5096
|
+
const has = (key) => files.some((f) => typeof f?.[key] === "string" && f[key].trim() !== "");
|
|
5097
|
+
if (has("account_address")) return "account address stored as account_address";
|
|
5098
|
+
if (has("safe_address")) {
|
|
5099
|
+
return "account address stored under the pre-#2908 name safe_address \u2014 still read; the next --rekey or setup rewrites it as account_address";
|
|
5100
|
+
}
|
|
5101
|
+
return "no account address stored";
|
|
5338
5102
|
}
|
|
5339
|
-
function
|
|
5340
|
-
|
|
5103
|
+
async function checksForAgent(entry, input, deps) {
|
|
5104
|
+
const verdicts = await verdictsForAgent(entry, input, deps);
|
|
5105
|
+
return {
|
|
5106
|
+
checks: verdicts.checks.map(finalizeCheck),
|
|
5107
|
+
...verdicts.signerCapabilities ? { signerCapabilities: verdicts.signerCapabilities } : {}
|
|
5108
|
+
};
|
|
5341
5109
|
}
|
|
5342
|
-
async function
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5110
|
+
async function verdictsForAgent(entry, input, deps) {
|
|
5111
|
+
const { directory, identity, sidecar } = entry;
|
|
5112
|
+
const checks = [];
|
|
5113
|
+
let signerCapabilities;
|
|
5114
|
+
let signerFile;
|
|
5115
|
+
try {
|
|
5116
|
+
const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
5117
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
5118
|
+
} catch {
|
|
5119
|
+
signerFile = void 0;
|
|
5120
|
+
}
|
|
5121
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
5122
|
+
checks.push({
|
|
5123
|
+
id: "credentials",
|
|
5124
|
+
label: "Agent credentials",
|
|
5125
|
+
level: credentialsOk ? "ok" : "failed",
|
|
5126
|
+
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.",
|
|
5127
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
5128
|
+
});
|
|
5129
|
+
if (!sidecar) {
|
|
5130
|
+
checks.push({
|
|
5131
|
+
id: "signer_runtime",
|
|
5132
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
5133
|
+
level: "failed",
|
|
5134
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
5135
|
+
repair: repairCommandFor(input.runtime)
|
|
5136
|
+
});
|
|
5137
|
+
} else if (sidecar.runtime_spec_override) {
|
|
5138
|
+
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
5139
|
+
signerVersion: sidecar.signer_version,
|
|
5140
|
+
sdkVersion: sidecar.sdk_version
|
|
5141
|
+
});
|
|
5142
|
+
checks.push({
|
|
5143
|
+
id: "signer_runtime",
|
|
5144
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
5145
|
+
level: matches ? "ok" : "failed",
|
|
5146
|
+
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.`,
|
|
5147
|
+
...matches ? {} : { repair: repairCommandFor(input.runtime, " with the same HAVEN_*_SPEC variables set.") }
|
|
5148
|
+
});
|
|
5149
|
+
} else {
|
|
5150
|
+
const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
5151
|
+
signerVersion: sidecar.signer_version,
|
|
5152
|
+
sdkVersion: sidecar.sdk_version
|
|
5153
|
+
});
|
|
5154
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
5155
|
+
const ok = intact && versionOk;
|
|
5156
|
+
checks.push({
|
|
5157
|
+
id: "signer_runtime",
|
|
5158
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
5159
|
+
level: ok ? "ok" : intact ? "advisory" : "failed",
|
|
5160
|
+
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.`,
|
|
5161
|
+
...ok ? {} : { repair: repairCommandFor(input.runtime) }
|
|
5162
|
+
});
|
|
5163
|
+
}
|
|
5164
|
+
const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
|
|
5165
|
+
if (overrideCheck) checks.push(overrideCheck);
|
|
5166
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
5167
|
+
if (identity?.api_key && hostedUrl) {
|
|
5168
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
5169
|
+
checks.push({
|
|
5170
|
+
id: "hosted_mcp",
|
|
5171
|
+
label: "Hosted Haven MCP",
|
|
5172
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
5173
|
+
detail: probe.status === "ok" ? `MCP tools endpoint is reachable (${hostedUrl}).` : `MCP tools endpoint probe failed: ${probe.status} (${hostedUrl}).`,
|
|
5174
|
+
...probe.status === "ok" ? {} : {
|
|
5175
|
+
repair: "Check network access and runtime configuration for the hosted MCP URL, then re-run --doctor."
|
|
5176
|
+
}
|
|
5177
|
+
});
|
|
5178
|
+
} else {
|
|
5179
|
+
checks.push({
|
|
5180
|
+
id: "hosted_mcp",
|
|
5181
|
+
label: "Hosted Haven MCP",
|
|
5182
|
+
level: "failed",
|
|
5183
|
+
detail: "No stored API key / hosted MCP URL to probe with.",
|
|
5184
|
+
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
5185
|
+
});
|
|
5186
|
+
}
|
|
5187
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
5188
|
+
let hostedDelegateAddress;
|
|
5189
|
+
if (identity?.api_key && identity.api_url) {
|
|
5190
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
5191
|
+
identity.api_key,
|
|
5192
|
+
identity.api_url,
|
|
5193
|
+
deps.fetch
|
|
5194
|
+
);
|
|
5195
|
+
if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
|
|
5196
|
+
if (probe.status !== "ok") {
|
|
5197
|
+
checks.push({
|
|
5198
|
+
id: "identity_match",
|
|
5199
|
+
label: "Hosted identity matches the local signing key",
|
|
5200
|
+
level: "failed",
|
|
5201
|
+
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.`,
|
|
5202
|
+
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)}`
|
|
5203
|
+
});
|
|
5204
|
+
} else if (!localDelegate) {
|
|
5205
|
+
checks.push({
|
|
5206
|
+
id: "identity_match",
|
|
5207
|
+
label: "Hosted identity matches the local signing key",
|
|
5208
|
+
level: "failed",
|
|
5209
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
5210
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
5211
|
+
});
|
|
5212
|
+
} else {
|
|
5213
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
5214
|
+
checks.push({
|
|
5215
|
+
id: "identity_match",
|
|
5216
|
+
label: "Hosted identity matches the local signing key",
|
|
5217
|
+
level: same ? "ok" : "failed",
|
|
5218
|
+
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.`,
|
|
5219
|
+
...same ? {} : {
|
|
5220
|
+
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.`
|
|
5221
|
+
}
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
|
|
5226
|
+
if (pending) {
|
|
5227
|
+
checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
|
|
5228
|
+
}
|
|
5229
|
+
if (sidecar) {
|
|
5230
|
+
const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
|
|
5231
|
+
if (!consent.acknowledged) {
|
|
5232
|
+
checks.push({
|
|
5233
|
+
id: "signer_process",
|
|
5234
|
+
label: "Signer stdio handshake",
|
|
5235
|
+
level: "failed",
|
|
5236
|
+
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
5237
|
+
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
5238
|
+
});
|
|
5239
|
+
} else {
|
|
5240
|
+
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
5241
|
+
sidecar.wrapper_path,
|
|
5242
|
+
[],
|
|
5243
|
+
MCP_RUNTIME_MANIFEST.requiredSignerTools
|
|
5353
5244
|
);
|
|
5245
|
+
const experimental = probe.capabilities?.experimental ?? probe.capabilities;
|
|
5246
|
+
const compat = experimental?.["haven/signer-compatibility"];
|
|
5247
|
+
signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
|
|
5248
|
+
const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
|
|
5249
|
+
checks.push({
|
|
5250
|
+
id: "signer_process",
|
|
5251
|
+
label: "Signer stdio handshake",
|
|
5252
|
+
level: probe.status === "ok" ? "ok" : "failed",
|
|
5253
|
+
detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
|
|
5254
|
+
...probe.status === "ok" ? {} : { repair: repairCommandFor(input.runtime) }
|
|
5255
|
+
});
|
|
5354
5256
|
}
|
|
5355
|
-
|
|
5257
|
+
} else {
|
|
5258
|
+
checks.push({
|
|
5259
|
+
id: "signer_process",
|
|
5260
|
+
label: "Signer stdio handshake",
|
|
5261
|
+
level: "failed",
|
|
5262
|
+
detail: "Skipped \u2014 no prepared signer runtime to probe.",
|
|
5263
|
+
repair: repairCommandFor(input.runtime)
|
|
5264
|
+
});
|
|
5356
5265
|
}
|
|
5357
|
-
|
|
5358
|
-
"wiring_collision",
|
|
5359
|
-
`This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still revoke the old agent on the Haven agent page), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
|
|
5360
|
-
"relay_wiring_collision_to_user",
|
|
5361
|
-
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
5362
|
-
);
|
|
5266
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
5363
5267
|
}
|
|
5364
|
-
function
|
|
5365
|
-
const
|
|
5366
|
-
const
|
|
5367
|
-
|
|
5368
|
-
const
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
5381
|
-
instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
|
|
5382
|
-
},
|
|
5383
|
-
error: {
|
|
5384
|
-
code,
|
|
5385
|
-
next_action: nextAction2,
|
|
5386
|
-
// Only a ConnectError's message enters the JSON record: the vocabulary's
|
|
5387
|
-
// prose is connector-authored and safe to serialize, while a plain
|
|
5388
|
-
// Error can carry arbitrary server or filesystem detail (that stance is
|
|
5389
|
-
// pinned by test). Redaction stays on as belt-and-braces; plain-Error
|
|
5390
|
-
// runs still get their redacted message on stderr via the CLI mirror.
|
|
5391
|
-
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
5392
|
-
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
5393
|
-
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
5394
|
-
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {},
|
|
5395
|
-
...error instanceof ConnectError && error.details.supersededAgentIds ? { superseded_agent_ids: error.details.supersededAgentIds } : {},
|
|
5396
|
-
...error instanceof ConnectError && error.details.suggestedServerName ? { suggested_name: error.details.suggestedServerName } : {}
|
|
5268
|
+
async function runDoctor(input, deps = {}) {
|
|
5269
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
5270
|
+
const checks = [];
|
|
5271
|
+
let signerCapabilities;
|
|
5272
|
+
const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
5273
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
5274
|
+
const runtime = resolution.runtime;
|
|
5275
|
+
const input2 = { ...input, runtime };
|
|
5276
|
+
const normalizedRuntime = normalizeRuntimeName(input2.runtime);
|
|
5277
|
+
const configPath = runtimeConfigPathFor(normalizedRuntime ?? input2.runtime, homeDir);
|
|
5278
|
+
let configText = null;
|
|
5279
|
+
if (configPath !== null) {
|
|
5280
|
+
try {
|
|
5281
|
+
configText = await readFile(configPath, "utf8");
|
|
5282
|
+
} catch {
|
|
5283
|
+
configText = null;
|
|
5397
5284
|
}
|
|
5398
|
-
}
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5285
|
+
}
|
|
5286
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
5287
|
+
let bareOwnerExists = false;
|
|
5288
|
+
for (const dir of allDirectories) {
|
|
5289
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
5290
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
5291
|
+
bareOwnerExists = true;
|
|
5292
|
+
break;
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
const inventory = [];
|
|
5296
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
5297
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
5298
|
+
for (const dir of allDirectories) {
|
|
5299
|
+
const identity = await readIdentity(dir);
|
|
5300
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
5301
|
+
const tombstone = await readAgentTombstone(dir);
|
|
5302
|
+
const slug = sidecar?.server_name;
|
|
5303
|
+
const names = serverNamesFor(slug);
|
|
5304
|
+
const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
|
|
5305
|
+
if (!identity?.api_key) {
|
|
5306
|
+
const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
|
|
5307
|
+
inventory.push({
|
|
5308
|
+
...slug ? { slug } : {},
|
|
5309
|
+
...agentId ? { agentId } : {},
|
|
5310
|
+
directory: dir,
|
|
5311
|
+
// A tombstone is a deliberate record and outranks the discovery tell:
|
|
5312
|
+
// a retired directory that also holds a parked key stays `retired`.
|
|
5313
|
+
classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
|
|
5314
|
+
checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)] : [],
|
|
5315
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
5316
|
+
});
|
|
5317
|
+
continue;
|
|
5318
|
+
}
|
|
5319
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
5320
|
+
const entry = {
|
|
5321
|
+
...slug ? { slug } : {},
|
|
5322
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
5323
|
+
directory: dir,
|
|
5324
|
+
classification: wired ? "wired" : "superseded",
|
|
5325
|
+
checks: [],
|
|
5326
|
+
...rekeyPending ? { rekeyPending } : {}
|
|
5327
|
+
};
|
|
5328
|
+
if (wired) {
|
|
5329
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
|
|
5330
|
+
entry.checks = result.checks;
|
|
5331
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
5332
|
+
} else if (rekeyPending) {
|
|
5333
|
+
entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input2.runtime, slug)];
|
|
5408
5334
|
}
|
|
5335
|
+
inventory.push(entry);
|
|
5409
5336
|
}
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
return `\u2192 Action needed: approve this agent's budget \u2014 ${budgetPhrase} from ${setup.haven_wallet.name} on ${setup.haven_wallet.network} \u2014 ${where}. The approval button is live now; setup continues here in the meantime.`;
|
|
5337
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
5338
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
5339
|
+
if (primaryDirectory) {
|
|
5340
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
5341
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
5342
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
5417
5343
|
}
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
});
|
|
5427
|
-
}
|
|
5428
|
-
function assertSetupChallengeIsUsable(expiresAt) {
|
|
5429
|
-
const expiresAtMs = Date.parse(expiresAt);
|
|
5430
|
-
if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
|
|
5431
|
-
throw new Error(
|
|
5432
|
-
"This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
|
|
5433
|
-
);
|
|
5434
|
-
}
|
|
5435
|
-
function deadSetupTokenError(err) {
|
|
5436
|
-
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
5437
|
-
return new ConnectError(
|
|
5438
|
-
"setup_challenge_expired_or_invalid",
|
|
5439
|
-
"This Haven setup token is expired or invalid \u2014 tokens are single-use and expire 30 minutes after the dashboard issues them, and a mistyped token reads the same way. Return to Haven, start a fresh connection, and run its new connector command. No local credentials were written.",
|
|
5440
|
-
"return_to_haven_for_fresh_setup"
|
|
5441
|
-
);
|
|
5442
|
-
}
|
|
5443
|
-
function isExpiredSetupChallenge(err) {
|
|
5444
|
-
return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
|
|
5445
|
-
}
|
|
5446
|
-
function secureLogger(log, redactPaths = false) {
|
|
5447
|
-
return (message) => {
|
|
5448
|
-
log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
|
|
5449
|
-
};
|
|
5450
|
-
}
|
|
5451
|
-
function printRuntimeInstall(result, log) {
|
|
5452
|
-
for (const message of result.messages) log(message);
|
|
5453
|
-
if (result.localMcpConfigured) {
|
|
5454
|
-
log("Configured local Haven MCP tools.");
|
|
5455
|
-
} else if (result.hostedMcpConfigured) {
|
|
5456
|
-
log("Configured hosted Haven MCP identity.");
|
|
5344
|
+
if (!primaryDirectory) {
|
|
5345
|
+
checks.push({
|
|
5346
|
+
id: "credentials",
|
|
5347
|
+
label: "Agent credentials",
|
|
5348
|
+
level: "failed",
|
|
5349
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
5350
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
5351
|
+
});
|
|
5457
5352
|
} else {
|
|
5458
|
-
|
|
5353
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
5354
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
5355
|
+
if (!primaryChecksById.has("credentials")) {
|
|
5356
|
+
const result = await checksForAgent(
|
|
5357
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
5358
|
+
input2,
|
|
5359
|
+
deps
|
|
5360
|
+
);
|
|
5361
|
+
signerCapabilities = result.signerCapabilities;
|
|
5362
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
5363
|
+
}
|
|
5364
|
+
for (const id of ["credentials", "signer_runtime", "runtime_spec_override"]) {
|
|
5365
|
+
const check = primaryChecksById.get(id);
|
|
5366
|
+
if (check) checks.push(check);
|
|
5367
|
+
}
|
|
5459
5368
|
}
|
|
5460
|
-
|
|
5461
|
-
|
|
5369
|
+
const runtimeOwnsNoConfig = normalizedRuntime !== null && configPath === null;
|
|
5370
|
+
if (configPath === null && input2.runtime !== "" && normalizedRuntime === null) {
|
|
5371
|
+
checks.push({
|
|
5372
|
+
id: "runtime_config",
|
|
5373
|
+
label: "Runtime MCP config",
|
|
5374
|
+
level: "failed",
|
|
5375
|
+
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(", ")}.`,
|
|
5376
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
5377
|
+
});
|
|
5378
|
+
} else if (configPath === null && input2.runtime === "") {
|
|
5379
|
+
checks.push({
|
|
5380
|
+
id: "runtime_config",
|
|
5381
|
+
label: "Runtime MCP config",
|
|
5382
|
+
level: "failed",
|
|
5383
|
+
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(", ")}.`,
|
|
5384
|
+
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
5385
|
+
});
|
|
5386
|
+
} else if (configPath === null) {
|
|
5387
|
+
checks.push({
|
|
5388
|
+
id: "runtime_config",
|
|
5389
|
+
label: "Runtime MCP config",
|
|
5390
|
+
level: "ok",
|
|
5391
|
+
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.`
|
|
5392
|
+
});
|
|
5393
|
+
} else if (configText === null) {
|
|
5394
|
+
checks.push({
|
|
5395
|
+
id: "runtime_config",
|
|
5396
|
+
label: "Runtime MCP config",
|
|
5397
|
+
level: "failed",
|
|
5398
|
+
detail: `No runtime config at ${configPath}.`,
|
|
5399
|
+
repair: repairCommandFor(input2.runtime)
|
|
5400
|
+
});
|
|
5462
5401
|
} else {
|
|
5463
|
-
|
|
5402
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
5403
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
5404
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
5405
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
5406
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
5407
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
5408
|
+
checks.push({
|
|
5409
|
+
id: "runtime_config",
|
|
5410
|
+
label: "Runtime MCP config",
|
|
5411
|
+
level: ok ? "ok" : "failed",
|
|
5412
|
+
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)" : ""}.`,
|
|
5413
|
+
...ok ? {} : { repair: repairCommandFor(input2.runtime) }
|
|
5414
|
+
});
|
|
5464
5415
|
}
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
const intPart = s.slice(0, s.length - decimals) || "0";
|
|
5469
|
-
const fracPart = s.slice(s.length - decimals).replace(/0+$/, "");
|
|
5470
|
-
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
5471
|
-
}
|
|
5472
|
-
function describeResetPeriod(resetPeriodMin) {
|
|
5473
|
-
if (resetPeriodMin === 1440) return "per day";
|
|
5474
|
-
if (resetPeriodMin === 10080) return "per week";
|
|
5475
|
-
if (resetPeriodMin === 43200) return "per month";
|
|
5476
|
-
if (resetPeriodMin === 60) return "per hour";
|
|
5477
|
-
if (resetPeriodMin === 0) return "in total";
|
|
5478
|
-
return `every ${resetPeriodMin} minutes`;
|
|
5479
|
-
}
|
|
5480
|
-
function describeApprovedBudget(budget) {
|
|
5481
|
-
const token = resolveTokenFromAddress(budget.token_address);
|
|
5482
|
-
const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
|
|
5483
|
-
return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
|
|
5484
|
-
}
|
|
5485
|
-
function describeWaitBound(timeoutMs) {
|
|
5486
|
-
if (timeoutMs >= 12e4) {
|
|
5487
|
-
const minutes = Math.round(timeoutMs / 6e4);
|
|
5488
|
-
return `${minutes} minutes`;
|
|
5416
|
+
for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
|
|
5417
|
+
const check = primaryChecksById.get(id);
|
|
5418
|
+
if (check) checks.push(check);
|
|
5489
5419
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
for (let i = 0; i < maxPolls; i++) {
|
|
5505
|
-
if (i > 0) await sleep(intervalMs);
|
|
5506
|
-
let status;
|
|
5507
|
-
try {
|
|
5508
|
-
status = await api.getConnectorStatus(setupId, apiKey);
|
|
5509
|
-
} catch (err) {
|
|
5510
|
-
if (err instanceof ConnectRequestError && (err.status === 401 || err.status === 404)) {
|
|
5511
|
-
log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
|
|
5512
|
-
return "ended";
|
|
5420
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
5421
|
+
if (otherEntries.length > 0) {
|
|
5422
|
+
const live = [];
|
|
5423
|
+
const revoked = [];
|
|
5424
|
+
const unverifiable = [];
|
|
5425
|
+
const retired = [];
|
|
5426
|
+
for (const entry of otherEntries) {
|
|
5427
|
+
const identity = await readIdentity(entry.directory);
|
|
5428
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
5429
|
+
const otherAgent = entry.agentId ?? basename(entry.directory);
|
|
5430
|
+
if (!identity?.api_key || !identity.api_url) {
|
|
5431
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
5432
|
+
else unverifiable.push(`${otherAgent} (no stored key/API URL to probe)`);
|
|
5433
|
+
continue;
|
|
5513
5434
|
}
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
status.approved_budget ? `Budget approved \u{1F389} \u2014 I can now spend up to ${describeApprovedBudget(status.approved_budget)} from your Haven wallet.` : "Budget approved \u{1F389} \u2014 the agent can now spend within its Haven rules."
|
|
5435
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
5436
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
5437
|
+
identity.api_key,
|
|
5438
|
+
identity.api_url,
|
|
5439
|
+
deps.fetch
|
|
5520
5440
|
);
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
|
|
5525
|
-
return "ended";
|
|
5441
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
5442
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
5443
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
5526
5444
|
}
|
|
5527
|
-
|
|
5528
|
-
if (
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5445
|
+
const parts = [];
|
|
5446
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
5447
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
5448
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
5449
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
5450
|
+
const knownIds = new Set([...inventory].map((e) => e.agentId ?? basename(e.directory)));
|
|
5451
|
+
const ledgerDir = input.credentialsDir ? tombstonesDirForAgentDirectory(input.credentialsDir, homeDir) : join(homeDir, ".haven", "tombstones");
|
|
5452
|
+
const ghostRecords = (await readTombstoneRecords(ledgerDir)).filter(
|
|
5453
|
+
(rec) => !knownIds.has(rec.agent_id)
|
|
5454
|
+
);
|
|
5455
|
+
if (ghostRecords.length > 0) {
|
|
5456
|
+
parts.push(
|
|
5457
|
+
`retired records (dir removed): ${ghostRecords.map((rec) => `${rec.agent_id} (${rec.reason})`).join(", ")}`
|
|
5532
5458
|
);
|
|
5533
5459
|
}
|
|
5460
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
5461
|
+
const classificationUnreliable = configText === null && runtimeOwnsNoConfig;
|
|
5462
|
+
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
5463
|
+
checks.push({
|
|
5464
|
+
id: "superseded_agents",
|
|
5465
|
+
label: "Superseded agent credentials",
|
|
5466
|
+
level: supersededLevel,
|
|
5467
|
+
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("; ")}.`,
|
|
5468
|
+
...supersededLevel === "failed" ? {
|
|
5469
|
+
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.`
|
|
5470
|
+
} : supersededLevel === "advisory" ? {
|
|
5471
|
+
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.`
|
|
5472
|
+
} : {}
|
|
5473
|
+
});
|
|
5534
5474
|
}
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
);
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
return [
|
|
5544
|
-
"Next steps:",
|
|
5545
|
-
`1. ${approveStep}`,
|
|
5546
|
-
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
5547
|
-
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
5548
|
-
];
|
|
5475
|
+
const bindings = (await Promise.all(
|
|
5476
|
+
inventory.filter((entry) => entry.classification !== "retired").map(async (entry) => ({ entry, binding: await readMcpServerBinding(entry.directory) }))
|
|
5477
|
+
)).filter((item) => item.binding !== null);
|
|
5478
|
+
const byName = /* @__PURE__ */ new Map();
|
|
5479
|
+
for (const item of bindings) {
|
|
5480
|
+
const list = byName.get(item.binding.server_name) ?? [];
|
|
5481
|
+
list.push(item);
|
|
5482
|
+
byName.set(item.binding.server_name, list);
|
|
5549
5483
|
}
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5484
|
+
const rebound = [...byName.entries()].filter(([, items]) => items.length > 1);
|
|
5485
|
+
if (rebound.length > 0) {
|
|
5486
|
+
const parts = rebound.map(([name, items]) => {
|
|
5487
|
+
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);
|
|
5488
|
+
const backends = new Set(ordered.map((i) => withoutUserinfo(i.binding.api_url)));
|
|
5489
|
+
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)" : "");
|
|
5490
|
+
});
|
|
5491
|
+
checks.push({
|
|
5492
|
+
id: "mcp_server_name_rebound",
|
|
5493
|
+
label: "MCP server names bound more than once",
|
|
5494
|
+
level: "advisory",
|
|
5495
|
+
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.`,
|
|
5496
|
+
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
5497
|
+
});
|
|
5554
5498
|
}
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5499
|
+
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
5500
|
+
if (parkedElsewhere.length > 0) {
|
|
5501
|
+
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
5502
|
+
const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
|
|
5503
|
+
checks.push({
|
|
5504
|
+
id: "rekey_pending_elsewhere",
|
|
5505
|
+
label: "Parked re-keys in other credential directories",
|
|
5506
|
+
level: abandoned.length === 0 ? "ok" : "failed",
|
|
5507
|
+
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(", ")}.`,
|
|
5508
|
+
...abandoned.length > 0 ? {
|
|
5509
|
+
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."
|
|
5510
|
+
} : {}
|
|
5511
|
+
});
|
|
5559
5512
|
}
|
|
5560
|
-
const
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5513
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
5514
|
+
if (signerProcess) checks.push(signerProcess);
|
|
5515
|
+
const prune = await (deps.pruneSignerRuntimes ?? pruneSignerRuntimes)({ dryRun: true, measure: false }, { homeDir, credentialsDir: input.credentialsDir });
|
|
5516
|
+
const unused = prune.entries.filter((entry) => entry.action === "would_remove");
|
|
5517
|
+
if (unused.length > 0) {
|
|
5518
|
+
checks.push({
|
|
5519
|
+
id: "signer_runtime_unused",
|
|
5520
|
+
label: "Unused signer-runtime directories",
|
|
5521
|
+
level: "advisory",
|
|
5522
|
+
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).`,
|
|
5523
|
+
repair: `Run: ${RERUN} --prune-signer-runtimes (add --dry-run to list only).`
|
|
5524
|
+
});
|
|
5568
5525
|
}
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5526
|
+
if (resolution.origin === "record") {
|
|
5527
|
+
const rc = checks.find((check) => check.id === "runtime_config");
|
|
5528
|
+
if (rc) {
|
|
5529
|
+
rc.detail += ` (Resolved '${resolution.runtime}' from ${join(directory ?? "", CONNECT_OUTCOME_FILENAME)}; pass --runtime to check a different one.)`;
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
const restart = normalizedRuntime === null ? false : restartRequiredForRuntime(input2.runtime, deps.env);
|
|
5533
|
+
checks.push({
|
|
5534
|
+
id: "restart",
|
|
5535
|
+
label: "Runtime restart",
|
|
5536
|
+
level: "ok",
|
|
5537
|
+
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."
|
|
5538
|
+
});
|
|
5539
|
+
const wiredChecks = inventory.filter((entry) => entry.classification === "wired").flatMap((entry) => entry.checks);
|
|
5540
|
+
const finalChecks = checks.map(finalizeCheck);
|
|
5541
|
+
const level = rollUpLevel([...finalChecks, ...wiredChecks]);
|
|
5542
|
+
return {
|
|
5543
|
+
version: 1,
|
|
5544
|
+
ok: level !== "failed",
|
|
5545
|
+
level,
|
|
5546
|
+
runtime: input2.runtime,
|
|
5547
|
+
credentialDirectory: primaryDirectory,
|
|
5548
|
+
checks: finalChecks,
|
|
5549
|
+
agents: inventory,
|
|
5550
|
+
...signerCapabilities ? { signerCapabilities } : {}
|
|
5551
|
+
};
|
|
5575
5552
|
}
|
|
5576
|
-
function
|
|
5577
|
-
|
|
5578
|
-
|
|
5553
|
+
async function runRepair(input, deps = {}) {
|
|
5554
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
5555
|
+
const messages = [];
|
|
5556
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
5557
|
+
if (others.length > 0) {
|
|
5558
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
5579
5559
|
}
|
|
5580
|
-
if (
|
|
5581
|
-
return
|
|
5560
|
+
if (!directory) {
|
|
5561
|
+
return {
|
|
5562
|
+
ok: false,
|
|
5563
|
+
messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
|
|
5564
|
+
};
|
|
5582
5565
|
}
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
async function listExistingKeyedAgents(baseDir) {
|
|
5587
|
-
const root = defaultCredentialRoot(baseDir);
|
|
5588
|
-
let entries = [];
|
|
5589
|
-
try {
|
|
5590
|
-
entries = await readdir(root);
|
|
5591
|
-
} catch {
|
|
5592
|
-
return [];
|
|
5566
|
+
const resolution = await resolveDoctorRuntime(input, directory);
|
|
5567
|
+
if (resolution.origin === "record") {
|
|
5568
|
+
messages.push(`Runtime not given \u2014 resolved '${resolution.runtime}' from ${join(directory, CONNECT_OUTCOME_FILENAME)}.`);
|
|
5593
5569
|
}
|
|
5594
|
-
const
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
}
|
|
5605
|
-
const { accountAddress } = readStoredAccountAddress(identity, agent);
|
|
5606
|
-
out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
|
|
5607
|
-
} catch {
|
|
5608
|
-
}
|
|
5570
|
+
const runtime = resolution.runtime;
|
|
5571
|
+
const input2 = { ...input, runtime };
|
|
5572
|
+
if (runtime === "") {
|
|
5573
|
+
return {
|
|
5574
|
+
ok: false,
|
|
5575
|
+
messages: [
|
|
5576
|
+
"Runtime is unknown \u2014 no runtime flag was given and the connector record carries no resolvable runtime.",
|
|
5577
|
+
`Repair rewrites the runtime config, so it will not guess. Re-run repair naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
5578
|
+
]
|
|
5579
|
+
};
|
|
5609
5580
|
}
|
|
5610
|
-
|
|
5611
|
-
}
|
|
5612
|
-
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
5613
|
-
const root = defaultCredentialRoot(baseDir);
|
|
5614
|
-
let entries = [];
|
|
5581
|
+
let identity;
|
|
5615
5582
|
try {
|
|
5616
|
-
|
|
5583
|
+
identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
5617
5584
|
} catch {
|
|
5618
|
-
return
|
|
5585
|
+
return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
|
|
5619
5586
|
}
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
await stat(identityPath);
|
|
5626
|
-
} catch {
|
|
5627
|
-
continue;
|
|
5628
|
-
}
|
|
5587
|
+
if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
|
|
5588
|
+
return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
|
|
5589
|
+
}
|
|
5590
|
+
const configPath = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir);
|
|
5591
|
+
if (configPath) {
|
|
5629
5592
|
try {
|
|
5630
|
-
const
|
|
5631
|
-
|
|
5593
|
+
const existing = await readFile(configPath, "utf8");
|
|
5594
|
+
if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
|
|
5595
|
+
return {
|
|
5596
|
+
ok: false,
|
|
5597
|
+
messages: [
|
|
5598
|
+
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
5599
|
+
"Re-run your original connector command (with --local) to repair a local-stdio install."
|
|
5600
|
+
]
|
|
5601
|
+
};
|
|
5602
|
+
}
|
|
5632
5603
|
} catch {
|
|
5633
|
-
ids.push(entry);
|
|
5634
5604
|
}
|
|
5635
5605
|
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5606
|
+
const existingSidecar = await readRuntimeSidecar(directory);
|
|
5607
|
+
const serverName = existingSidecar?.server_name;
|
|
5608
|
+
const signerPath = join(directory, "signer.json");
|
|
5609
|
+
const prepared = await prepareSignerRuntime(
|
|
5610
|
+
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
5611
|
+
{ runCommand: deps.runCommand, env: deps.env }
|
|
5612
|
+
);
|
|
5613
|
+
messages.push(...prepared.messages);
|
|
5614
|
+
const names = serverNamesFor(serverName);
|
|
5615
|
+
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
5616
|
+
const configResult = await writeRuntimeConfig({
|
|
5617
|
+
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
5618
|
+
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
5619
|
+
// — a repair that reported success while writing nothing.
|
|
5620
|
+
runtime: normalizeRuntimeName(input2.runtime) ?? input2.runtime,
|
|
5621
|
+
hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
|
|
5622
|
+
apiKey: identity.api_key,
|
|
5623
|
+
identityPath: join(directory, "identity.json"),
|
|
5624
|
+
signerPath,
|
|
5625
|
+
credentialDirectory: directory,
|
|
5626
|
+
signerCommand: { command: prepared.command, args: prepared.args },
|
|
5627
|
+
homeDir,
|
|
5628
|
+
mode: "hosted",
|
|
5629
|
+
serverName
|
|
5630
|
+
});
|
|
5631
|
+
messages.push(...configResult.messages);
|
|
5632
|
+
messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
|
|
5633
|
+
return { ok: true, messages };
|
|
5640
5634
|
}
|
|
5635
|
+
var RERUN;
|
|
5636
|
+
var init_doctor = __esm({
|
|
5637
|
+
"src/doctor.ts"() {
|
|
5638
|
+
init_prune_runtimes();
|
|
5639
|
+
init_runtime_manifest();
|
|
5640
|
+
init_probes();
|
|
5641
|
+
init_signer_runtime();
|
|
5642
|
+
init_runtime_spec_override();
|
|
5643
|
+
init_config_writers();
|
|
5644
|
+
init_runtime_registry();
|
|
5645
|
+
init_signer_consent();
|
|
5646
|
+
init_tombstone();
|
|
5647
|
+
init_server_names();
|
|
5648
|
+
init_storage();
|
|
5649
|
+
init_redact();
|
|
5650
|
+
RERUN = connectorRerunCommand();
|
|
5651
|
+
}
|
|
5652
|
+
});
|
|
5641
5653
|
|
|
5642
5654
|
// src/args.ts
|
|
5655
|
+
init_runtime();
|
|
5643
5656
|
init_server_names();
|
|
5644
5657
|
init_rekey_messages();
|
|
5645
5658
|
function parseArgs(argv, env = process.env) {
|
|
@@ -5912,6 +5925,8 @@ function requireValue(argv, index, option) {
|
|
|
5912
5925
|
init_api();
|
|
5913
5926
|
init_key();
|
|
5914
5927
|
init_redact();
|
|
5928
|
+
init_runtime();
|
|
5929
|
+
init_runtime();
|
|
5915
5930
|
init_redact();
|
|
5916
5931
|
init_connect_error();
|
|
5917
5932
|
function failSubcommand(io, json, err, envelope, fallback) {
|
|
@@ -6344,6 +6359,7 @@ init_runtime_install();
|
|
|
6344
6359
|
init_signer_runtime();
|
|
6345
6360
|
init_runtime_registry();
|
|
6346
6361
|
init_connect_error();
|
|
6362
|
+
init_installed_clients();
|
|
6347
6363
|
init_storage();
|
|
6348
6364
|
init_runtime_manifest();
|
|
6349
6365
|
|