@haven_ai/connect 0.1.27-alpha.0 → 0.1.29-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/cli.cjs +997 -188
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +999 -190
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1009 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -5
- package/dist/index.d.ts +195 -5
- package/dist/index.js +1007 -191
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/cli.cjs
CHANGED
|
@@ -14,6 +14,7 @@ var fs = require('fs');
|
|
|
14
14
|
var url = require('url');
|
|
15
15
|
var crypto = require('crypto');
|
|
16
16
|
var ethers = require('ethers');
|
|
17
|
+
var readline = require('readline');
|
|
17
18
|
|
|
18
19
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
19
20
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -29,6 +30,69 @@ var __export = (target, all) => {
|
|
|
29
30
|
for (var name in all)
|
|
30
31
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
31
32
|
};
|
|
33
|
+
|
|
34
|
+
// src/redact.ts
|
|
35
|
+
function redactSecrets(value) {
|
|
36
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
37
|
+
}
|
|
38
|
+
function shortAddress(address) {
|
|
39
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
40
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
41
|
+
}
|
|
42
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
43
|
+
var init_redact = __esm({
|
|
44
|
+
"src/redact.ts"() {
|
|
45
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
46
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/server-names.ts
|
|
51
|
+
function assertValidServerSlug(slug) {
|
|
52
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function serverNamesFor(slug) {
|
|
69
|
+
if (slug === void 0) {
|
|
70
|
+
return {
|
|
71
|
+
hosted: "haven",
|
|
72
|
+
signer: "haven-signer",
|
|
73
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
74
|
+
codexHosted: "haven",
|
|
75
|
+
codexSigner: "haven_signer",
|
|
76
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
assertValidServerSlug(slug);
|
|
80
|
+
return {
|
|
81
|
+
hosted: `haven-${slug}`,
|
|
82
|
+
signer: `haven-signer-${slug}`,
|
|
83
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
84
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
85
|
+
codexHosted: `haven-${slug}`,
|
|
86
|
+
codexSigner: `haven-signer-${slug}`,
|
|
87
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
var SLUG_RE;
|
|
91
|
+
var init_server_names = __esm({
|
|
92
|
+
"src/server-names.ts"() {
|
|
93
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
32
96
|
function mcpPackageSpec() {
|
|
33
97
|
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
34
98
|
}
|
|
@@ -45,9 +109,9 @@ var init_runtime_manifest = __esm({
|
|
|
45
109
|
mcpPackage: "@haven_ai/mcp",
|
|
46
110
|
mcpVersion: mcp.MCP_VERSION,
|
|
47
111
|
sdkPackage: "@haven_ai/sdk",
|
|
48
|
-
sdkVersion: "0.1.
|
|
112
|
+
sdkVersion: "0.1.29-alpha.0",
|
|
49
113
|
signerPackage: "@haven_ai/signer",
|
|
50
|
-
signerVersion: "0.1.
|
|
114
|
+
signerVersion: "0.1.29-alpha.0",
|
|
51
115
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
52
116
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
53
117
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -123,31 +187,33 @@ function buildSignerServer(spec, runtime) {
|
|
|
123
187
|
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
124
188
|
return server;
|
|
125
189
|
}
|
|
126
|
-
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
|
|
127
|
-
const config = existingJson?.trim() ? parseJsonObject(existingJson) : {};
|
|
190
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
191
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
128
192
|
const existingRoot = config[serverRoot];
|
|
129
193
|
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
130
194
|
config[serverRoot] = {
|
|
131
195
|
...servers,
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
[names.hosted]: hostedServer,
|
|
197
|
+
[names.signer]: signerServer
|
|
134
198
|
};
|
|
135
199
|
return `${JSON.stringify(config, null, 2)}
|
|
136
200
|
`;
|
|
137
201
|
}
|
|
138
|
-
function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
139
|
-
if (!existingYaml?.trim())
|
|
202
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
203
|
+
if (!existingYaml?.trim()) {
|
|
204
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
205
|
+
}
|
|
140
206
|
const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
|
|
141
207
|
if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
|
|
142
|
-
throw new
|
|
208
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
143
209
|
}
|
|
144
210
|
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
145
211
|
const existingServers = mcpPair && yaml.isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
|
|
146
212
|
const servers = isRecord(existingServers) ? existingServers : {};
|
|
147
213
|
const mergedServers = {
|
|
148
214
|
...servers,
|
|
149
|
-
|
|
150
|
-
|
|
215
|
+
[names.hosted]: hostedServer,
|
|
216
|
+
[names.signer]: signerServer
|
|
151
217
|
};
|
|
152
218
|
if (!mcpPair) {
|
|
153
219
|
return appendHermesMcpServers(existingYaml, mergedServers);
|
|
@@ -155,9 +221,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
|
155
221
|
if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
|
|
156
222
|
return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
|
|
157
223
|
}
|
|
158
|
-
function mergeHermesEnv(existingEnv, apiKey) {
|
|
224
|
+
function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
|
|
159
225
|
if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
|
|
160
|
-
const assignment = `${
|
|
226
|
+
const assignment = `${envKey}=${apiKey}`;
|
|
161
227
|
if (!existingEnv) return `${assignment}
|
|
162
228
|
`;
|
|
163
229
|
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -166,12 +232,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
166
232
|
if (hasTrailingNewline) lines.pop();
|
|
167
233
|
let found = false;
|
|
168
234
|
const merged = lines.flatMap((line) => {
|
|
169
|
-
if (isHermesEnvAssignment(line)) {
|
|
235
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
170
236
|
if (found) return [];
|
|
171
237
|
found = true;
|
|
172
238
|
return [assignment];
|
|
173
239
|
}
|
|
174
|
-
if (isAmbiguousHermesEnvLine(line)) {
|
|
240
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
175
241
|
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
176
242
|
}
|
|
177
243
|
return [line];
|
|
@@ -181,11 +247,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
181
247
|
}
|
|
182
248
|
return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
|
|
183
249
|
}
|
|
184
|
-
function isHermesEnvAssignment(line) {
|
|
185
|
-
return
|
|
250
|
+
function isHermesEnvAssignment(line, envKey) {
|
|
251
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
|
|
186
252
|
}
|
|
187
|
-
function isAmbiguousHermesEnvLine(line) {
|
|
188
|
-
return
|
|
253
|
+
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
254
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
189
255
|
}
|
|
190
256
|
function appendHermesMcpServers(source, servers) {
|
|
191
257
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
@@ -244,11 +310,14 @@ function renderHermesMcpServerEntries(servers, indent) {
|
|
|
244
310
|
function isRecord(value) {
|
|
245
311
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
246
312
|
}
|
|
247
|
-
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
248
|
-
let next = removeTomlTableTree(
|
|
313
|
+
function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
|
|
314
|
+
let next = removeTomlTableTree(
|
|
315
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
316
|
+
`mcp_servers.${names.codexSigner}`
|
|
317
|
+
);
|
|
249
318
|
next = next.trimEnd();
|
|
250
319
|
const block = [
|
|
251
|
-
|
|
320
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
252
321
|
`command = ${tomlString(localMcpCommand)}`,
|
|
253
322
|
"args = []",
|
|
254
323
|
"startup_timeout_sec = 120"
|
|
@@ -260,15 +329,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
|
|
|
260
329
|
`;
|
|
261
330
|
return merged;
|
|
262
331
|
}
|
|
263
|
-
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
|
|
264
|
-
let next = removeTomlTableTree(
|
|
332
|
+
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
|
|
333
|
+
let next = removeTomlTableTree(
|
|
334
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
335
|
+
`mcp_servers.${names.codexSigner}`
|
|
336
|
+
);
|
|
265
337
|
next = next.trimEnd();
|
|
266
338
|
const block = [
|
|
267
|
-
|
|
339
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
268
340
|
`url = ${tomlString(hostedMcpUrl)}`,
|
|
269
341
|
`http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
|
|
270
342
|
"",
|
|
271
|
-
|
|
343
|
+
`[mcp_servers.${names.codexSigner}]`,
|
|
272
344
|
`command = ${tomlString(signerSpec.command)}`,
|
|
273
345
|
`args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
|
|
274
346
|
"startup_timeout_sec = 120"
|
|
@@ -287,7 +359,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
287
359
|
existing,
|
|
288
360
|
serverRoot,
|
|
289
361
|
buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
|
|
290
|
-
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
|
|
362
|
+
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
|
|
363
|
+
serverNamesFor(input.serverName),
|
|
364
|
+
target
|
|
291
365
|
);
|
|
292
366
|
await writeOwnerOnlyText(target, merged);
|
|
293
367
|
return {
|
|
@@ -301,6 +375,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
301
375
|
messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
|
|
302
376
|
};
|
|
303
377
|
} catch (err) {
|
|
378
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
304
379
|
return {
|
|
305
380
|
hostedConfigured: false,
|
|
306
381
|
signerConfigured: false,
|
|
@@ -309,8 +384,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
309
384
|
target: configTargetLabel(input.runtime),
|
|
310
385
|
changed: false,
|
|
311
386
|
restartRequired: true,
|
|
312
|
-
messages:
|
|
313
|
-
|
|
387
|
+
messages: unreadable ? [
|
|
388
|
+
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
389
|
+
`Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
390
|
+
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
391
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
314
392
|
};
|
|
315
393
|
}
|
|
316
394
|
}
|
|
@@ -319,8 +397,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
319
397
|
const envTarget = hermesEnvPath(input.homeDir);
|
|
320
398
|
try {
|
|
321
399
|
const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
|
|
400
|
+
const names = serverNamesFor(input.serverName);
|
|
322
401
|
const hostedServer = {
|
|
323
|
-
...buildHostedServer(input.hostedMcpUrl, `\${${
|
|
402
|
+
...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
|
|
324
403
|
enabled: true
|
|
325
404
|
};
|
|
326
405
|
const signerServer = {
|
|
@@ -330,9 +409,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
330
409
|
const merged = mergeHermesYaml(
|
|
331
410
|
existing,
|
|
332
411
|
hostedServer,
|
|
333
|
-
signerServer
|
|
412
|
+
signerServer,
|
|
413
|
+
names,
|
|
414
|
+
target
|
|
334
415
|
);
|
|
335
|
-
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
|
|
416
|
+
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
|
|
336
417
|
const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
|
|
337
418
|
await writeText(envTarget, mergedEnv);
|
|
338
419
|
try {
|
|
@@ -353,11 +434,13 @@ async function writeHermesConfig(input, deps) {
|
|
|
353
434
|
messages: [
|
|
354
435
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
355
436
|
"Restart Hermes (start a new session; gateway users: /restart), then verify with `hermes mcp list`, `hermes mcp test haven`, and `hermes mcp test haven-signer`.",
|
|
437
|
+
"If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
|
|
356
438
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
357
439
|
]
|
|
358
440
|
};
|
|
359
441
|
} catch (err) {
|
|
360
442
|
const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
|
|
443
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
361
444
|
return {
|
|
362
445
|
hostedConfigured: false,
|
|
363
446
|
signerConfigured: false,
|
|
@@ -366,8 +449,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
366
449
|
target: "Hermes Agent config",
|
|
367
450
|
changed: false,
|
|
368
451
|
restartRequired: true,
|
|
369
|
-
messages:
|
|
370
|
-
|
|
452
|
+
messages: unreadable ? [
|
|
453
|
+
`Could not update Hermes Agent config: ${err.message}.`,
|
|
454
|
+
`Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
|
|
455
|
+
] : [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
|
|
456
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
371
457
|
};
|
|
372
458
|
}
|
|
373
459
|
}
|
|
@@ -399,7 +485,7 @@ async function writeCodexConfig(input) {
|
|
|
399
485
|
if (!input.localMcpCommand) {
|
|
400
486
|
throw new Error("local MCP wrapper command is required");
|
|
401
487
|
}
|
|
402
|
-
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
488
|
+
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
|
|
403
489
|
await writeOwnerOnlyText(target, merged2);
|
|
404
490
|
return {
|
|
405
491
|
hostedConfigured: false,
|
|
@@ -414,7 +500,13 @@ async function writeCodexConfig(input) {
|
|
|
414
500
|
]
|
|
415
501
|
};
|
|
416
502
|
}
|
|
417
|
-
const merged = mergeCodexTomlHosted(
|
|
503
|
+
const merged = mergeCodexTomlHosted(
|
|
504
|
+
existing ?? "",
|
|
505
|
+
input.hostedMcpUrl,
|
|
506
|
+
input.apiKey,
|
|
507
|
+
resolveSignerLaunchSpec(input),
|
|
508
|
+
serverNamesFor(input.serverName)
|
|
509
|
+
);
|
|
418
510
|
await writeOwnerOnlyText(target, merged);
|
|
419
511
|
return {
|
|
420
512
|
hostedConfigured: true,
|
|
@@ -456,10 +548,15 @@ async function writeOwnerOnlyText(path$1, value) {
|
|
|
456
548
|
await promises.writeFile(path$1, value, { mode: 384 });
|
|
457
549
|
await promises.chmod(path$1, 384).catch(() => void 0);
|
|
458
550
|
}
|
|
459
|
-
function parseJsonObject(value) {
|
|
460
|
-
|
|
551
|
+
function parseJsonObject(value, configPath) {
|
|
552
|
+
let parsed;
|
|
553
|
+
try {
|
|
554
|
+
parsed = JSON.parse(value);
|
|
555
|
+
} catch {
|
|
556
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
|
|
557
|
+
}
|
|
461
558
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
462
|
-
throw new
|
|
559
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
|
|
463
560
|
}
|
|
464
561
|
return parsed;
|
|
465
562
|
}
|
|
@@ -726,10 +823,11 @@ function configTargetLabel(runtime) {
|
|
|
726
823
|
return "runtime MCP config";
|
|
727
824
|
}
|
|
728
825
|
}
|
|
729
|
-
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
826
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
|
|
730
827
|
var init_config_writers = __esm({
|
|
731
828
|
"src/config-writers.ts"() {
|
|
732
829
|
init_runtime_manifest();
|
|
830
|
+
init_server_names();
|
|
733
831
|
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
734
832
|
HermesConfigRecoveryError = class extends Error {
|
|
735
833
|
constructor() {
|
|
@@ -743,8 +841,37 @@ var init_config_writers = __esm({
|
|
|
743
841
|
this.name = "InvalidCodexTomlError";
|
|
744
842
|
}
|
|
745
843
|
};
|
|
844
|
+
REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
|
|
845
|
+
UnreadableRuntimeConfigError = class extends Error {
|
|
846
|
+
configPath;
|
|
847
|
+
constructor(configPath, detail) {
|
|
848
|
+
super(`${configPath} is not a config Haven can merge into (${detail})`);
|
|
849
|
+
this.name = "UnreadableRuntimeConfigError";
|
|
850
|
+
this.configPath = configPath;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
746
853
|
}
|
|
747
854
|
});
|
|
855
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
856
|
+
let response;
|
|
857
|
+
try {
|
|
858
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
859
|
+
method: "GET",
|
|
860
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
861
|
+
});
|
|
862
|
+
} catch {
|
|
863
|
+
return { status: "network_error" };
|
|
864
|
+
}
|
|
865
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
866
|
+
if (!response.ok) return { status: "bad_response" };
|
|
867
|
+
try {
|
|
868
|
+
const payload = JSON.parse(await response.text());
|
|
869
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
870
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
871
|
+
} catch {
|
|
872
|
+
return { status: "bad_response" };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
748
875
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
749
876
|
let response;
|
|
750
877
|
try {
|
|
@@ -782,7 +909,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
782
909
|
}
|
|
783
910
|
}
|
|
784
911
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
785
|
-
return new Promise((
|
|
912
|
+
return new Promise((resolve9) => {
|
|
786
913
|
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
787
914
|
let stdout = "";
|
|
788
915
|
let settled = false;
|
|
@@ -794,7 +921,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
794
921
|
settled = true;
|
|
795
922
|
clearTimeout(timeout);
|
|
796
923
|
child.kill();
|
|
797
|
-
|
|
924
|
+
resolve9(result);
|
|
798
925
|
};
|
|
799
926
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
800
927
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -908,7 +1035,8 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
908
1035
|
wrapperPath,
|
|
909
1036
|
runtimeDirectory,
|
|
910
1037
|
npmCacheDirectory,
|
|
911
|
-
cliPath
|
|
1038
|
+
cliPath,
|
|
1039
|
+
serverName: input.serverName
|
|
912
1040
|
});
|
|
913
1041
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
914
1042
|
return {
|
|
@@ -1009,6 +1137,7 @@ async function readRuntimeSidecar(credentialDirectory) {
|
|
|
1009
1137
|
}
|
|
1010
1138
|
async function writeRuntimeSidecar(input) {
|
|
1011
1139
|
const value = {
|
|
1140
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1012
1141
|
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1013
1142
|
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1014
1143
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -1039,6 +1168,23 @@ var init_signer_runtime = __esm({
|
|
|
1039
1168
|
}
|
|
1040
1169
|
});
|
|
1041
1170
|
|
|
1171
|
+
// src/connect-error.ts
|
|
1172
|
+
var ConnectError;
|
|
1173
|
+
var init_connect_error = __esm({
|
|
1174
|
+
"src/connect-error.ts"() {
|
|
1175
|
+
ConnectError = class extends Error {
|
|
1176
|
+
code;
|
|
1177
|
+
nextAction;
|
|
1178
|
+
constructor(code, message, nextAction2) {
|
|
1179
|
+
super(message);
|
|
1180
|
+
this.name = "ConnectError";
|
|
1181
|
+
this.code = code;
|
|
1182
|
+
this.nextAction = nextAction2;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1042
1188
|
// src/runtime-registry.ts
|
|
1043
1189
|
function runtimeProfile(runtime, env = process.env) {
|
|
1044
1190
|
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
@@ -1048,6 +1194,42 @@ function normalizeRuntime(runtime, env = process.env) {
|
|
|
1048
1194
|
if (explicit) return explicit;
|
|
1049
1195
|
return detectRuntime(env) ?? "other";
|
|
1050
1196
|
}
|
|
1197
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1198
|
+
const env = options.env ?? process.env;
|
|
1199
|
+
if (force !== void 0) {
|
|
1200
|
+
const forced = normalizeRuntimeName(force);
|
|
1201
|
+
if (!forced) {
|
|
1202
|
+
throw new ConnectError(
|
|
1203
|
+
"runtime_force_unrecognized",
|
|
1204
|
+
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1205
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
return { runtime: forced, source: "force" };
|
|
1209
|
+
}
|
|
1210
|
+
const detected = detectRuntime(env);
|
|
1211
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1212
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1213
|
+
if (supplied && !hint) {
|
|
1214
|
+
if (!detected) {
|
|
1215
|
+
throw new ConnectError(
|
|
1216
|
+
"runtime_unrecognized",
|
|
1217
|
+
`"${supplied}" is not an agent runtime Haven knows. Valid values: ${RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
|
|
1218
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1222
|
+
}
|
|
1223
|
+
if (detected && hint && detected !== hint) {
|
|
1224
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1225
|
+
}
|
|
1226
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
1227
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
1228
|
+
if (options.promptForRuntime) {
|
|
1229
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1230
|
+
}
|
|
1231
|
+
return { runtime: null, source: "none" };
|
|
1232
|
+
}
|
|
1051
1233
|
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1052
1234
|
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1053
1235
|
return mode === "restart-session" || mode === "restart-app";
|
|
@@ -1068,9 +1250,10 @@ function detectRuntime(env) {
|
|
|
1068
1250
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1069
1251
|
return null;
|
|
1070
1252
|
}
|
|
1071
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1253
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
1072
1254
|
var init_runtime_registry = __esm({
|
|
1073
1255
|
"src/runtime-registry.ts"() {
|
|
1256
|
+
init_connect_error();
|
|
1074
1257
|
RUNTIME_PROFILES = {
|
|
1075
1258
|
"claude-code": {
|
|
1076
1259
|
id: "claude-code",
|
|
@@ -1141,6 +1324,11 @@ var init_runtime_registry = __esm({
|
|
|
1141
1324
|
"claude-code": "claude-code",
|
|
1142
1325
|
claudecode: "claude-code",
|
|
1143
1326
|
"claude_code": "claude-code",
|
|
1327
|
+
// #1682: Cowork runs Claude Code's config, so it resolves to the same
|
|
1328
|
+
// profile. The dashboard's command carries no --runtime for it (it is a
|
|
1329
|
+
// detected, command-path runtime), but an explicit `--runtime cowork` typed
|
|
1330
|
+
// by hand must not fall through to the no-runtime refusal.
|
|
1331
|
+
cowork: "claude-code",
|
|
1144
1332
|
codex: "codex-cli",
|
|
1145
1333
|
"codex-cli": "codex-cli",
|
|
1146
1334
|
codexcli: "codex-cli",
|
|
@@ -1170,9 +1358,18 @@ var init_runtime_registry = __esm({
|
|
|
1170
1358
|
"hermes-agent": "hermes",
|
|
1171
1359
|
hermes_agent: "hermes",
|
|
1172
1360
|
hermesagent: "hermes",
|
|
1361
|
+
// #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
|
|
1362
|
+
// into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
|
|
1363
|
+
// the 'other' profile's behaviour (credentials written to disk, no config
|
|
1364
|
+
// auto-written, manual finish), so it resolves there rather than earning a
|
|
1365
|
+
// profile whose only distinguishing feature would be its label.
|
|
1366
|
+
openclaw: "other",
|
|
1367
|
+
"open-claw": "other",
|
|
1368
|
+
"open_claw": "other",
|
|
1173
1369
|
other: "other",
|
|
1174
1370
|
manual: "other"
|
|
1175
1371
|
};
|
|
1372
|
+
RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
|
|
1176
1373
|
}
|
|
1177
1374
|
});
|
|
1178
1375
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1249,6 +1446,82 @@ var init_signer_consent = __esm({
|
|
|
1249
1446
|
}
|
|
1250
1447
|
});
|
|
1251
1448
|
|
|
1449
|
+
// src/tombstone.ts
|
|
1450
|
+
var tombstone_exports = {};
|
|
1451
|
+
__export(tombstone_exports, {
|
|
1452
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
1453
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
1454
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
1455
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
1456
|
+
});
|
|
1457
|
+
function tombstoneScript(info) {
|
|
1458
|
+
const lines = [
|
|
1459
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
1460
|
+
"",
|
|
1461
|
+
` agent: ${info.agent_id}`,
|
|
1462
|
+
` retired at: ${info.retired_at}`,
|
|
1463
|
+
` reason: ${info.reason}`,
|
|
1464
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
1465
|
+
"",
|
|
1466
|
+
"This process is running with a wiring snapshot that predates the",
|
|
1467
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
1468
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
1469
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
1470
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
1471
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
1472
|
+
"",
|
|
1473
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
1474
|
+
];
|
|
1475
|
+
return [
|
|
1476
|
+
"#!/usr/bin/env node",
|
|
1477
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
1478
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
1479
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
1480
|
+
"process.exit(1)",
|
|
1481
|
+
""
|
|
1482
|
+
].join("\n");
|
|
1483
|
+
}
|
|
1484
|
+
async function writeAgentTombstone(input) {
|
|
1485
|
+
const dirStat = await promises.stat(input.directory).catch(() => null);
|
|
1486
|
+
if (!dirStat?.isDirectory()) {
|
|
1487
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
1488
|
+
}
|
|
1489
|
+
const info = {
|
|
1490
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
1491
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
1492
|
+
// like every other output path, at the write layer so any future caller
|
|
1493
|
+
// inherits it. (#1681 review, finding 1)
|
|
1494
|
+
agent_id: input.agentId,
|
|
1495
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1496
|
+
reason: redactSecrets(input.reason),
|
|
1497
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1498
|
+
};
|
|
1499
|
+
const binDir = path.join(input.directory, "bin");
|
|
1500
|
+
await promises.mkdir(binDir, { recursive: true });
|
|
1501
|
+
const wrapperPath = path.join(binDir, "haven-signer.mjs");
|
|
1502
|
+
await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
1503
|
+
await promises.chmod(wrapperPath, 493);
|
|
1504
|
+
await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
1505
|
+
return info;
|
|
1506
|
+
}
|
|
1507
|
+
async function readAgentTombstone(directory) {
|
|
1508
|
+
try {
|
|
1509
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
1510
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
1511
|
+
return parsed;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
1517
|
+
var init_tombstone = __esm({
|
|
1518
|
+
"src/tombstone.ts"() {
|
|
1519
|
+
init_redact();
|
|
1520
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
1521
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1252
1525
|
// src/doctor.ts
|
|
1253
1526
|
var doctor_exports = {};
|
|
1254
1527
|
__export(doctor_exports, {
|
|
@@ -1256,129 +1529,101 @@ __export(doctor_exports, {
|
|
|
1256
1529
|
runRepair: () => runRepair
|
|
1257
1530
|
});
|
|
1258
1531
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1259
|
-
|
|
1260
|
-
const root = path.join(homeDir, ".haven", "agents");
|
|
1532
|
+
const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
|
|
1261
1533
|
let entries = [];
|
|
1262
1534
|
try {
|
|
1263
1535
|
entries = await promises.readdir(root);
|
|
1264
1536
|
} catch {
|
|
1265
|
-
return {};
|
|
1537
|
+
return explicit ? { directory: explicit, others: [] } : { others: [] };
|
|
1266
1538
|
}
|
|
1267
1539
|
const candidates = [];
|
|
1540
|
+
const tombstonedOnly = [];
|
|
1268
1541
|
for (const entry of entries) {
|
|
1269
1542
|
const directory = path.join(root, entry);
|
|
1270
1543
|
try {
|
|
1271
1544
|
const s = await promises.stat(path.join(directory, "identity.json"));
|
|
1272
1545
|
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1273
1546
|
} catch {
|
|
1547
|
+
try {
|
|
1548
|
+
await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
|
|
1549
|
+
tombstonedOnly.push(directory);
|
|
1550
|
+
} catch {
|
|
1551
|
+
}
|
|
1274
1552
|
}
|
|
1275
1553
|
}
|
|
1276
|
-
if (candidates.length === 0) return {};
|
|
1277
1554
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1555
|
+
if (explicit) {
|
|
1556
|
+
return {
|
|
1557
|
+
directory: explicit,
|
|
1558
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly].filter((d) => d !== explicit)
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0) return { others: [] };
|
|
1278
1562
|
return {
|
|
1279
|
-
directory: candidates[0]
|
|
1280
|
-
|
|
1563
|
+
directory: candidates[0]?.directory,
|
|
1564
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly]
|
|
1281
1565
|
};
|
|
1282
1566
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1567
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
1568
|
+
if (configText === null) return isPrimary;
|
|
1569
|
+
if (slug) {
|
|
1570
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
1571
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
1576
|
+
if (bareOwnerExists) return false;
|
|
1577
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
1578
|
+
}
|
|
1579
|
+
async function readIdentity(directory) {
|
|
1580
|
+
try {
|
|
1581
|
+
return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
1582
|
+
} catch {
|
|
1583
|
+
return void 0;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
async function checksForAgent(entry, input, deps) {
|
|
1587
|
+
const { directory, identity, sidecar } = entry;
|
|
1285
1588
|
const checks = [];
|
|
1286
1589
|
let signerCapabilities;
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1590
|
+
let signerFile;
|
|
1591
|
+
try {
|
|
1592
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
1593
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
1594
|
+
} catch {
|
|
1595
|
+
signerFile = void 0;
|
|
1596
|
+
}
|
|
1597
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
1598
|
+
checks.push({
|
|
1599
|
+
id: "credentials",
|
|
1600
|
+
label: "Agent credentials",
|
|
1601
|
+
ok: credentialsOk,
|
|
1602
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
1603
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1604
|
+
});
|
|
1605
|
+
if (!sidecar) {
|
|
1291
1606
|
checks.push({
|
|
1292
|
-
id: "
|
|
1293
|
-
label: "
|
|
1607
|
+
id: "signer_runtime",
|
|
1608
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1294
1609
|
ok: false,
|
|
1295
|
-
detail: "No
|
|
1296
|
-
repair: `Run
|
|
1610
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1611
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1297
1612
|
});
|
|
1298
1613
|
} else {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
identity = void 0;
|
|
1303
|
-
}
|
|
1304
|
-
try {
|
|
1305
|
-
const signer = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
|
|
1306
|
-
signerParses = typeof signer === "object" && signer !== null;
|
|
1307
|
-
} catch {
|
|
1308
|
-
signerParses = false;
|
|
1309
|
-
}
|
|
1310
|
-
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1614
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1615
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1616
|
+
const ok = matches && versionOk;
|
|
1311
1617
|
checks.push({
|
|
1312
|
-
id: "
|
|
1313
|
-
label: "
|
|
1618
|
+
id: "signer_runtime",
|
|
1619
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1314
1620
|
ok,
|
|
1315
|
-
detail: ok ? `
|
|
1316
|
-
...ok ? {} : { repair: `
|
|
1621
|
+
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1622
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1317
1623
|
});
|
|
1318
1624
|
}
|
|
1319
|
-
|
|
1320
|
-
if (
|
|
1321
|
-
sidecar = await readRuntimeSidecar(directory);
|
|
1322
|
-
if (!sidecar) {
|
|
1323
|
-
checks.push({
|
|
1324
|
-
id: "signer_runtime",
|
|
1325
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1326
|
-
ok: false,
|
|
1327
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1328
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1329
|
-
});
|
|
1330
|
-
} else {
|
|
1331
|
-
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1332
|
-
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1333
|
-
const ok = matches && versionOk;
|
|
1334
|
-
checks.push({
|
|
1335
|
-
id: "signer_runtime",
|
|
1336
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1337
|
-
ok,
|
|
1338
|
-
detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
|
|
1339
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1344
|
-
if (configPath === null) {
|
|
1345
|
-
checks.push({
|
|
1346
|
-
id: "runtime_config",
|
|
1347
|
-
label: "Runtime MCP config",
|
|
1348
|
-
ok: true,
|
|
1349
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1350
|
-
});
|
|
1351
|
-
} else {
|
|
1352
|
-
let configText = null;
|
|
1353
|
-
try {
|
|
1354
|
-
configText = await promises.readFile(configPath, "utf8");
|
|
1355
|
-
} catch {
|
|
1356
|
-
configText = null;
|
|
1357
|
-
}
|
|
1358
|
-
if (configText === null) {
|
|
1359
|
-
checks.push({
|
|
1360
|
-
id: "runtime_config",
|
|
1361
|
-
label: "Runtime MCP config",
|
|
1362
|
-
ok: false,
|
|
1363
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1364
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1365
|
-
});
|
|
1366
|
-
} else {
|
|
1367
|
-
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1368
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1369
|
-
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1370
|
-
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1371
|
-
checks.push({
|
|
1372
|
-
id: "runtime_config",
|
|
1373
|
-
label: "Runtime MCP config",
|
|
1374
|
-
ok,
|
|
1375
|
-
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${sidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
|
|
1376
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1381
|
-
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1625
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1626
|
+
if (identity?.api_key && hostedUrl) {
|
|
1382
1627
|
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1383
1628
|
checks.push({
|
|
1384
1629
|
id: "hosted_mcp",
|
|
@@ -1398,7 +1643,43 @@ async function runDoctor(input, deps = {}) {
|
|
|
1398
1643
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1399
1644
|
});
|
|
1400
1645
|
}
|
|
1401
|
-
|
|
1646
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
1647
|
+
if (identity?.api_key && identity.api_url) {
|
|
1648
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
1649
|
+
identity.api_key,
|
|
1650
|
+
identity.api_url,
|
|
1651
|
+
deps.fetch
|
|
1652
|
+
);
|
|
1653
|
+
if (probe.status !== "ok") {
|
|
1654
|
+
checks.push({
|
|
1655
|
+
id: "identity_match",
|
|
1656
|
+
label: "Hosted identity matches the local signing key",
|
|
1657
|
+
ok: false,
|
|
1658
|
+
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.`,
|
|
1659
|
+
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 --runtime ${input.runtime}`
|
|
1660
|
+
});
|
|
1661
|
+
} else if (!localDelegate) {
|
|
1662
|
+
checks.push({
|
|
1663
|
+
id: "identity_match",
|
|
1664
|
+
label: "Hosted identity matches the local signing key",
|
|
1665
|
+
ok: false,
|
|
1666
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
1667
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
1668
|
+
});
|
|
1669
|
+
} else {
|
|
1670
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
1671
|
+
checks.push({
|
|
1672
|
+
id: "identity_match",
|
|
1673
|
+
label: "Hosted identity matches the local signing key",
|
|
1674
|
+
ok: same,
|
|
1675
|
+
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.`,
|
|
1676
|
+
...same ? {} : {
|
|
1677
|
+
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.`
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (sidecar) {
|
|
1402
1683
|
const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
|
|
1403
1684
|
if (!consent.acknowledged) {
|
|
1404
1685
|
checks.push({
|
|
@@ -1426,7 +1707,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
1426
1707
|
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1427
1708
|
});
|
|
1428
1709
|
}
|
|
1429
|
-
} else
|
|
1710
|
+
} else {
|
|
1430
1711
|
checks.push({
|
|
1431
1712
|
id: "signer_process",
|
|
1432
1713
|
label: "Signer stdio handshake",
|
|
@@ -1435,6 +1716,171 @@ async function runDoctor(input, deps = {}) {
|
|
|
1435
1716
|
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1436
1717
|
});
|
|
1437
1718
|
}
|
|
1719
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
1720
|
+
}
|
|
1721
|
+
async function runDoctor(input, deps = {}) {
|
|
1722
|
+
const homeDir = deps.homeDir ?? os.homedir();
|
|
1723
|
+
const checks = [];
|
|
1724
|
+
let signerCapabilities;
|
|
1725
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1726
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1727
|
+
let configText = null;
|
|
1728
|
+
if (configPath !== null) {
|
|
1729
|
+
try {
|
|
1730
|
+
configText = await promises.readFile(configPath, "utf8");
|
|
1731
|
+
} catch {
|
|
1732
|
+
configText = null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
1736
|
+
let bareOwnerExists = false;
|
|
1737
|
+
for (const dir of allDirectories) {
|
|
1738
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1739
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
1740
|
+
bareOwnerExists = true;
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const inventory = [];
|
|
1745
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
1746
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const dir of allDirectories) {
|
|
1748
|
+
const identity = await readIdentity(dir);
|
|
1749
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1750
|
+
const tombstone = await readAgentTombstone(dir);
|
|
1751
|
+
const slug = sidecar?.server_name;
|
|
1752
|
+
const names = serverNamesFor(slug);
|
|
1753
|
+
if (!identity?.api_key) {
|
|
1754
|
+
inventory.push({
|
|
1755
|
+
...slug ? { slug } : {},
|
|
1756
|
+
...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
|
|
1757
|
+
directory: dir,
|
|
1758
|
+
classification: tombstone ? "retired" : "orphaned",
|
|
1759
|
+
checks: []
|
|
1760
|
+
});
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
1764
|
+
const entry = {
|
|
1765
|
+
...slug ? { slug } : {},
|
|
1766
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
1767
|
+
directory: dir,
|
|
1768
|
+
classification: wired ? "wired" : "superseded",
|
|
1769
|
+
checks: []
|
|
1770
|
+
};
|
|
1771
|
+
if (wired) {
|
|
1772
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
1773
|
+
entry.checks = result.checks;
|
|
1774
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
1775
|
+
}
|
|
1776
|
+
inventory.push(entry);
|
|
1777
|
+
}
|
|
1778
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
1779
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
1780
|
+
if (primaryDirectory) {
|
|
1781
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
1782
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
1783
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
1784
|
+
}
|
|
1785
|
+
if (!primaryDirectory) {
|
|
1786
|
+
checks.push({
|
|
1787
|
+
id: "credentials",
|
|
1788
|
+
label: "Agent credentials",
|
|
1789
|
+
ok: false,
|
|
1790
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1791
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1792
|
+
});
|
|
1793
|
+
} else {
|
|
1794
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
1795
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
1796
|
+
if (primaryChecksById.size === 0) {
|
|
1797
|
+
const result = await checksForAgent(
|
|
1798
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
1799
|
+
input,
|
|
1800
|
+
deps
|
|
1801
|
+
);
|
|
1802
|
+
signerCapabilities = result.signerCapabilities;
|
|
1803
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
1804
|
+
}
|
|
1805
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
1806
|
+
const check = primaryChecksById.get(id);
|
|
1807
|
+
if (check) checks.push(check);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (configPath === null) {
|
|
1811
|
+
checks.push({
|
|
1812
|
+
id: "runtime_config",
|
|
1813
|
+
label: "Runtime MCP config",
|
|
1814
|
+
ok: true,
|
|
1815
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1816
|
+
});
|
|
1817
|
+
} else if (configText === null) {
|
|
1818
|
+
checks.push({
|
|
1819
|
+
id: "runtime_config",
|
|
1820
|
+
label: "Runtime MCP config",
|
|
1821
|
+
ok: false,
|
|
1822
|
+
detail: `No runtime config at ${configPath}.`,
|
|
1823
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1824
|
+
});
|
|
1825
|
+
} else {
|
|
1826
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
1827
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
1828
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
1829
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1830
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
1831
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
1832
|
+
checks.push({
|
|
1833
|
+
id: "runtime_config",
|
|
1834
|
+
label: "Runtime MCP config",
|
|
1835
|
+
ok,
|
|
1836
|
+
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)" : ""}.`,
|
|
1837
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
for (const id of ["hosted_mcp", "identity_match"]) {
|
|
1841
|
+
const check = primaryChecksById.get(id);
|
|
1842
|
+
if (check) checks.push(check);
|
|
1843
|
+
}
|
|
1844
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
1845
|
+
if (otherEntries.length > 0) {
|
|
1846
|
+
const live = [];
|
|
1847
|
+
const revoked = [];
|
|
1848
|
+
const unverifiable = [];
|
|
1849
|
+
const retired = [];
|
|
1850
|
+
for (const entry of otherEntries) {
|
|
1851
|
+
const identity = await readIdentity(entry.directory);
|
|
1852
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
1853
|
+
const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? path.basename(entry.directory);
|
|
1854
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1855
|
+
if (!identity?.api_key || !otherUrl) {
|
|
1856
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
1857
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
1861
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
1862
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
1863
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
1864
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
1865
|
+
}
|
|
1866
|
+
const parts = [];
|
|
1867
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
1868
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
1869
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
1870
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
1871
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
1872
|
+
checks.push({
|
|
1873
|
+
id: "superseded_agents",
|
|
1874
|
+
label: "Superseded agent credentials",
|
|
1875
|
+
ok: supersededLive.length === 0,
|
|
1876
|
+
detail: supersededLive.length > 0 ? `${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.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
1877
|
+
...supersededLive.length > 0 ? {
|
|
1878
|
+
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.`
|
|
1879
|
+
} : {}
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
1883
|
+
if (signerProcess) checks.push(signerProcess);
|
|
1438
1884
|
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1439
1885
|
checks.push({
|
|
1440
1886
|
id: "restart",
|
|
@@ -1442,20 +1888,24 @@ async function runDoctor(input, deps = {}) {
|
|
|
1442
1888
|
ok: true,
|
|
1443
1889
|
detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
|
|
1444
1890
|
});
|
|
1891
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
1445
1892
|
return {
|
|
1446
1893
|
version: 1,
|
|
1447
|
-
ok: checks.every((check) => check.ok),
|
|
1894
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
1448
1895
|
runtime: input.runtime,
|
|
1449
|
-
credentialDirectory:
|
|
1896
|
+
credentialDirectory: primaryDirectory,
|
|
1450
1897
|
checks,
|
|
1898
|
+
agents: inventory,
|
|
1451
1899
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
1452
1900
|
};
|
|
1453
1901
|
}
|
|
1454
1902
|
async function runRepair(input, deps = {}) {
|
|
1455
1903
|
const homeDir = deps.homeDir ?? os.homedir();
|
|
1456
1904
|
const messages = [];
|
|
1457
|
-
const { directory,
|
|
1458
|
-
if (
|
|
1905
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1906
|
+
if (others.length > 0) {
|
|
1907
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
1908
|
+
}
|
|
1459
1909
|
if (!directory) {
|
|
1460
1910
|
return {
|
|
1461
1911
|
ok: false,
|
|
@@ -1517,6 +1967,9 @@ var init_doctor = __esm({
|
|
|
1517
1967
|
init_config_writers();
|
|
1518
1968
|
init_runtime_registry();
|
|
1519
1969
|
init_signer_consent();
|
|
1970
|
+
init_tombstone();
|
|
1971
|
+
init_server_names();
|
|
1972
|
+
init_redact();
|
|
1520
1973
|
RERUN = "npx @haven_ai/connect@alpha";
|
|
1521
1974
|
}
|
|
1522
1975
|
});
|
|
@@ -1626,16 +2079,9 @@ function agentApiKeyPrefix(apiKey) {
|
|
|
1626
2079
|
return apiKey.slice(0, 12);
|
|
1627
2080
|
}
|
|
1628
2081
|
|
|
1629
|
-
// src/
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
function redactSecrets(value) {
|
|
1633
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1634
|
-
}
|
|
1635
|
-
function shortAddress(address) {
|
|
1636
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1637
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1638
|
-
}
|
|
2082
|
+
// src/runtime.ts
|
|
2083
|
+
init_redact();
|
|
2084
|
+
init_server_names();
|
|
1639
2085
|
async function preflightCredentialStorage(input = {}) {
|
|
1640
2086
|
const directory = defaultCredentialRoot(input.baseDir);
|
|
1641
2087
|
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -1649,7 +2095,7 @@ async function preflightCredentialStorage(input = {}) {
|
|
|
1649
2095
|
return directory;
|
|
1650
2096
|
}
|
|
1651
2097
|
async function writeCredentialFiles(input) {
|
|
1652
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
2098
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
1653
2099
|
await promises.mkdir(directory, { recursive: true, mode: 448 });
|
|
1654
2100
|
await restrictPermissions(directory, 448, input.warn);
|
|
1655
2101
|
const identityPath = path.join(directory, "identity.json");
|
|
@@ -1714,6 +2160,17 @@ async function writeCredentialFiles(input) {
|
|
|
1714
2160
|
}
|
|
1715
2161
|
return { directory, identityPath, signerPath, agentPath };
|
|
1716
2162
|
}
|
|
2163
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
2164
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
2165
|
+
try {
|
|
2166
|
+
await promises.stat(path.join(directory, "identity.json"));
|
|
2167
|
+
} catch {
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
`The name "${serverName}" is already wired on this machine (${directory} holds credentials). Pick a different --name, or revoke and remove that agent first \u2014 connect never overwrites credentials.`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
1717
2174
|
function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
|
|
1718
2175
|
return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1719
2176
|
}
|
|
@@ -1753,6 +2210,7 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
1753
2210
|
|
|
1754
2211
|
// src/runtime-install.ts
|
|
1755
2212
|
init_config_writers();
|
|
2213
|
+
init_server_names();
|
|
1756
2214
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1757
2215
|
try {
|
|
1758
2216
|
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
@@ -1878,7 +2336,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1878
2336
|
wrapperPath,
|
|
1879
2337
|
runtimeDirectory,
|
|
1880
2338
|
npmCacheDirectory,
|
|
1881
|
-
cliPath
|
|
2339
|
+
cliPath,
|
|
2340
|
+
serverName: input.serverName
|
|
1882
2341
|
});
|
|
1883
2342
|
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1884
2343
|
return {
|
|
@@ -1974,6 +2433,7 @@ async function writeWrapper2(input) {
|
|
|
1974
2433
|
}
|
|
1975
2434
|
async function writeRuntimeSidecar2(input) {
|
|
1976
2435
|
const value = {
|
|
2436
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1977
2437
|
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1978
2438
|
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1979
2439
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -2207,12 +2667,13 @@ async function installRuntime(input, deps = {}) {
|
|
|
2207
2667
|
}
|
|
2208
2668
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2209
2669
|
progress("Setting up your Haven tools\u2026");
|
|
2210
|
-
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2670
|
+
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2211
2671
|
runtime,
|
|
2212
2672
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
2213
2673
|
apiKey: input.apiKey,
|
|
2214
2674
|
identityPath: input.identityPath,
|
|
2215
2675
|
signerPath: input.signerPath,
|
|
2676
|
+
serverName: input.serverName,
|
|
2216
2677
|
credentialDirectory: input.credentialDirectory,
|
|
2217
2678
|
localMcpCommand: localRuntimeInstall?.command,
|
|
2218
2679
|
signerCommand,
|
|
@@ -2292,7 +2753,7 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
2292
2753
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2293
2754
|
};
|
|
2294
2755
|
}
|
|
2295
|
-
async function configureClaudeCode(deps, localMcpCommand) {
|
|
2756
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2296
2757
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2297
2758
|
const serverJson = JSON.stringify({
|
|
2298
2759
|
type: "stdio",
|
|
@@ -2302,12 +2763,13 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
2302
2763
|
});
|
|
2303
2764
|
try {
|
|
2304
2765
|
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2305
|
-
|
|
2306
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2307
|
-
await runCommand("claude", ["mcp", "
|
|
2308
|
-
|
|
2766
|
+
const names = serverNamesFor(serverName);
|
|
2767
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2768
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2769
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2770
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2309
2771
|
});
|
|
2310
|
-
const verified = await runCommand("claude", ["mcp", "get",
|
|
2772
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2311
2773
|
return {
|
|
2312
2774
|
hostedConfigured: false,
|
|
2313
2775
|
signerConfigured: true,
|
|
@@ -2352,11 +2814,12 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
|
2352
2814
|
env: {}
|
|
2353
2815
|
});
|
|
2354
2816
|
try {
|
|
2355
|
-
|
|
2356
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2357
|
-
await runCommand("claude", ["mcp", "
|
|
2358
|
-
await runCommand("claude", ["mcp", "add-json",
|
|
2359
|
-
|
|
2817
|
+
const names = serverNamesFor(input.serverName);
|
|
2818
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2819
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2820
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2821
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2822
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2360
2823
|
return {
|
|
2361
2824
|
hostedConfigured: true,
|
|
2362
2825
|
signerConfigured: true,
|
|
@@ -2460,7 +2923,8 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2460
2923
|
credentialDirectory: input.credentialDirectory,
|
|
2461
2924
|
identityPath: input.identityPath,
|
|
2462
2925
|
signerPath: input.signerPath,
|
|
2463
|
-
homeDir: deps.homeDir
|
|
2926
|
+
homeDir: deps.homeDir,
|
|
2927
|
+
serverName: input.serverName
|
|
2464
2928
|
});
|
|
2465
2929
|
}
|
|
2466
2930
|
async function prepareSignerForRuntime(input, deps) {
|
|
@@ -2474,7 +2938,8 @@ async function prepareSignerForRuntime(input, deps) {
|
|
|
2474
2938
|
return prepare({
|
|
2475
2939
|
credentialDirectory: input.credentialDirectory,
|
|
2476
2940
|
signerPath: input.signerPath,
|
|
2477
|
-
homeDir: deps.homeDir
|
|
2941
|
+
homeDir: deps.homeDir,
|
|
2942
|
+
serverName: input.serverName
|
|
2478
2943
|
});
|
|
2479
2944
|
}
|
|
2480
2945
|
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
@@ -2494,8 +2959,179 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2494
2959
|
|
|
2495
2960
|
// src/runtime.ts
|
|
2496
2961
|
init_runtime_registry();
|
|
2962
|
+
init_connect_error();
|
|
2963
|
+
|
|
2964
|
+
// src/installed-clients.ts
|
|
2965
|
+
init_connect_error();
|
|
2966
|
+
init_config_writers();
|
|
2967
|
+
init_runtime_registry();
|
|
2968
|
+
var SCAN_ORDER = [
|
|
2969
|
+
"claude-code",
|
|
2970
|
+
"codex-cli",
|
|
2971
|
+
"cursor",
|
|
2972
|
+
"vscode",
|
|
2973
|
+
"vscode-insiders",
|
|
2974
|
+
"claude-desktop",
|
|
2975
|
+
"hermes"
|
|
2976
|
+
];
|
|
2977
|
+
function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env = process.env) {
|
|
2978
|
+
const targets = [
|
|
2979
|
+
{
|
|
2980
|
+
runtime: "claude-code",
|
|
2981
|
+
label: "Claude Code",
|
|
2982
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2983
|
+
// not by writing a file this module owns — so its evidence is the
|
|
2984
|
+
// client directory, never a config path.
|
|
2985
|
+
configPath: null,
|
|
2986
|
+
markers: [path.join(homeDir, ".claude"), path.join(homeDir, ".claude.json")]
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
runtime: "codex-cli",
|
|
2990
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
2991
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
2992
|
+
// whose answers are the same write.
|
|
2993
|
+
label: "Codex (CLI or Desktop)",
|
|
2994
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
2995
|
+
markers: [path.join(homeDir, ".codex")]
|
|
2996
|
+
},
|
|
2997
|
+
{
|
|
2998
|
+
runtime: "cursor",
|
|
2999
|
+
label: "Cursor",
|
|
3000
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
3001
|
+
markers: [path.join(homeDir, ".cursor")]
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
runtime: "vscode",
|
|
3005
|
+
label: "VS Code",
|
|
3006
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3007
|
+
markers: [path.resolve(cwd, ".vscode")]
|
|
3008
|
+
},
|
|
3009
|
+
{
|
|
3010
|
+
runtime: "vscode-insiders",
|
|
3011
|
+
label: "VS Code Insiders",
|
|
3012
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3013
|
+
markers: []
|
|
3014
|
+
},
|
|
3015
|
+
{
|
|
3016
|
+
runtime: "claude-desktop",
|
|
3017
|
+
label: "Claude Desktop",
|
|
3018
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3019
|
+
markers: []
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
runtime: "hermes",
|
|
3023
|
+
label: "Hermes Agent",
|
|
3024
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3025
|
+
markers: [env.HERMES_HOME ?? path.join(homeDir, ".hermes")]
|
|
3026
|
+
}
|
|
3027
|
+
];
|
|
3028
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3029
|
+
}
|
|
3030
|
+
async function scanInstalledClients(options = {}) {
|
|
3031
|
+
const exists = options.exists ?? pathExists;
|
|
3032
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3033
|
+
const found = [];
|
|
3034
|
+
for (const target of targets) {
|
|
3035
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3036
|
+
found.push({
|
|
3037
|
+
runtime: target.runtime,
|
|
3038
|
+
label: target.label,
|
|
3039
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3040
|
+
configPath: target.configPath,
|
|
3041
|
+
evidence: "config-file"
|
|
3042
|
+
});
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
for (const marker of target.markers) {
|
|
3046
|
+
if (!await exists(marker)) continue;
|
|
3047
|
+
found.push({
|
|
3048
|
+
runtime: target.runtime,
|
|
3049
|
+
label: target.label,
|
|
3050
|
+
detail: `installed (${marker})`,
|
|
3051
|
+
configPath: target.configPath,
|
|
3052
|
+
evidence: "client-directory"
|
|
3053
|
+
});
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return found.sort((a, b) => {
|
|
3058
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3059
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3063
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3064
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3065
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3066
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3067
|
+
candidates.forEach((candidate, index) => {
|
|
3068
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3069
|
+
`);
|
|
3070
|
+
});
|
|
3071
|
+
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");
|
|
3072
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3073
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3074
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3075
|
+
const trimmed = answer.trim();
|
|
3076
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3077
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3078
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3079
|
+
return candidates[picked - 1].runtime;
|
|
3080
|
+
}
|
|
3081
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3082
|
+
`);
|
|
3083
|
+
}
|
|
3084
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3085
|
+
}
|
|
3086
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3087
|
+
const candidates = await scanInstalledClients(options);
|
|
3088
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3089
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3090
|
+
}
|
|
3091
|
+
function noInstalledClientsError() {
|
|
3092
|
+
return new ConnectError(
|
|
3093
|
+
"runtime_no_installed_clients",
|
|
3094
|
+
"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.",
|
|
3095
|
+
"rerun_connect_with_explicit_runtime"
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
function promptAbortedError(reason) {
|
|
3099
|
+
return new ConnectError(
|
|
3100
|
+
"runtime_prompt_aborted",
|
|
3101
|
+
`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 setup command again, or pass --runtime <name> to skip the prompt.`,
|
|
3102
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
async function pathExists(path) {
|
|
3106
|
+
try {
|
|
3107
|
+
await promises.access(path);
|
|
3108
|
+
return true;
|
|
3109
|
+
} catch {
|
|
3110
|
+
return false;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function defaultPromptIo() {
|
|
3114
|
+
return {
|
|
3115
|
+
write: (text) => process.stdout.write(text),
|
|
3116
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3117
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3118
|
+
let settled = false;
|
|
3119
|
+
const settle = (value) => {
|
|
3120
|
+
if (settled) return;
|
|
3121
|
+
settled = true;
|
|
3122
|
+
rl.close();
|
|
3123
|
+
resolvePromise(value);
|
|
3124
|
+
};
|
|
3125
|
+
rl.once("SIGINT", () => settle(null));
|
|
3126
|
+
rl.once("close", () => settle(null));
|
|
3127
|
+
rl.question(query, (answer) => settle(answer));
|
|
3128
|
+
})
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// src/runtime.ts
|
|
2497
3133
|
init_runtime_manifest();
|
|
2498
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3134
|
+
var CONNECTOR_VERSION = "0.1.29-alpha.0";
|
|
2499
3135
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2500
3136
|
async function runConnect(options, deps = {}) {
|
|
2501
3137
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2511,24 +3147,49 @@ async function runConnect(options, deps = {}) {
|
|
|
2511
3147
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
2512
3148
|
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
2513
3149
|
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
2514
|
-
const
|
|
3150
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3151
|
+
env: deps.env ?? process.env,
|
|
3152
|
+
selfReported: options.runtimeSelfReport,
|
|
3153
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3154
|
+
});
|
|
3155
|
+
if (!selection.runtime) {
|
|
3156
|
+
throw new ConnectError(
|
|
3157
|
+
"runtime_undetermined",
|
|
3158
|
+
`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. Nothing was written and the Haven setup token is still unused.`,
|
|
3159
|
+
"rerun_connect_with_explicit_runtime"
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
const runtime = selection.runtime;
|
|
3163
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
2515
3164
|
if (options.localMcp) {
|
|
2516
|
-
|
|
2517
|
-
if (!supportsLocalMcp(resolvedRuntime)) {
|
|
3165
|
+
if (!supportsLocalMcp(runtime)) {
|
|
2518
3166
|
throw new Error(
|
|
2519
|
-
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(
|
|
3167
|
+
`--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.`
|
|
2520
3168
|
);
|
|
2521
3169
|
}
|
|
2522
3170
|
}
|
|
3171
|
+
if (selection.overrodeHint) {
|
|
3172
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3173
|
+
}
|
|
3174
|
+
if (selection.discardedHint) {
|
|
3175
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${RUNTIME_FLAG_VALUES})`);
|
|
3176
|
+
}
|
|
3177
|
+
if (selection.source === "prompted") {
|
|
3178
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3179
|
+
}
|
|
2523
3180
|
log("Warming up your connection to Haven\u2026");
|
|
2524
3181
|
const setup = await api.resolveSetup({
|
|
2525
3182
|
setupToken: options.setupToken,
|
|
2526
3183
|
connectorVersion,
|
|
2527
|
-
runtime
|
|
3184
|
+
runtime
|
|
2528
3185
|
});
|
|
2529
3186
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
2530
3187
|
printSetupSummary(setup, log);
|
|
2531
3188
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3189
|
+
if (options.serverName) {
|
|
3190
|
+
assertValidServerSlug(options.serverName);
|
|
3191
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3192
|
+
}
|
|
2532
3193
|
log("Checked local credential storage \u2014 all clear.");
|
|
2533
3194
|
const localKey = generateKey();
|
|
2534
3195
|
const localApiKey = generateLocalApiKey();
|
|
@@ -2540,7 +3201,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2540
3201
|
registration = await api.registerSetup({
|
|
2541
3202
|
setupToken: options.setupToken,
|
|
2542
3203
|
connectorVersion,
|
|
2543
|
-
runtime
|
|
3204
|
+
runtime,
|
|
2544
3205
|
challengeId: setup.challenge.id,
|
|
2545
3206
|
delegateAddress: localKey.address,
|
|
2546
3207
|
proofSignature,
|
|
@@ -2565,6 +3226,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2565
3226
|
const credentialPaths = await writeCredentials({
|
|
2566
3227
|
baseDir: options.credentialsDir,
|
|
2567
3228
|
agentId: registration.agent_id,
|
|
3229
|
+
serverName: options.serverName,
|
|
2568
3230
|
apiKey: localApiKey,
|
|
2569
3231
|
delegateKey: localKey.privateKey,
|
|
2570
3232
|
delegateAddress: localKey.address,
|
|
@@ -2592,7 +3254,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2592
3254
|
);
|
|
2593
3255
|
}
|
|
2594
3256
|
const runtimeInstall = await runRuntimeInstall({
|
|
2595
|
-
runtime
|
|
3257
|
+
runtime,
|
|
2596
3258
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
2597
3259
|
apiKey: localApiKey,
|
|
2598
3260
|
signerPath: credentialPaths.signerPath,
|
|
@@ -2601,7 +3263,8 @@ async function runConnect(options, deps = {}) {
|
|
|
2601
3263
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
2602
3264
|
ackSigner: options.ackSigner,
|
|
2603
3265
|
ackLocalTools: options.ackLocalTools,
|
|
2604
|
-
localMcp: options.localMcp
|
|
3266
|
+
localMcp: options.localMcp,
|
|
3267
|
+
serverName: options.serverName
|
|
2605
3268
|
}, {
|
|
2606
3269
|
onProgress: log,
|
|
2607
3270
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -2637,6 +3300,19 @@ async function runConnect(options, deps = {}) {
|
|
|
2637
3300
|
} else {
|
|
2638
3301
|
log("Haven setup on this machine is complete.");
|
|
2639
3302
|
}
|
|
3303
|
+
try {
|
|
3304
|
+
const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3305
|
+
if (supersededIds.length > 0) {
|
|
3306
|
+
log("");
|
|
3307
|
+
log(
|
|
3308
|
+
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3309
|
+
);
|
|
3310
|
+
log(
|
|
3311
|
+
`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.`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
} catch {
|
|
3315
|
+
}
|
|
2640
3316
|
try {
|
|
2641
3317
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2642
3318
|
runtime: runtimeInstall.runtime,
|
|
@@ -2711,11 +3387,16 @@ function completionOutcome(input) {
|
|
|
2711
3387
|
};
|
|
2712
3388
|
return outcome;
|
|
2713
3389
|
}
|
|
3390
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
3391
|
+
if (options.interactive !== true) return void 0;
|
|
3392
|
+
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
3393
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3394
|
+
}
|
|
2714
3395
|
function failedConnectOutcome(runtimeHint, error) {
|
|
2715
3396
|
const message = error instanceof Error ? error.message : "";
|
|
2716
|
-
const 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";
|
|
3397
|
+
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";
|
|
2717
3398
|
const runtime = normalizeRuntime(runtimeHint);
|
|
2718
|
-
const nextAction2 = 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";
|
|
3399
|
+
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";
|
|
2719
3400
|
return {
|
|
2720
3401
|
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2721
3402
|
outcome: "failed",
|
|
@@ -2801,7 +3482,7 @@ function describeApprovedBudget(budget) {
|
|
|
2801
3482
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2802
3483
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
2803
3484
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2804
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
3485
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
2805
3486
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2806
3487
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2807
3488
|
let waitingAnnounced = false;
|
|
@@ -2887,11 +3568,39 @@ function activationInstructionWithWhy(profile) {
|
|
|
2887
3568
|
}
|
|
2888
3569
|
return profile.activationInstruction;
|
|
2889
3570
|
}
|
|
3571
|
+
var RERUN_HINT = "npx @haven_ai/connect@alpha";
|
|
3572
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
3573
|
+
const root = defaultCredentialRoot(baseDir);
|
|
3574
|
+
let entries = [];
|
|
3575
|
+
try {
|
|
3576
|
+
entries = await promises.readdir(root);
|
|
3577
|
+
} catch {
|
|
3578
|
+
return [];
|
|
3579
|
+
}
|
|
3580
|
+
const ids = [];
|
|
3581
|
+
for (const entry of entries) {
|
|
3582
|
+
if (path.join(root, entry) === currentDirectory) continue;
|
|
3583
|
+
const identityPath = path.join(root, entry, "identity.json");
|
|
3584
|
+
try {
|
|
3585
|
+
await promises.stat(identityPath);
|
|
3586
|
+
} catch {
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
try {
|
|
3590
|
+
const identity = JSON.parse(await promises.readFile(identityPath, "utf8"));
|
|
3591
|
+
ids.push(identity.agent_id ?? entry);
|
|
3592
|
+
} catch {
|
|
3593
|
+
ids.push(entry);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
return ids;
|
|
3597
|
+
}
|
|
2890
3598
|
function printNextSteps(result, log, approval) {
|
|
2891
3599
|
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2892
3600
|
}
|
|
2893
3601
|
|
|
2894
3602
|
// src/args.ts
|
|
3603
|
+
init_server_names();
|
|
2895
3604
|
function parseArgs(argv, env = process.env) {
|
|
2896
3605
|
const options = {
|
|
2897
3606
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -2901,6 +3610,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
2901
3610
|
let json = false;
|
|
2902
3611
|
let doctor = false;
|
|
2903
3612
|
let repair = false;
|
|
3613
|
+
let tombstoneDir;
|
|
3614
|
+
let tombstoneReason;
|
|
3615
|
+
let tombstoneReplacedBy;
|
|
2904
3616
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2905
3617
|
const arg = argv[i];
|
|
2906
3618
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -2911,14 +3623,25 @@ function parseArgs(argv, env = process.env) {
|
|
|
2911
3623
|
doctor = true;
|
|
2912
3624
|
} else if (arg === "--repair") {
|
|
2913
3625
|
repair = true;
|
|
3626
|
+
} else if (arg === "--tombstone") {
|
|
3627
|
+
tombstoneDir = requireValue(argv, ++i, arg);
|
|
3628
|
+
} else if (arg === "--reason") {
|
|
3629
|
+
tombstoneReason = requireValue(argv, ++i, arg);
|
|
3630
|
+
} else if (arg === "--replaced-by") {
|
|
3631
|
+
tombstoneReplacedBy = requireValue(argv, ++i, arg);
|
|
2914
3632
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2915
3633
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2916
3634
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
2917
3635
|
options.apiBaseUrl = requireValue(argv, ++i, arg);
|
|
2918
3636
|
} else if (arg === "--runtime") {
|
|
2919
3637
|
options.runtime = requireValue(argv, ++i, arg);
|
|
3638
|
+
} else if (arg === "--runtime-force") {
|
|
3639
|
+
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
2920
3640
|
} else if (arg === "--credentials-dir") {
|
|
2921
3641
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
3642
|
+
} else if (arg === "--name") {
|
|
3643
|
+
options.serverName = requireValue(argv, ++i, arg);
|
|
3644
|
+
assertValidServerSlug(options.serverName);
|
|
2922
3645
|
} else if (arg === "--environment-label") {
|
|
2923
3646
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
2924
3647
|
} else if (arg === "--ack-local-tools") {
|
|
@@ -2936,14 +3659,21 @@ function parseArgs(argv, env = process.env) {
|
|
|
2936
3659
|
throw new Error(`Unknown option: ${arg}`);
|
|
2937
3660
|
}
|
|
2938
3661
|
}
|
|
3662
|
+
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
2939
3663
|
if (help) {
|
|
2940
|
-
return { options, help, json, doctor, repair };
|
|
3664
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
3665
|
+
}
|
|
3666
|
+
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
3667
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
3668
|
+
}
|
|
3669
|
+
if (tombstone) {
|
|
3670
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2941
3671
|
}
|
|
2942
3672
|
if (doctor || repair) {
|
|
2943
3673
|
if (!options.runtime) {
|
|
2944
3674
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2945
3675
|
}
|
|
2946
|
-
return { options, help, json, doctor, repair };
|
|
3676
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2947
3677
|
}
|
|
2948
3678
|
if (!options.setupToken) {
|
|
2949
3679
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2952,7 +3682,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2952
3682
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2953
3683
|
}
|
|
2954
3684
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2955
|
-
return { options, help, json, doctor, repair };
|
|
3685
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2956
3686
|
}
|
|
2957
3687
|
function helpText() {
|
|
2958
3688
|
return [
|
|
@@ -2962,14 +3692,23 @@ function helpText() {
|
|
|
2962
3692
|
"sends Haven only the public signing address plus a proof signature.",
|
|
2963
3693
|
"",
|
|
2964
3694
|
"Usage:",
|
|
2965
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools
|
|
3695
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
|
|
2966
3696
|
"",
|
|
2967
3697
|
"Options:",
|
|
2968
3698
|
" --setup <token> Short-lived setup token from Haven.",
|
|
2969
3699
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
2970
3700
|
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
|
|
3701
|
+
" Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
|
|
3702
|
+
" that contradicts this hint wins (with a printed notice). When nothing is detected, an",
|
|
3703
|
+
" interactive terminal is offered the agent clients installed on this machine; this flag is",
|
|
3704
|
+
" how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
|
|
3705
|
+
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
2971
3706
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
2972
3707
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
3708
|
+
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
3709
|
+
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
3710
|
+
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
3711
|
+
" single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
|
|
2973
3712
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
2974
3713
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2975
3714
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -2980,6 +3719,11 @@ function helpText() {
|
|
|
2980
3719
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2981
3720
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2982
3721
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
3722
|
+
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
3723
|
+
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
3724
|
+
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
3725
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone).",
|
|
3726
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
|
|
2983
3727
|
" --help Show this help.",
|
|
2984
3728
|
"",
|
|
2985
3729
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2994,6 +3738,7 @@ function requireValue(argv, index, option) {
|
|
|
2994
3738
|
}
|
|
2995
3739
|
|
|
2996
3740
|
// src/cli.ts
|
|
3741
|
+
init_redact();
|
|
2997
3742
|
async function runCli(argv, io = {
|
|
2998
3743
|
stdout: (message) => process.stdout.write(message),
|
|
2999
3744
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -3017,6 +3762,45 @@ async function runCli(argv, io = {
|
|
|
3017
3762
|
`);
|
|
3018
3763
|
return 0;
|
|
3019
3764
|
}
|
|
3765
|
+
if (parsed.tombstone) {
|
|
3766
|
+
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
3767
|
+
const { readFile: readFile11 } = await import('fs/promises');
|
|
3768
|
+
const { join: join10 } = await import('path');
|
|
3769
|
+
try {
|
|
3770
|
+
let agentId = "unknown";
|
|
3771
|
+
try {
|
|
3772
|
+
const identity = JSON.parse(
|
|
3773
|
+
await readFile11(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
3774
|
+
);
|
|
3775
|
+
agentId = identity.agent_id ?? "unknown";
|
|
3776
|
+
} catch {
|
|
3777
|
+
}
|
|
3778
|
+
const info = await writeAgentTombstone2({
|
|
3779
|
+
directory: parsed.tombstone.directory,
|
|
3780
|
+
agentId,
|
|
3781
|
+
reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
|
|
3782
|
+
replacedBy: parsed.tombstone.replacedBy
|
|
3783
|
+
});
|
|
3784
|
+
if (parsed.json) {
|
|
3785
|
+
io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
|
|
3786
|
+
`);
|
|
3787
|
+
} else {
|
|
3788
|
+
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
3789
|
+
`));
|
|
3790
|
+
io.stdout(
|
|
3791
|
+
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
3792
|
+
);
|
|
3793
|
+
io.stdout(
|
|
3794
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path.\n"
|
|
3795
|
+
);
|
|
3796
|
+
}
|
|
3797
|
+
return 0;
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3800
|
+
`);
|
|
3801
|
+
return 1;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3020
3804
|
if (parsed.doctor || parsed.repair) {
|
|
3021
3805
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3022
3806
|
const runtime = parsed.options.runtime ?? "";
|
|
@@ -3039,6 +3823,23 @@ async function runCli(argv, io = {
|
|
|
3039
3823
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3040
3824
|
`));
|
|
3041
3825
|
}
|
|
3826
|
+
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
3827
|
+
if (otherAgents.length > 0) {
|
|
3828
|
+
io.stdout("\nOther agents on this machine:\n");
|
|
3829
|
+
for (const agent of otherAgents) {
|
|
3830
|
+
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
3831
|
+
const failed = agent.checks.filter((check) => !check.ok);
|
|
3832
|
+
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
|
|
3833
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
3834
|
+
`));
|
|
3835
|
+
for (const check of failed) {
|
|
3836
|
+
io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
|
|
3837
|
+
`));
|
|
3838
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3839
|
+
`));
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3042
3843
|
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3043
3844
|
}
|
|
3044
3845
|
return report.ok ? 0 : 1;
|
|
@@ -3050,7 +3851,15 @@ async function runCli(argv, io = {
|
|
|
3050
3851
|
}
|
|
3051
3852
|
try {
|
|
3052
3853
|
const result = await runConnect(
|
|
3053
|
-
{
|
|
3854
|
+
{
|
|
3855
|
+
...parsed.options,
|
|
3856
|
+
waitForApproval: !parsed.json,
|
|
3857
|
+
// #1719: only a human-facing run may be asked which installed client to
|
|
3858
|
+
// configure. --json is the automation contract — it must fail with a
|
|
3859
|
+
// machine-readable code, never block on stdin. runConnect additionally
|
|
3860
|
+
// requires a real TTY before it prompts.
|
|
3861
|
+
interactive: !parsed.json
|
|
3862
|
+
},
|
|
3054
3863
|
{
|
|
3055
3864
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
3056
3865
|
`),
|