@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.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
3
3
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
4
4
|
import { isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION } from '@haven_ai/sdk';
|
|
5
|
-
import { mkdir, rm,
|
|
5
|
+
import { mkdir, rm, stat, readdir, readFile, chmod, access, writeFile, unlink } from 'fs/promises';
|
|
6
6
|
import { homedir, platform } from 'os';
|
|
7
|
-
import { join, resolve, dirname } from 'path';
|
|
7
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
8
8
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
9
9
|
import { execFile, spawn } from 'child_process';
|
|
10
10
|
import { promisify } from 'util';
|
|
@@ -12,6 +12,7 @@ import { realpathSync } from 'fs';
|
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13
13
|
import crypto from 'crypto';
|
|
14
14
|
import { Wallet } from 'ethers';
|
|
15
|
+
import { createInterface } from 'readline';
|
|
15
16
|
|
|
16
17
|
var __defProp = Object.defineProperty;
|
|
17
18
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -22,6 +23,69 @@ var __export = (target, all) => {
|
|
|
22
23
|
for (var name in all)
|
|
23
24
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
25
|
};
|
|
26
|
+
|
|
27
|
+
// src/redact.ts
|
|
28
|
+
function redactSecrets(value) {
|
|
29
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
30
|
+
}
|
|
31
|
+
function shortAddress(address) {
|
|
32
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
33
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
34
|
+
}
|
|
35
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
36
|
+
var init_redact = __esm({
|
|
37
|
+
"src/redact.ts"() {
|
|
38
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
39
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/server-names.ts
|
|
44
|
+
function assertValidServerSlug(slug) {
|
|
45
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function serverNamesFor(slug) {
|
|
62
|
+
if (slug === void 0) {
|
|
63
|
+
return {
|
|
64
|
+
hosted: "haven",
|
|
65
|
+
signer: "haven-signer",
|
|
66
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
67
|
+
codexHosted: "haven",
|
|
68
|
+
codexSigner: "haven_signer",
|
|
69
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
assertValidServerSlug(slug);
|
|
73
|
+
return {
|
|
74
|
+
hosted: `haven-${slug}`,
|
|
75
|
+
signer: `haven-signer-${slug}`,
|
|
76
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
77
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
78
|
+
codexHosted: `haven-${slug}`,
|
|
79
|
+
codexSigner: `haven-signer-${slug}`,
|
|
80
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var SLUG_RE;
|
|
84
|
+
var init_server_names = __esm({
|
|
85
|
+
"src/server-names.ts"() {
|
|
86
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
25
89
|
function mcpPackageSpec() {
|
|
26
90
|
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
27
91
|
}
|
|
@@ -38,9 +102,9 @@ var init_runtime_manifest = __esm({
|
|
|
38
102
|
mcpPackage: "@haven_ai/mcp",
|
|
39
103
|
mcpVersion: MCP_VERSION,
|
|
40
104
|
sdkPackage: "@haven_ai/sdk",
|
|
41
|
-
sdkVersion: "0.1.
|
|
105
|
+
sdkVersion: "0.1.29-alpha.0",
|
|
42
106
|
signerPackage: "@haven_ai/signer",
|
|
43
|
-
signerVersion: "0.1.
|
|
107
|
+
signerVersion: "0.1.29-alpha.0",
|
|
44
108
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
45
109
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
46
110
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -116,31 +180,33 @@ function buildSignerServer(spec, runtime) {
|
|
|
116
180
|
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
117
181
|
return server;
|
|
118
182
|
}
|
|
119
|
-
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
|
|
120
|
-
const config = existingJson?.trim() ? parseJsonObject(existingJson) : {};
|
|
183
|
+
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
184
|
+
const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
|
|
121
185
|
const existingRoot = config[serverRoot];
|
|
122
186
|
const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
|
|
123
187
|
config[serverRoot] = {
|
|
124
188
|
...servers,
|
|
125
|
-
|
|
126
|
-
|
|
189
|
+
[names.hosted]: hostedServer,
|
|
190
|
+
[names.signer]: signerServer
|
|
127
191
|
};
|
|
128
192
|
return `${JSON.stringify(config, null, 2)}
|
|
129
193
|
`;
|
|
130
194
|
}
|
|
131
|
-
function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
132
|
-
if (!existingYaml?.trim())
|
|
195
|
+
function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
|
|
196
|
+
if (!existingYaml?.trim()) {
|
|
197
|
+
return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
|
|
198
|
+
}
|
|
133
199
|
const doc = parseDocument(existingYaml, { keepSourceTokens: true });
|
|
134
200
|
if (doc.errors.length > 0 || !isMap(doc.contents)) {
|
|
135
|
-
throw new
|
|
201
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
|
|
136
202
|
}
|
|
137
203
|
const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
|
|
138
204
|
const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
|
|
139
205
|
const servers = isRecord(existingServers) ? existingServers : {};
|
|
140
206
|
const mergedServers = {
|
|
141
207
|
...servers,
|
|
142
|
-
|
|
143
|
-
|
|
208
|
+
[names.hosted]: hostedServer,
|
|
209
|
+
[names.signer]: signerServer
|
|
144
210
|
};
|
|
145
211
|
if (!mcpPair) {
|
|
146
212
|
return appendHermesMcpServers(existingYaml, mergedServers);
|
|
@@ -148,9 +214,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
|
|
|
148
214
|
if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
|
|
149
215
|
return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
|
|
150
216
|
}
|
|
151
|
-
function mergeHermesEnv(existingEnv, apiKey) {
|
|
217
|
+
function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
|
|
152
218
|
if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
|
|
153
|
-
const assignment = `${
|
|
219
|
+
const assignment = `${envKey}=${apiKey}`;
|
|
154
220
|
if (!existingEnv) return `${assignment}
|
|
155
221
|
`;
|
|
156
222
|
const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -159,12 +225,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
159
225
|
if (hasTrailingNewline) lines.pop();
|
|
160
226
|
let found = false;
|
|
161
227
|
const merged = lines.flatMap((line) => {
|
|
162
|
-
if (isHermesEnvAssignment(line)) {
|
|
228
|
+
if (isHermesEnvAssignment(line, envKey)) {
|
|
163
229
|
if (found) return [];
|
|
164
230
|
found = true;
|
|
165
231
|
return [assignment];
|
|
166
232
|
}
|
|
167
|
-
if (isAmbiguousHermesEnvLine(line)) {
|
|
233
|
+
if (isAmbiguousHermesEnvLine(line, envKey)) {
|
|
168
234
|
throw new Error("Hermes environment contains an ambiguous managed key");
|
|
169
235
|
}
|
|
170
236
|
return [line];
|
|
@@ -174,11 +240,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
|
|
|
174
240
|
}
|
|
175
241
|
return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
|
|
176
242
|
}
|
|
177
|
-
function isHermesEnvAssignment(line) {
|
|
178
|
-
return
|
|
243
|
+
function isHermesEnvAssignment(line, envKey) {
|
|
244
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
|
|
179
245
|
}
|
|
180
|
-
function isAmbiguousHermesEnvLine(line) {
|
|
181
|
-
return
|
|
246
|
+
function isAmbiguousHermesEnvLine(line, envKey) {
|
|
247
|
+
return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
|
|
182
248
|
}
|
|
183
249
|
function appendHermesMcpServers(source, servers) {
|
|
184
250
|
const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
|
|
@@ -237,11 +303,14 @@ function renderHermesMcpServerEntries(servers, indent) {
|
|
|
237
303
|
function isRecord(value) {
|
|
238
304
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
239
305
|
}
|
|
240
|
-
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
241
|
-
let next = removeTomlTableTree(
|
|
306
|
+
function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
|
|
307
|
+
let next = removeTomlTableTree(
|
|
308
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
309
|
+
`mcp_servers.${names.codexSigner}`
|
|
310
|
+
);
|
|
242
311
|
next = next.trimEnd();
|
|
243
312
|
const block = [
|
|
244
|
-
|
|
313
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
245
314
|
`command = ${tomlString(localMcpCommand)}`,
|
|
246
315
|
"args = []",
|
|
247
316
|
"startup_timeout_sec = 120"
|
|
@@ -253,15 +322,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
|
|
|
253
322
|
`;
|
|
254
323
|
return merged;
|
|
255
324
|
}
|
|
256
|
-
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
|
|
257
|
-
let next = removeTomlTableTree(
|
|
325
|
+
function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
|
|
326
|
+
let next = removeTomlTableTree(
|
|
327
|
+
removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
|
|
328
|
+
`mcp_servers.${names.codexSigner}`
|
|
329
|
+
);
|
|
258
330
|
next = next.trimEnd();
|
|
259
331
|
const block = [
|
|
260
|
-
|
|
332
|
+
`[mcp_servers.${names.codexHosted}]`,
|
|
261
333
|
`url = ${tomlString(hostedMcpUrl)}`,
|
|
262
334
|
`http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
|
|
263
335
|
"",
|
|
264
|
-
|
|
336
|
+
`[mcp_servers.${names.codexSigner}]`,
|
|
265
337
|
`command = ${tomlString(signerSpec.command)}`,
|
|
266
338
|
`args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
|
|
267
339
|
"startup_timeout_sec = 120"
|
|
@@ -280,7 +352,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
280
352
|
existing,
|
|
281
353
|
serverRoot,
|
|
282
354
|
buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
|
|
283
|
-
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
|
|
355
|
+
buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
|
|
356
|
+
serverNamesFor(input.serverName),
|
|
357
|
+
target
|
|
284
358
|
);
|
|
285
359
|
await writeOwnerOnlyText(target, merged);
|
|
286
360
|
return {
|
|
@@ -294,6 +368,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
294
368
|
messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
|
|
295
369
|
};
|
|
296
370
|
} catch (err) {
|
|
371
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
297
372
|
return {
|
|
298
373
|
hostedConfigured: false,
|
|
299
374
|
signerConfigured: false,
|
|
@@ -302,8 +377,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
302
377
|
target: configTargetLabel(input.runtime),
|
|
303
378
|
changed: false,
|
|
304
379
|
restartRequired: true,
|
|
305
|
-
messages:
|
|
306
|
-
|
|
380
|
+
messages: unreadable ? [
|
|
381
|
+
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
382
|
+
`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.`
|
|
383
|
+
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
384
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
307
385
|
};
|
|
308
386
|
}
|
|
309
387
|
}
|
|
@@ -312,8 +390,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
312
390
|
const envTarget = hermesEnvPath(input.homeDir);
|
|
313
391
|
try {
|
|
314
392
|
const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
|
|
393
|
+
const names = serverNamesFor(input.serverName);
|
|
315
394
|
const hostedServer = {
|
|
316
|
-
...buildHostedServer(input.hostedMcpUrl, `\${${
|
|
395
|
+
...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
|
|
317
396
|
enabled: true
|
|
318
397
|
};
|
|
319
398
|
const signerServer = {
|
|
@@ -323,9 +402,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
323
402
|
const merged = mergeHermesYaml(
|
|
324
403
|
existing,
|
|
325
404
|
hostedServer,
|
|
326
|
-
signerServer
|
|
405
|
+
signerServer,
|
|
406
|
+
names,
|
|
407
|
+
target
|
|
327
408
|
);
|
|
328
|
-
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
|
|
409
|
+
const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
|
|
329
410
|
const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
|
|
330
411
|
await writeText(envTarget, mergedEnv);
|
|
331
412
|
try {
|
|
@@ -346,11 +427,13 @@ async function writeHermesConfig(input, deps) {
|
|
|
346
427
|
messages: [
|
|
347
428
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
348
429
|
"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`.",
|
|
430
|
+
"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.",
|
|
349
431
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
350
432
|
]
|
|
351
433
|
};
|
|
352
434
|
} catch (err) {
|
|
353
435
|
const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
|
|
436
|
+
const unreadable = err instanceof UnreadableRuntimeConfigError;
|
|
354
437
|
return {
|
|
355
438
|
hostedConfigured: false,
|
|
356
439
|
signerConfigured: false,
|
|
@@ -359,8 +442,11 @@ async function writeHermesConfig(input, deps) {
|
|
|
359
442
|
target: "Hermes Agent config",
|
|
360
443
|
changed: false,
|
|
361
444
|
restartRequired: true,
|
|
362
|
-
messages:
|
|
363
|
-
|
|
445
|
+
messages: unreadable ? [
|
|
446
|
+
`Could not update Hermes Agent config: ${err.message}.`,
|
|
447
|
+
`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.`
|
|
448
|
+
] : [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."],
|
|
449
|
+
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
364
450
|
};
|
|
365
451
|
}
|
|
366
452
|
}
|
|
@@ -392,7 +478,7 @@ async function writeCodexConfig(input) {
|
|
|
392
478
|
if (!input.localMcpCommand) {
|
|
393
479
|
throw new Error("local MCP wrapper command is required");
|
|
394
480
|
}
|
|
395
|
-
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
481
|
+
const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
|
|
396
482
|
await writeOwnerOnlyText(target, merged2);
|
|
397
483
|
return {
|
|
398
484
|
hostedConfigured: false,
|
|
@@ -407,7 +493,13 @@ async function writeCodexConfig(input) {
|
|
|
407
493
|
]
|
|
408
494
|
};
|
|
409
495
|
}
|
|
410
|
-
const merged = mergeCodexTomlHosted(
|
|
496
|
+
const merged = mergeCodexTomlHosted(
|
|
497
|
+
existing ?? "",
|
|
498
|
+
input.hostedMcpUrl,
|
|
499
|
+
input.apiKey,
|
|
500
|
+
resolveSignerLaunchSpec(input),
|
|
501
|
+
serverNamesFor(input.serverName)
|
|
502
|
+
);
|
|
411
503
|
await writeOwnerOnlyText(target, merged);
|
|
412
504
|
return {
|
|
413
505
|
hostedConfigured: true,
|
|
@@ -449,10 +541,15 @@ async function writeOwnerOnlyText(path, value) {
|
|
|
449
541
|
await writeFile(path, value, { mode: 384 });
|
|
450
542
|
await chmod(path, 384).catch(() => void 0);
|
|
451
543
|
}
|
|
452
|
-
function parseJsonObject(value) {
|
|
453
|
-
|
|
544
|
+
function parseJsonObject(value, configPath) {
|
|
545
|
+
let parsed;
|
|
546
|
+
try {
|
|
547
|
+
parsed = JSON.parse(value);
|
|
548
|
+
} catch {
|
|
549
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
|
|
550
|
+
}
|
|
454
551
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
455
|
-
throw new
|
|
552
|
+
throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
|
|
456
553
|
}
|
|
457
554
|
return parsed;
|
|
458
555
|
}
|
|
@@ -719,10 +816,11 @@ function configTargetLabel(runtime) {
|
|
|
719
816
|
return "runtime MCP config";
|
|
720
817
|
}
|
|
721
818
|
}
|
|
722
|
-
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
|
|
819
|
+
var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
|
|
723
820
|
var init_config_writers = __esm({
|
|
724
821
|
"src/config-writers.ts"() {
|
|
725
822
|
init_runtime_manifest();
|
|
823
|
+
init_server_names();
|
|
726
824
|
HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
|
|
727
825
|
HermesConfigRecoveryError = class extends Error {
|
|
728
826
|
constructor() {
|
|
@@ -736,8 +834,37 @@ var init_config_writers = __esm({
|
|
|
736
834
|
this.name = "InvalidCodexTomlError";
|
|
737
835
|
}
|
|
738
836
|
};
|
|
837
|
+
REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
|
|
838
|
+
UnreadableRuntimeConfigError = class extends Error {
|
|
839
|
+
configPath;
|
|
840
|
+
constructor(configPath, detail) {
|
|
841
|
+
super(`${configPath} is not a config Haven can merge into (${detail})`);
|
|
842
|
+
this.name = "UnreadableRuntimeConfigError";
|
|
843
|
+
this.configPath = configPath;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
739
846
|
}
|
|
740
847
|
});
|
|
848
|
+
async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
|
|
849
|
+
let response;
|
|
850
|
+
try {
|
|
851
|
+
response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
|
|
852
|
+
method: "GET",
|
|
853
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
|
|
854
|
+
});
|
|
855
|
+
} catch {
|
|
856
|
+
return { status: "network_error" };
|
|
857
|
+
}
|
|
858
|
+
if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
|
|
859
|
+
if (!response.ok) return { status: "bad_response" };
|
|
860
|
+
try {
|
|
861
|
+
const payload = JSON.parse(await response.text());
|
|
862
|
+
if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
|
|
863
|
+
return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
|
|
864
|
+
} catch {
|
|
865
|
+
return { status: "bad_response" };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
741
868
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
742
869
|
let response;
|
|
743
870
|
try {
|
|
@@ -775,7 +902,7 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
775
902
|
}
|
|
776
903
|
}
|
|
777
904
|
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
778
|
-
return new Promise((
|
|
905
|
+
return new Promise((resolve9) => {
|
|
779
906
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
780
907
|
let stdout = "";
|
|
781
908
|
let settled = false;
|
|
@@ -787,7 +914,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
|
|
|
787
914
|
settled = true;
|
|
788
915
|
clearTimeout(timeout);
|
|
789
916
|
child.kill();
|
|
790
|
-
|
|
917
|
+
resolve9(result);
|
|
791
918
|
};
|
|
792
919
|
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
793
920
|
child.on("error", () => finish({ status: "process_error" }));
|
|
@@ -901,7 +1028,8 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
901
1028
|
wrapperPath,
|
|
902
1029
|
runtimeDirectory,
|
|
903
1030
|
npmCacheDirectory,
|
|
904
|
-
cliPath
|
|
1031
|
+
cliPath,
|
|
1032
|
+
serverName: input.serverName
|
|
905
1033
|
});
|
|
906
1034
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
907
1035
|
return {
|
|
@@ -1002,6 +1130,7 @@ async function readRuntimeSidecar(credentialDirectory) {
|
|
|
1002
1130
|
}
|
|
1003
1131
|
async function writeRuntimeSidecar(input) {
|
|
1004
1132
|
const value = {
|
|
1133
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1005
1134
|
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1006
1135
|
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1007
1136
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -1032,6 +1161,23 @@ var init_signer_runtime = __esm({
|
|
|
1032
1161
|
}
|
|
1033
1162
|
});
|
|
1034
1163
|
|
|
1164
|
+
// src/connect-error.ts
|
|
1165
|
+
var ConnectError;
|
|
1166
|
+
var init_connect_error = __esm({
|
|
1167
|
+
"src/connect-error.ts"() {
|
|
1168
|
+
ConnectError = class extends Error {
|
|
1169
|
+
code;
|
|
1170
|
+
nextAction;
|
|
1171
|
+
constructor(code, message, nextAction2) {
|
|
1172
|
+
super(message);
|
|
1173
|
+
this.name = "ConnectError";
|
|
1174
|
+
this.code = code;
|
|
1175
|
+
this.nextAction = nextAction2;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1035
1181
|
// src/runtime-registry.ts
|
|
1036
1182
|
function runtimeProfile(runtime, env = process.env) {
|
|
1037
1183
|
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
@@ -1041,6 +1187,42 @@ function normalizeRuntime(runtime, env = process.env) {
|
|
|
1041
1187
|
if (explicit) return explicit;
|
|
1042
1188
|
return detectRuntime(env) ?? "other";
|
|
1043
1189
|
}
|
|
1190
|
+
async function resolveRuntimeSelection(explicit, force, options = {}) {
|
|
1191
|
+
const env = options.env ?? process.env;
|
|
1192
|
+
if (force !== void 0) {
|
|
1193
|
+
const forced = normalizeRuntimeName(force);
|
|
1194
|
+
if (!forced) {
|
|
1195
|
+
throw new ConnectError(
|
|
1196
|
+
"runtime_force_unrecognized",
|
|
1197
|
+
`Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
|
|
1198
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
return { runtime: forced, source: "force" };
|
|
1202
|
+
}
|
|
1203
|
+
const detected = detectRuntime(env);
|
|
1204
|
+
const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
|
|
1205
|
+
const hint = normalizeRuntimeName(supplied);
|
|
1206
|
+
if (supplied && !hint) {
|
|
1207
|
+
if (!detected) {
|
|
1208
|
+
throw new ConnectError(
|
|
1209
|
+
"runtime_unrecognized",
|
|
1210
|
+
`"${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.`,
|
|
1211
|
+
"rerun_connect_with_a_valid_runtime_name"
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
return { runtime: detected, source: "detected", discardedHint: supplied };
|
|
1215
|
+
}
|
|
1216
|
+
if (detected && hint && detected !== hint) {
|
|
1217
|
+
return { runtime: detected, source: "detected", overrodeHint: hint };
|
|
1218
|
+
}
|
|
1219
|
+
if (hint) return { runtime: hint, source: "explicit" };
|
|
1220
|
+
if (detected) return { runtime: detected, source: "detected" };
|
|
1221
|
+
if (options.promptForRuntime) {
|
|
1222
|
+
return { runtime: await options.promptForRuntime(), source: "prompted" };
|
|
1223
|
+
}
|
|
1224
|
+
return { runtime: null, source: "none" };
|
|
1225
|
+
}
|
|
1044
1226
|
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
1045
1227
|
const mode = runtimeProfile(runtime, env).restartMode;
|
|
1046
1228
|
return mode === "restart-session" || mode === "restart-app";
|
|
@@ -1061,9 +1243,10 @@ function detectRuntime(env) {
|
|
|
1061
1243
|
if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
|
|
1062
1244
|
return null;
|
|
1063
1245
|
}
|
|
1064
|
-
var RUNTIME_PROFILES, RUNTIME_ALIASES;
|
|
1246
|
+
var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
|
|
1065
1247
|
var init_runtime_registry = __esm({
|
|
1066
1248
|
"src/runtime-registry.ts"() {
|
|
1249
|
+
init_connect_error();
|
|
1067
1250
|
RUNTIME_PROFILES = {
|
|
1068
1251
|
"claude-code": {
|
|
1069
1252
|
id: "claude-code",
|
|
@@ -1134,6 +1317,11 @@ var init_runtime_registry = __esm({
|
|
|
1134
1317
|
"claude-code": "claude-code",
|
|
1135
1318
|
claudecode: "claude-code",
|
|
1136
1319
|
"claude_code": "claude-code",
|
|
1320
|
+
// #1682: Cowork runs Claude Code's config, so it resolves to the same
|
|
1321
|
+
// profile. The dashboard's command carries no --runtime for it (it is a
|
|
1322
|
+
// detected, command-path runtime), but an explicit `--runtime cowork` typed
|
|
1323
|
+
// by hand must not fall through to the no-runtime refusal.
|
|
1324
|
+
cowork: "claude-code",
|
|
1137
1325
|
codex: "codex-cli",
|
|
1138
1326
|
"codex-cli": "codex-cli",
|
|
1139
1327
|
codexcli: "codex-cli",
|
|
@@ -1163,9 +1351,18 @@ var init_runtime_registry = __esm({
|
|
|
1163
1351
|
"hermes-agent": "hermes",
|
|
1164
1352
|
hermes_agent: "hermes",
|
|
1165
1353
|
hermesagent: "hermes",
|
|
1354
|
+
// #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
|
|
1355
|
+
// into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
|
|
1356
|
+
// the 'other' profile's behaviour (credentials written to disk, no config
|
|
1357
|
+
// auto-written, manual finish), so it resolves there rather than earning a
|
|
1358
|
+
// profile whose only distinguishing feature would be its label.
|
|
1359
|
+
openclaw: "other",
|
|
1360
|
+
"open-claw": "other",
|
|
1361
|
+
"open_claw": "other",
|
|
1166
1362
|
other: "other",
|
|
1167
1363
|
manual: "other"
|
|
1168
1364
|
};
|
|
1365
|
+
RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
|
|
1169
1366
|
}
|
|
1170
1367
|
});
|
|
1171
1368
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
@@ -1242,6 +1439,82 @@ var init_signer_consent = __esm({
|
|
|
1242
1439
|
}
|
|
1243
1440
|
});
|
|
1244
1441
|
|
|
1442
|
+
// src/tombstone.ts
|
|
1443
|
+
var tombstone_exports = {};
|
|
1444
|
+
__export(tombstone_exports, {
|
|
1445
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
1446
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
1447
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
1448
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
1449
|
+
});
|
|
1450
|
+
function tombstoneScript(info) {
|
|
1451
|
+
const lines = [
|
|
1452
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
1453
|
+
"",
|
|
1454
|
+
` agent: ${info.agent_id}`,
|
|
1455
|
+
` retired at: ${info.retired_at}`,
|
|
1456
|
+
` reason: ${info.reason}`,
|
|
1457
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
1458
|
+
"",
|
|
1459
|
+
"This process is running with a wiring snapshot that predates the",
|
|
1460
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
1461
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
1462
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
1463
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
1464
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
1465
|
+
"",
|
|
1466
|
+
"Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
|
|
1467
|
+
];
|
|
1468
|
+
return [
|
|
1469
|
+
"#!/usr/bin/env node",
|
|
1470
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
1471
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
1472
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
1473
|
+
"process.exit(1)",
|
|
1474
|
+
""
|
|
1475
|
+
].join("\n");
|
|
1476
|
+
}
|
|
1477
|
+
async function writeAgentTombstone(input) {
|
|
1478
|
+
const dirStat = await stat(input.directory).catch(() => null);
|
|
1479
|
+
if (!dirStat?.isDirectory()) {
|
|
1480
|
+
throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
|
|
1481
|
+
}
|
|
1482
|
+
const info = {
|
|
1483
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
1484
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
1485
|
+
// like every other output path, at the write layer so any future caller
|
|
1486
|
+
// inherits it. (#1681 review, finding 1)
|
|
1487
|
+
agent_id: input.agentId,
|
|
1488
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1489
|
+
reason: redactSecrets(input.reason),
|
|
1490
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
1491
|
+
};
|
|
1492
|
+
const binDir = join(input.directory, "bin");
|
|
1493
|
+
await mkdir(binDir, { recursive: true });
|
|
1494
|
+
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
1495
|
+
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
1496
|
+
await chmod(wrapperPath, 493);
|
|
1497
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
|
|
1498
|
+
return info;
|
|
1499
|
+
}
|
|
1500
|
+
async function readAgentTombstone(directory) {
|
|
1501
|
+
try {
|
|
1502
|
+
const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
1503
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
1504
|
+
return parsed;
|
|
1505
|
+
} catch {
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
|
|
1510
|
+
var init_tombstone = __esm({
|
|
1511
|
+
"src/tombstone.ts"() {
|
|
1512
|
+
init_redact();
|
|
1513
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
1514
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1245
1518
|
// src/doctor.ts
|
|
1246
1519
|
var doctor_exports = {};
|
|
1247
1520
|
__export(doctor_exports, {
|
|
@@ -1249,129 +1522,101 @@ __export(doctor_exports, {
|
|
|
1249
1522
|
runRepair: () => runRepair
|
|
1250
1523
|
});
|
|
1251
1524
|
async function discoverCredentialDirectory(homeDir, explicit) {
|
|
1252
|
-
|
|
1253
|
-
const root = join(homeDir, ".haven", "agents");
|
|
1525
|
+
const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
|
|
1254
1526
|
let entries = [];
|
|
1255
1527
|
try {
|
|
1256
1528
|
entries = await readdir(root);
|
|
1257
1529
|
} catch {
|
|
1258
|
-
return {};
|
|
1530
|
+
return explicit ? { directory: explicit, others: [] } : { others: [] };
|
|
1259
1531
|
}
|
|
1260
1532
|
const candidates = [];
|
|
1533
|
+
const tombstonedOnly = [];
|
|
1261
1534
|
for (const entry of entries) {
|
|
1262
1535
|
const directory = join(root, entry);
|
|
1263
1536
|
try {
|
|
1264
1537
|
const s = await stat(join(directory, "identity.json"));
|
|
1265
1538
|
candidates.push({ directory, mtimeMs: s.mtimeMs });
|
|
1266
1539
|
} catch {
|
|
1540
|
+
try {
|
|
1541
|
+
await stat(join(directory, TOMBSTONE_FILENAME));
|
|
1542
|
+
tombstonedOnly.push(directory);
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1267
1545
|
}
|
|
1268
1546
|
}
|
|
1269
|
-
if (candidates.length === 0) return {};
|
|
1270
1547
|
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1548
|
+
if (explicit) {
|
|
1549
|
+
return {
|
|
1550
|
+
directory: explicit,
|
|
1551
|
+
others: [...candidates.map((c) => c.directory), ...tombstonedOnly].filter((d) => d !== explicit)
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
if (candidates.length === 0 && tombstonedOnly.length === 0) return { others: [] };
|
|
1271
1555
|
return {
|
|
1272
|
-
directory: candidates[0]
|
|
1273
|
-
|
|
1556
|
+
directory: candidates[0]?.directory,
|
|
1557
|
+
others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly]
|
|
1274
1558
|
};
|
|
1275
1559
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1560
|
+
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
1561
|
+
if (configText === null) return isPrimary;
|
|
1562
|
+
if (slug) {
|
|
1563
|
+
for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
|
|
1564
|
+
if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
|
|
1565
|
+
}
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
|
|
1569
|
+
if (bareOwnerExists) return false;
|
|
1570
|
+
return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
|
|
1571
|
+
}
|
|
1572
|
+
async function readIdentity(directory) {
|
|
1573
|
+
try {
|
|
1574
|
+
return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
1575
|
+
} catch {
|
|
1576
|
+
return void 0;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function checksForAgent(entry, input, deps) {
|
|
1580
|
+
const { directory, identity, sidecar } = entry;
|
|
1278
1581
|
const checks = [];
|
|
1279
1582
|
let signerCapabilities;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1583
|
+
let signerFile;
|
|
1584
|
+
try {
|
|
1585
|
+
const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
1586
|
+
signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
1587
|
+
} catch {
|
|
1588
|
+
signerFile = void 0;
|
|
1589
|
+
}
|
|
1590
|
+
const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
|
|
1591
|
+
checks.push({
|
|
1592
|
+
id: "credentials",
|
|
1593
|
+
label: "Agent credentials",
|
|
1594
|
+
ok: credentialsOk,
|
|
1595
|
+
detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
|
|
1596
|
+
...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
|
|
1597
|
+
});
|
|
1598
|
+
if (!sidecar) {
|
|
1284
1599
|
checks.push({
|
|
1285
|
-
id: "
|
|
1286
|
-
label: "
|
|
1600
|
+
id: "signer_runtime",
|
|
1601
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1287
1602
|
ok: false,
|
|
1288
|
-
detail: "No
|
|
1289
|
-
repair: `Run
|
|
1603
|
+
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1604
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1290
1605
|
});
|
|
1291
1606
|
} else {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
identity = void 0;
|
|
1296
|
-
}
|
|
1297
|
-
try {
|
|
1298
|
-
const signer = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
|
|
1299
|
-
signerParses = typeof signer === "object" && signer !== null;
|
|
1300
|
-
} catch {
|
|
1301
|
-
signerParses = false;
|
|
1302
|
-
}
|
|
1303
|
-
const ok = Boolean(identity?.api_key) && signerParses;
|
|
1607
|
+
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1608
|
+
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1609
|
+
const ok = matches && versionOk;
|
|
1304
1610
|
checks.push({
|
|
1305
|
-
id: "
|
|
1306
|
-
label: "
|
|
1611
|
+
id: "signer_runtime",
|
|
1612
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
1307
1613
|
ok,
|
|
1308
|
-
detail: ok ? `
|
|
1309
|
-
...ok ? {} : { repair: `
|
|
1614
|
+
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.`,
|
|
1615
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1310
1616
|
});
|
|
1311
1617
|
}
|
|
1312
|
-
|
|
1313
|
-
if (
|
|
1314
|
-
sidecar = await readRuntimeSidecar(directory);
|
|
1315
|
-
if (!sidecar) {
|
|
1316
|
-
checks.push({
|
|
1317
|
-
id: "signer_runtime",
|
|
1318
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1319
|
-
ok: false,
|
|
1320
|
-
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
1321
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1322
|
-
});
|
|
1323
|
-
} else {
|
|
1324
|
-
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
1325
|
-
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
1326
|
-
const ok = matches && versionOk;
|
|
1327
|
-
checks.push({
|
|
1328
|
-
id: "signer_runtime",
|
|
1329
|
-
label: "Signer runtime (preinstalled wrapper)",
|
|
1330
|
-
ok,
|
|
1331
|
-
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.`,
|
|
1332
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1337
|
-
if (configPath === null) {
|
|
1338
|
-
checks.push({
|
|
1339
|
-
id: "runtime_config",
|
|
1340
|
-
label: "Runtime MCP config",
|
|
1341
|
-
ok: true,
|
|
1342
|
-
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1343
|
-
});
|
|
1344
|
-
} else {
|
|
1345
|
-
let configText = null;
|
|
1346
|
-
try {
|
|
1347
|
-
configText = await readFile(configPath, "utf8");
|
|
1348
|
-
} catch {
|
|
1349
|
-
configText = null;
|
|
1350
|
-
}
|
|
1351
|
-
if (configText === null) {
|
|
1352
|
-
checks.push({
|
|
1353
|
-
id: "runtime_config",
|
|
1354
|
-
label: "Runtime MCP config",
|
|
1355
|
-
ok: false,
|
|
1356
|
-
detail: `No runtime config at ${configPath}.`,
|
|
1357
|
-
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1358
|
-
});
|
|
1359
|
-
} else {
|
|
1360
|
-
const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
|
|
1361
|
-
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1362
|
-
const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
|
|
1363
|
-
const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
|
|
1364
|
-
checks.push({
|
|
1365
|
-
id: "runtime_config",
|
|
1366
|
-
label: "Runtime MCP config",
|
|
1367
|
-
ok,
|
|
1368
|
-
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)" : ""}.`,
|
|
1369
|
-
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
|
|
1374
|
-
const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
|
|
1618
|
+
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1619
|
+
if (identity?.api_key && hostedUrl) {
|
|
1375
1620
|
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
1376
1621
|
checks.push({
|
|
1377
1622
|
id: "hosted_mcp",
|
|
@@ -1391,7 +1636,43 @@ async function runDoctor(input, deps = {}) {
|
|
|
1391
1636
|
repair: `Re-run the full setup: ${RERUN} --setup <token>.`
|
|
1392
1637
|
});
|
|
1393
1638
|
}
|
|
1394
|
-
|
|
1639
|
+
const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
|
|
1640
|
+
if (identity?.api_key && identity.api_url) {
|
|
1641
|
+
const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
|
|
1642
|
+
identity.api_key,
|
|
1643
|
+
identity.api_url,
|
|
1644
|
+
deps.fetch
|
|
1645
|
+
);
|
|
1646
|
+
if (probe.status !== "ok") {
|
|
1647
|
+
checks.push({
|
|
1648
|
+
id: "identity_match",
|
|
1649
|
+
label: "Hosted identity matches the local signing key",
|
|
1650
|
+
ok: false,
|
|
1651
|
+
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.`,
|
|
1652
|
+
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}`
|
|
1653
|
+
});
|
|
1654
|
+
} else if (!localDelegate) {
|
|
1655
|
+
checks.push({
|
|
1656
|
+
id: "identity_match",
|
|
1657
|
+
label: "Hosted identity matches the local signing key",
|
|
1658
|
+
ok: false,
|
|
1659
|
+
detail: "signer.json holds no delegate_address to compare against the hosted identity.",
|
|
1660
|
+
repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
|
|
1661
|
+
});
|
|
1662
|
+
} else {
|
|
1663
|
+
const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
|
|
1664
|
+
checks.push({
|
|
1665
|
+
id: "identity_match",
|
|
1666
|
+
label: "Hosted identity matches the local signing key",
|
|
1667
|
+
ok: same,
|
|
1668
|
+
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.`,
|
|
1669
|
+
...same ? {} : {
|
|
1670
|
+
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.`
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (sidecar) {
|
|
1395
1676
|
const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
|
|
1396
1677
|
if (!consent.acknowledged) {
|
|
1397
1678
|
checks.push({
|
|
@@ -1419,7 +1700,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
1419
1700
|
...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1420
1701
|
});
|
|
1421
1702
|
}
|
|
1422
|
-
} else
|
|
1703
|
+
} else {
|
|
1423
1704
|
checks.push({
|
|
1424
1705
|
id: "signer_process",
|
|
1425
1706
|
label: "Signer stdio handshake",
|
|
@@ -1428,6 +1709,171 @@ async function runDoctor(input, deps = {}) {
|
|
|
1428
1709
|
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1429
1710
|
});
|
|
1430
1711
|
}
|
|
1712
|
+
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
1713
|
+
}
|
|
1714
|
+
async function runDoctor(input, deps = {}) {
|
|
1715
|
+
const homeDir = deps.homeDir ?? homedir();
|
|
1716
|
+
const checks = [];
|
|
1717
|
+
let signerCapabilities;
|
|
1718
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1719
|
+
const configPath = runtimeConfigPathFor(input.runtime, homeDir);
|
|
1720
|
+
let configText = null;
|
|
1721
|
+
if (configPath !== null) {
|
|
1722
|
+
try {
|
|
1723
|
+
configText = await readFile(configPath, "utf8");
|
|
1724
|
+
} catch {
|
|
1725
|
+
configText = null;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
const allDirectories = directory ? [directory, ...others] : others;
|
|
1729
|
+
let bareOwnerExists = false;
|
|
1730
|
+
for (const dir of allDirectories) {
|
|
1731
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1732
|
+
if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
|
|
1733
|
+
bareOwnerExists = true;
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const inventory = [];
|
|
1738
|
+
const capabilitiesByDirectory = /* @__PURE__ */ new Map();
|
|
1739
|
+
const primaryChecksById = /* @__PURE__ */ new Map();
|
|
1740
|
+
for (const dir of allDirectories) {
|
|
1741
|
+
const identity = await readIdentity(dir);
|
|
1742
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
1743
|
+
const tombstone = await readAgentTombstone(dir);
|
|
1744
|
+
const slug = sidecar?.server_name;
|
|
1745
|
+
const names = serverNamesFor(slug);
|
|
1746
|
+
if (!identity?.api_key) {
|
|
1747
|
+
inventory.push({
|
|
1748
|
+
...slug ? { slug } : {},
|
|
1749
|
+
...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
|
|
1750
|
+
directory: dir,
|
|
1751
|
+
classification: tombstone ? "retired" : "orphaned",
|
|
1752
|
+
checks: []
|
|
1753
|
+
});
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
|
|
1757
|
+
const entry = {
|
|
1758
|
+
...slug ? { slug } : {},
|
|
1759
|
+
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
1760
|
+
directory: dir,
|
|
1761
|
+
classification: wired ? "wired" : "superseded",
|
|
1762
|
+
checks: []
|
|
1763
|
+
};
|
|
1764
|
+
if (wired) {
|
|
1765
|
+
const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
|
|
1766
|
+
entry.checks = result.checks;
|
|
1767
|
+
capabilitiesByDirectory.set(dir, result.signerCapabilities);
|
|
1768
|
+
}
|
|
1769
|
+
inventory.push(entry);
|
|
1770
|
+
}
|
|
1771
|
+
const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
|
|
1772
|
+
const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
|
|
1773
|
+
if (primaryDirectory) {
|
|
1774
|
+
const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
|
|
1775
|
+
signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
|
|
1776
|
+
for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
|
|
1777
|
+
}
|
|
1778
|
+
if (!primaryDirectory) {
|
|
1779
|
+
checks.push({
|
|
1780
|
+
id: "credentials",
|
|
1781
|
+
label: "Agent credentials",
|
|
1782
|
+
ok: false,
|
|
1783
|
+
detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
|
|
1784
|
+
repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
|
|
1785
|
+
});
|
|
1786
|
+
} else {
|
|
1787
|
+
const primaryIdentity = await readIdentity(primaryDirectory);
|
|
1788
|
+
const primarySidecar = await readRuntimeSidecar(primaryDirectory);
|
|
1789
|
+
if (primaryChecksById.size === 0) {
|
|
1790
|
+
const result = await checksForAgent(
|
|
1791
|
+
{ directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
|
|
1792
|
+
input,
|
|
1793
|
+
deps
|
|
1794
|
+
);
|
|
1795
|
+
signerCapabilities = result.signerCapabilities;
|
|
1796
|
+
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
1797
|
+
}
|
|
1798
|
+
for (const id of ["credentials", "signer_runtime"]) {
|
|
1799
|
+
const check = primaryChecksById.get(id);
|
|
1800
|
+
if (check) checks.push(check);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (configPath === null) {
|
|
1804
|
+
checks.push({
|
|
1805
|
+
id: "runtime_config",
|
|
1806
|
+
label: "Runtime MCP config",
|
|
1807
|
+
ok: true,
|
|
1808
|
+
detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
|
|
1809
|
+
});
|
|
1810
|
+
} else if (configText === null) {
|
|
1811
|
+
checks.push({
|
|
1812
|
+
id: "runtime_config",
|
|
1813
|
+
label: "Runtime MCP config",
|
|
1814
|
+
ok: false,
|
|
1815
|
+
detail: `No runtime config at ${configPath}.`,
|
|
1816
|
+
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
1817
|
+
});
|
|
1818
|
+
} else {
|
|
1819
|
+
const primaryIdentity = await readIdentity(primaryDirectory ?? "");
|
|
1820
|
+
const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
|
|
1821
|
+
const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
|
|
1822
|
+
const signerViaNpx = configText.includes("@haven_ai/signer");
|
|
1823
|
+
const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
|
|
1824
|
+
const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
|
|
1825
|
+
checks.push({
|
|
1826
|
+
id: "runtime_config",
|
|
1827
|
+
label: "Runtime MCP config",
|
|
1828
|
+
ok,
|
|
1829
|
+
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)" : ""}.`,
|
|
1830
|
+
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
for (const id of ["hosted_mcp", "identity_match"]) {
|
|
1834
|
+
const check = primaryChecksById.get(id);
|
|
1835
|
+
if (check) checks.push(check);
|
|
1836
|
+
}
|
|
1837
|
+
const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
|
|
1838
|
+
if (otherEntries.length > 0) {
|
|
1839
|
+
const live = [];
|
|
1840
|
+
const revoked = [];
|
|
1841
|
+
const unverifiable = [];
|
|
1842
|
+
const retired = [];
|
|
1843
|
+
for (const entry of otherEntries) {
|
|
1844
|
+
const identity = await readIdentity(entry.directory);
|
|
1845
|
+
const tombstone = await readAgentTombstone(entry.directory);
|
|
1846
|
+
const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? basename(entry.directory);
|
|
1847
|
+
const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
1848
|
+
if (!identity?.api_key || !otherUrl) {
|
|
1849
|
+
if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
|
|
1850
|
+
else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
|
|
1854
|
+
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
|
|
1855
|
+
if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
|
|
1856
|
+
else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
|
|
1857
|
+
else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
|
|
1858
|
+
}
|
|
1859
|
+
const parts = [];
|
|
1860
|
+
if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
|
|
1861
|
+
if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
|
|
1862
|
+
if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
|
|
1863
|
+
if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
|
|
1864
|
+
const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
|
|
1865
|
+
checks.push({
|
|
1866
|
+
id: "superseded_agents",
|
|
1867
|
+
label: "Superseded agent credentials",
|
|
1868
|
+
ok: supersededLive.length === 0,
|
|
1869
|
+
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("; ")}.`,
|
|
1870
|
+
...supersededLive.length > 0 ? {
|
|
1871
|
+
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.`
|
|
1872
|
+
} : {}
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
const signerProcess = primaryChecksById.get("signer_process");
|
|
1876
|
+
if (signerProcess) checks.push(signerProcess);
|
|
1431
1877
|
const restart = restartRequiredForRuntime(input.runtime, deps.env);
|
|
1432
1878
|
checks.push({
|
|
1433
1879
|
id: "restart",
|
|
@@ -1435,20 +1881,24 @@ async function runDoctor(input, deps = {}) {
|
|
|
1435
1881
|
ok: true,
|
|
1436
1882
|
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."
|
|
1437
1883
|
});
|
|
1884
|
+
const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
|
|
1438
1885
|
return {
|
|
1439
1886
|
version: 1,
|
|
1440
|
-
ok: checks.every((check) => check.ok),
|
|
1887
|
+
ok: checks.every((check) => check.ok) && wiredOk,
|
|
1441
1888
|
runtime: input.runtime,
|
|
1442
|
-
credentialDirectory:
|
|
1889
|
+
credentialDirectory: primaryDirectory,
|
|
1443
1890
|
checks,
|
|
1891
|
+
agents: inventory,
|
|
1444
1892
|
...signerCapabilities ? { signerCapabilities } : {}
|
|
1445
1893
|
};
|
|
1446
1894
|
}
|
|
1447
1895
|
async function runRepair(input, deps = {}) {
|
|
1448
1896
|
const homeDir = deps.homeDir ?? homedir();
|
|
1449
1897
|
const messages = [];
|
|
1450
|
-
const { directory,
|
|
1451
|
-
if (
|
|
1898
|
+
const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
|
|
1899
|
+
if (others.length > 0) {
|
|
1900
|
+
messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
|
|
1901
|
+
}
|
|
1452
1902
|
if (!directory) {
|
|
1453
1903
|
return {
|
|
1454
1904
|
ok: false,
|
|
@@ -1510,6 +1960,9 @@ var init_doctor = __esm({
|
|
|
1510
1960
|
init_config_writers();
|
|
1511
1961
|
init_runtime_registry();
|
|
1512
1962
|
init_signer_consent();
|
|
1963
|
+
init_tombstone();
|
|
1964
|
+
init_server_names();
|
|
1965
|
+
init_redact();
|
|
1513
1966
|
RERUN = "npx @haven_ai/connect@alpha";
|
|
1514
1967
|
}
|
|
1515
1968
|
});
|
|
@@ -1619,16 +2072,9 @@ function agentApiKeyPrefix(apiKey) {
|
|
|
1619
2072
|
return apiKey.slice(0, 12);
|
|
1620
2073
|
}
|
|
1621
2074
|
|
|
1622
|
-
// src/
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
function redactSecrets(value) {
|
|
1626
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
1627
|
-
}
|
|
1628
|
-
function shortAddress(address) {
|
|
1629
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
1630
|
-
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
1631
|
-
}
|
|
2075
|
+
// src/runtime.ts
|
|
2076
|
+
init_redact();
|
|
2077
|
+
init_server_names();
|
|
1632
2078
|
async function preflightCredentialStorage(input = {}) {
|
|
1633
2079
|
const directory = defaultCredentialRoot(input.baseDir);
|
|
1634
2080
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -1642,7 +2088,7 @@ async function preflightCredentialStorage(input = {}) {
|
|
|
1642
2088
|
return directory;
|
|
1643
2089
|
}
|
|
1644
2090
|
async function writeCredentialFiles(input) {
|
|
1645
|
-
const directory = defaultAgentDirectory(input.agentId, input.baseDir);
|
|
2091
|
+
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
1646
2092
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
1647
2093
|
await restrictPermissions(directory, 448, input.warn);
|
|
1648
2094
|
const identityPath = join(directory, "identity.json");
|
|
@@ -1707,6 +2153,17 @@ async function writeCredentialFiles(input) {
|
|
|
1707
2153
|
}
|
|
1708
2154
|
return { directory, identityPath, signerPath, agentPath };
|
|
1709
2155
|
}
|
|
2156
|
+
async function assertServerSlugAvailable(serverName, baseDir) {
|
|
2157
|
+
const directory = defaultAgentDirectory(serverName, baseDir);
|
|
2158
|
+
try {
|
|
2159
|
+
await stat(join(directory, "identity.json"));
|
|
2160
|
+
} catch {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
throw new Error(
|
|
2164
|
+
`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.`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
1710
2167
|
function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
|
|
1711
2168
|
return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
|
|
1712
2169
|
}
|
|
@@ -1746,6 +2203,7 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
1746
2203
|
|
|
1747
2204
|
// src/runtime-install.ts
|
|
1748
2205
|
init_config_writers();
|
|
2206
|
+
init_server_names();
|
|
1749
2207
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
1750
2208
|
try {
|
|
1751
2209
|
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
@@ -1871,7 +2329,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1871
2329
|
wrapperPath,
|
|
1872
2330
|
runtimeDirectory,
|
|
1873
2331
|
npmCacheDirectory,
|
|
1874
|
-
cliPath
|
|
2332
|
+
cliPath,
|
|
2333
|
+
serverName: input.serverName
|
|
1875
2334
|
});
|
|
1876
2335
|
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1877
2336
|
return {
|
|
@@ -1967,6 +2426,7 @@ async function writeWrapper2(input) {
|
|
|
1967
2426
|
}
|
|
1968
2427
|
async function writeRuntimeSidecar2(input) {
|
|
1969
2428
|
const value = {
|
|
2429
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
1970
2430
|
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1971
2431
|
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1972
2432
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
@@ -2200,12 +2660,13 @@ async function installRuntime(input, deps = {}) {
|
|
|
2200
2660
|
}
|
|
2201
2661
|
const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
|
|
2202
2662
|
progress("Setting up your Haven tools\u2026");
|
|
2203
|
-
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2663
|
+
const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
|
|
2204
2664
|
runtime,
|
|
2205
2665
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
2206
2666
|
apiKey: input.apiKey,
|
|
2207
2667
|
identityPath: input.identityPath,
|
|
2208
2668
|
signerPath: input.signerPath,
|
|
2669
|
+
serverName: input.serverName,
|
|
2209
2670
|
credentialDirectory: input.credentialDirectory,
|
|
2210
2671
|
localMcpCommand: localRuntimeInstall?.command,
|
|
2211
2672
|
signerCommand,
|
|
@@ -2285,7 +2746,7 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
2285
2746
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
2286
2747
|
};
|
|
2287
2748
|
}
|
|
2288
|
-
async function configureClaudeCode(deps, localMcpCommand) {
|
|
2749
|
+
async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
2289
2750
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
2290
2751
|
const serverJson = JSON.stringify({
|
|
2291
2752
|
type: "stdio",
|
|
@@ -2295,12 +2756,13 @@ async function configureClaudeCode(deps, localMcpCommand) {
|
|
|
2295
2756
|
});
|
|
2296
2757
|
try {
|
|
2297
2758
|
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
2298
|
-
|
|
2299
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2300
|
-
await runCommand("claude", ["mcp", "
|
|
2301
|
-
|
|
2759
|
+
const names = serverNamesFor(serverName);
|
|
2760
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2761
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2762
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
|
|
2763
|
+
await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
|
|
2302
2764
|
});
|
|
2303
|
-
const verified = await runCommand("claude", ["mcp", "get",
|
|
2765
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2304
2766
|
return {
|
|
2305
2767
|
hostedConfigured: false,
|
|
2306
2768
|
signerConfigured: true,
|
|
@@ -2345,11 +2807,12 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
|
2345
2807
|
env: {}
|
|
2346
2808
|
});
|
|
2347
2809
|
try {
|
|
2348
|
-
|
|
2349
|
-
await runCommand("claude", ["mcp", "remove",
|
|
2350
|
-
await runCommand("claude", ["mcp", "
|
|
2351
|
-
await runCommand("claude", ["mcp", "add-json",
|
|
2352
|
-
|
|
2810
|
+
const names = serverNamesFor(input.serverName);
|
|
2811
|
+
await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
|
|
2812
|
+
await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
|
|
2813
|
+
await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
|
|
2814
|
+
await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
|
|
2815
|
+
const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
|
|
2353
2816
|
return {
|
|
2354
2817
|
hostedConfigured: true,
|
|
2355
2818
|
signerConfigured: true,
|
|
@@ -2453,7 +2916,8 @@ async function prepareRuntimeForLocalMcp(input, deps) {
|
|
|
2453
2916
|
credentialDirectory: input.credentialDirectory,
|
|
2454
2917
|
identityPath: input.identityPath,
|
|
2455
2918
|
signerPath: input.signerPath,
|
|
2456
|
-
homeDir: deps.homeDir
|
|
2919
|
+
homeDir: deps.homeDir,
|
|
2920
|
+
serverName: input.serverName
|
|
2457
2921
|
});
|
|
2458
2922
|
}
|
|
2459
2923
|
async function prepareSignerForRuntime(input, deps) {
|
|
@@ -2467,7 +2931,8 @@ async function prepareSignerForRuntime(input, deps) {
|
|
|
2467
2931
|
return prepare({
|
|
2468
2932
|
credentialDirectory: input.credentialDirectory,
|
|
2469
2933
|
signerPath: input.signerPath,
|
|
2470
|
-
homeDir: deps.homeDir
|
|
2934
|
+
homeDir: deps.homeDir,
|
|
2935
|
+
serverName: input.serverName
|
|
2471
2936
|
});
|
|
2472
2937
|
}
|
|
2473
2938
|
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
@@ -2487,8 +2952,179 @@ function localRuntimePrepareErrorCode(err) {
|
|
|
2487
2952
|
|
|
2488
2953
|
// src/runtime.ts
|
|
2489
2954
|
init_runtime_registry();
|
|
2955
|
+
init_connect_error();
|
|
2956
|
+
|
|
2957
|
+
// src/installed-clients.ts
|
|
2958
|
+
init_connect_error();
|
|
2959
|
+
init_config_writers();
|
|
2960
|
+
init_runtime_registry();
|
|
2961
|
+
var SCAN_ORDER = [
|
|
2962
|
+
"claude-code",
|
|
2963
|
+
"codex-cli",
|
|
2964
|
+
"cursor",
|
|
2965
|
+
"vscode",
|
|
2966
|
+
"vscode-insiders",
|
|
2967
|
+
"claude-desktop",
|
|
2968
|
+
"hermes"
|
|
2969
|
+
];
|
|
2970
|
+
function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env = process.env) {
|
|
2971
|
+
const targets = [
|
|
2972
|
+
{
|
|
2973
|
+
runtime: "claude-code",
|
|
2974
|
+
label: "Claude Code",
|
|
2975
|
+
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2976
|
+
// not by writing a file this module owns — so its evidence is the
|
|
2977
|
+
// client directory, never a config path.
|
|
2978
|
+
configPath: null,
|
|
2979
|
+
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
runtime: "codex-cli",
|
|
2983
|
+
// Both Codex surfaces write the same ~/.codex/config.toml, so they are
|
|
2984
|
+
// ONE candidate. Splitting them would ask the user to answer a question
|
|
2985
|
+
// whose answers are the same write.
|
|
2986
|
+
label: "Codex (CLI or Desktop)",
|
|
2987
|
+
configPath: runtimeConfigPathFor("codex-cli", homeDir),
|
|
2988
|
+
markers: [join(homeDir, ".codex")]
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
runtime: "cursor",
|
|
2992
|
+
label: "Cursor",
|
|
2993
|
+
configPath: runtimeConfigPathFor("cursor", homeDir),
|
|
2994
|
+
markers: [join(homeDir, ".cursor")]
|
|
2995
|
+
},
|
|
2996
|
+
{
|
|
2997
|
+
runtime: "vscode",
|
|
2998
|
+
label: "VS Code",
|
|
2999
|
+
configPath: runtimeConfigPathFor("vscode", homeDir),
|
|
3000
|
+
markers: [resolve(cwd, ".vscode")]
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
runtime: "vscode-insiders",
|
|
3004
|
+
label: "VS Code Insiders",
|
|
3005
|
+
configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
|
|
3006
|
+
markers: []
|
|
3007
|
+
},
|
|
3008
|
+
{
|
|
3009
|
+
runtime: "claude-desktop",
|
|
3010
|
+
label: "Claude Desktop",
|
|
3011
|
+
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3012
|
+
markers: []
|
|
3013
|
+
},
|
|
3014
|
+
{
|
|
3015
|
+
runtime: "hermes",
|
|
3016
|
+
label: "Hermes Agent",
|
|
3017
|
+
configPath: runtimeConfigPathFor("hermes", homeDir),
|
|
3018
|
+
markers: [env.HERMES_HOME ?? join(homeDir, ".hermes")]
|
|
3019
|
+
}
|
|
3020
|
+
];
|
|
3021
|
+
return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
|
|
3022
|
+
}
|
|
3023
|
+
async function scanInstalledClients(options = {}) {
|
|
3024
|
+
const exists = options.exists ?? pathExists;
|
|
3025
|
+
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3026
|
+
const found = [];
|
|
3027
|
+
for (const target of targets) {
|
|
3028
|
+
if (target.configPath && await exists(target.configPath)) {
|
|
3029
|
+
found.push({
|
|
3030
|
+
runtime: target.runtime,
|
|
3031
|
+
label: target.label,
|
|
3032
|
+
detail: `MCP config found at ${target.configPath}`,
|
|
3033
|
+
configPath: target.configPath,
|
|
3034
|
+
evidence: "config-file"
|
|
3035
|
+
});
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
for (const marker of target.markers) {
|
|
3039
|
+
if (!await exists(marker)) continue;
|
|
3040
|
+
found.push({
|
|
3041
|
+
runtime: target.runtime,
|
|
3042
|
+
label: target.label,
|
|
3043
|
+
detail: `installed (${marker})`,
|
|
3044
|
+
configPath: target.configPath,
|
|
3045
|
+
evidence: "client-directory"
|
|
3046
|
+
});
|
|
3047
|
+
break;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
return found.sort((a, b) => {
|
|
3051
|
+
if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
|
|
3052
|
+
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
3056
|
+
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3057
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3058
|
+
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3059
|
+
io.write("These agent clients are installed on this machine:\n");
|
|
3060
|
+
candidates.forEach((candidate, index) => {
|
|
3061
|
+
io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
|
|
3062
|
+
`);
|
|
3063
|
+
});
|
|
3064
|
+
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");
|
|
3065
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3066
|
+
const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
|
|
3067
|
+
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3068
|
+
const trimmed = answer.trim();
|
|
3069
|
+
if (trimmed === "") return candidates[0].runtime;
|
|
3070
|
+
const picked = Number.parseInt(trimmed, 10);
|
|
3071
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3072
|
+
return candidates[picked - 1].runtime;
|
|
3073
|
+
}
|
|
3074
|
+
io.write(`"${trimmed}" is not one of 1-${candidates.length}.
|
|
3075
|
+
`);
|
|
3076
|
+
}
|
|
3077
|
+
throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
|
|
3078
|
+
}
|
|
3079
|
+
async function resolveRuntimeByInstalledClientPrompt(options = {}) {
|
|
3080
|
+
const candidates = await scanInstalledClients(options);
|
|
3081
|
+
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3082
|
+
return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
|
|
3083
|
+
}
|
|
3084
|
+
function noInstalledClientsError() {
|
|
3085
|
+
return new ConnectError(
|
|
3086
|
+
"runtime_no_installed_clients",
|
|
3087
|
+
"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.",
|
|
3088
|
+
"rerun_connect_with_explicit_runtime"
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
function promptAbortedError(reason) {
|
|
3092
|
+
return new ConnectError(
|
|
3093
|
+
"runtime_prompt_aborted",
|
|
3094
|
+
`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.`,
|
|
3095
|
+
"rerun_connect_and_choose_a_runtime"
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
async function pathExists(path) {
|
|
3099
|
+
try {
|
|
3100
|
+
await access(path);
|
|
3101
|
+
return true;
|
|
3102
|
+
} catch {
|
|
3103
|
+
return false;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
function defaultPromptIo() {
|
|
3107
|
+
return {
|
|
3108
|
+
write: (text) => process.stdout.write(text),
|
|
3109
|
+
question: (query) => new Promise((resolvePromise) => {
|
|
3110
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3111
|
+
let settled = false;
|
|
3112
|
+
const settle = (value) => {
|
|
3113
|
+
if (settled) return;
|
|
3114
|
+
settled = true;
|
|
3115
|
+
rl.close();
|
|
3116
|
+
resolvePromise(value);
|
|
3117
|
+
};
|
|
3118
|
+
rl.once("SIGINT", () => settle(null));
|
|
3119
|
+
rl.once("close", () => settle(null));
|
|
3120
|
+
rl.question(query, (answer) => settle(answer));
|
|
3121
|
+
})
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// src/runtime.ts
|
|
2490
3126
|
init_runtime_manifest();
|
|
2491
|
-
var CONNECTOR_VERSION = "0.1.
|
|
3127
|
+
var CONNECTOR_VERSION = "0.1.29-alpha.0";
|
|
2492
3128
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
2493
3129
|
async function runConnect(options, deps = {}) {
|
|
2494
3130
|
assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
|
|
@@ -2504,24 +3140,49 @@ async function runConnect(options, deps = {}) {
|
|
|
2504
3140
|
const runRuntimeInstall = deps.installRuntime ?? installRuntime;
|
|
2505
3141
|
const generateKey = deps.generateKey ?? generateDelegateKey;
|
|
2506
3142
|
const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
|
|
2507
|
-
const
|
|
3143
|
+
const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
|
|
3144
|
+
env: deps.env ?? process.env,
|
|
3145
|
+
selfReported: options.runtimeSelfReport,
|
|
3146
|
+
promptForRuntime: runtimeSelectionPrompt(options, deps)
|
|
3147
|
+
});
|
|
3148
|
+
if (!selection.runtime) {
|
|
3149
|
+
throw new ConnectError(
|
|
3150
|
+
"runtime_undetermined",
|
|
3151
|
+
`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.`,
|
|
3152
|
+
"rerun_connect_with_explicit_runtime"
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
const runtime = selection.runtime;
|
|
3156
|
+
const installCapabilities = runtimeInstallCapabilities(runtime);
|
|
2508
3157
|
if (options.localMcp) {
|
|
2509
|
-
|
|
2510
|
-
if (!supportsLocalMcp(resolvedRuntime)) {
|
|
3158
|
+
if (!supportsLocalMcp(runtime)) {
|
|
2511
3159
|
throw new Error(
|
|
2512
|
-
`--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(
|
|
3160
|
+
`--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.`
|
|
2513
3161
|
);
|
|
2514
3162
|
}
|
|
2515
3163
|
}
|
|
3164
|
+
if (selection.overrodeHint) {
|
|
3165
|
+
log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
|
|
3166
|
+
}
|
|
3167
|
+
if (selection.discardedHint) {
|
|
3168
|
+
log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${RUNTIME_FLAG_VALUES})`);
|
|
3169
|
+
}
|
|
3170
|
+
if (selection.source === "prompted") {
|
|
3171
|
+
log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
|
|
3172
|
+
}
|
|
2516
3173
|
log("Warming up your connection to Haven\u2026");
|
|
2517
3174
|
const setup = await api.resolveSetup({
|
|
2518
3175
|
setupToken: options.setupToken,
|
|
2519
3176
|
connectorVersion,
|
|
2520
|
-
runtime
|
|
3177
|
+
runtime
|
|
2521
3178
|
});
|
|
2522
3179
|
assertSetupChallengeIsUsable(setup.challenge.expires_at);
|
|
2523
3180
|
printSetupSummary(setup, log);
|
|
2524
3181
|
await preflightStorage({ baseDir: options.credentialsDir, warn: log });
|
|
3182
|
+
if (options.serverName) {
|
|
3183
|
+
assertValidServerSlug(options.serverName);
|
|
3184
|
+
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
3185
|
+
}
|
|
2525
3186
|
log("Checked local credential storage \u2014 all clear.");
|
|
2526
3187
|
const localKey = generateKey();
|
|
2527
3188
|
const localApiKey = generateLocalApiKey();
|
|
@@ -2533,7 +3194,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2533
3194
|
registration = await api.registerSetup({
|
|
2534
3195
|
setupToken: options.setupToken,
|
|
2535
3196
|
connectorVersion,
|
|
2536
|
-
runtime
|
|
3197
|
+
runtime,
|
|
2537
3198
|
challengeId: setup.challenge.id,
|
|
2538
3199
|
delegateAddress: localKey.address,
|
|
2539
3200
|
proofSignature,
|
|
@@ -2558,6 +3219,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2558
3219
|
const credentialPaths = await writeCredentials({
|
|
2559
3220
|
baseDir: options.credentialsDir,
|
|
2560
3221
|
agentId: registration.agent_id,
|
|
3222
|
+
serverName: options.serverName,
|
|
2561
3223
|
apiKey: localApiKey,
|
|
2562
3224
|
delegateKey: localKey.privateKey,
|
|
2563
3225
|
delegateAddress: localKey.address,
|
|
@@ -2585,7 +3247,7 @@ async function runConnect(options, deps = {}) {
|
|
|
2585
3247
|
);
|
|
2586
3248
|
}
|
|
2587
3249
|
const runtimeInstall = await runRuntimeInstall({
|
|
2588
|
-
runtime
|
|
3250
|
+
runtime,
|
|
2589
3251
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
2590
3252
|
apiKey: localApiKey,
|
|
2591
3253
|
signerPath: credentialPaths.signerPath,
|
|
@@ -2594,7 +3256,8 @@ async function runConnect(options, deps = {}) {
|
|
|
2594
3256
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
2595
3257
|
ackSigner: options.ackSigner,
|
|
2596
3258
|
ackLocalTools: options.ackLocalTools,
|
|
2597
|
-
localMcp: options.localMcp
|
|
3259
|
+
localMcp: options.localMcp,
|
|
3260
|
+
serverName: options.serverName
|
|
2598
3261
|
}, {
|
|
2599
3262
|
onProgress: log,
|
|
2600
3263
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -2630,6 +3293,19 @@ async function runConnect(options, deps = {}) {
|
|
|
2630
3293
|
} else {
|
|
2631
3294
|
log("Haven setup on this machine is complete.");
|
|
2632
3295
|
}
|
|
3296
|
+
try {
|
|
3297
|
+
const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
3298
|
+
if (supersededIds.length > 0) {
|
|
3299
|
+
log("");
|
|
3300
|
+
log(
|
|
3301
|
+
`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.`
|
|
3302
|
+
);
|
|
3303
|
+
log(
|
|
3304
|
+
`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.`
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
} catch {
|
|
3308
|
+
}
|
|
2633
3309
|
try {
|
|
2634
3310
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
2635
3311
|
runtime: runtimeInstall.runtime,
|
|
@@ -2704,11 +3380,16 @@ function completionOutcome(input) {
|
|
|
2704
3380
|
};
|
|
2705
3381
|
return outcome;
|
|
2706
3382
|
}
|
|
3383
|
+
function runtimeSelectionPrompt(options, deps) {
|
|
3384
|
+
if (options.interactive !== true) return void 0;
|
|
3385
|
+
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
3386
|
+
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
3387
|
+
}
|
|
2707
3388
|
function failedConnectOutcome(runtimeHint, error) {
|
|
2708
3389
|
const message = error instanceof Error ? error.message : "";
|
|
2709
|
-
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";
|
|
3390
|
+
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";
|
|
2710
3391
|
const runtime = normalizeRuntime(runtimeHint);
|
|
2711
|
-
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";
|
|
3392
|
+
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";
|
|
2712
3393
|
return {
|
|
2713
3394
|
schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
|
|
2714
3395
|
outcome: "failed",
|
|
@@ -2794,7 +3475,7 @@ function describeApprovedBudget(budget) {
|
|
|
2794
3475
|
async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
2795
3476
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
2796
3477
|
const timeoutMs = options.timeoutMs ?? 18e4;
|
|
2797
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
3478
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
2798
3479
|
const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
|
|
2799
3480
|
const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
|
|
2800
3481
|
let waitingAnnounced = false;
|
|
@@ -2880,11 +3561,39 @@ function activationInstructionWithWhy(profile) {
|
|
|
2880
3561
|
}
|
|
2881
3562
|
return profile.activationInstruction;
|
|
2882
3563
|
}
|
|
3564
|
+
var RERUN_HINT = "npx @haven_ai/connect@alpha";
|
|
3565
|
+
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
3566
|
+
const root = defaultCredentialRoot(baseDir);
|
|
3567
|
+
let entries = [];
|
|
3568
|
+
try {
|
|
3569
|
+
entries = await readdir(root);
|
|
3570
|
+
} catch {
|
|
3571
|
+
return [];
|
|
3572
|
+
}
|
|
3573
|
+
const ids = [];
|
|
3574
|
+
for (const entry of entries) {
|
|
3575
|
+
if (join(root, entry) === currentDirectory) continue;
|
|
3576
|
+
const identityPath = join(root, entry, "identity.json");
|
|
3577
|
+
try {
|
|
3578
|
+
await stat(identityPath);
|
|
3579
|
+
} catch {
|
|
3580
|
+
continue;
|
|
3581
|
+
}
|
|
3582
|
+
try {
|
|
3583
|
+
const identity = JSON.parse(await readFile(identityPath, "utf8"));
|
|
3584
|
+
ids.push(identity.agent_id ?? entry);
|
|
3585
|
+
} catch {
|
|
3586
|
+
ids.push(entry);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
return ids;
|
|
3590
|
+
}
|
|
2883
3591
|
function printNextSteps(result, log, approval) {
|
|
2884
3592
|
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
2885
3593
|
}
|
|
2886
3594
|
|
|
2887
3595
|
// src/args.ts
|
|
3596
|
+
init_server_names();
|
|
2888
3597
|
function parseArgs(argv, env = process.env) {
|
|
2889
3598
|
const options = {
|
|
2890
3599
|
apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
|
|
@@ -2894,6 +3603,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
2894
3603
|
let json = false;
|
|
2895
3604
|
let doctor = false;
|
|
2896
3605
|
let repair = false;
|
|
3606
|
+
let tombstoneDir;
|
|
3607
|
+
let tombstoneReason;
|
|
3608
|
+
let tombstoneReplacedBy;
|
|
2897
3609
|
for (let i = 0; i < argv.length; i += 1) {
|
|
2898
3610
|
const arg = argv[i];
|
|
2899
3611
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -2904,14 +3616,25 @@ function parseArgs(argv, env = process.env) {
|
|
|
2904
3616
|
doctor = true;
|
|
2905
3617
|
} else if (arg === "--repair") {
|
|
2906
3618
|
repair = true;
|
|
3619
|
+
} else if (arg === "--tombstone") {
|
|
3620
|
+
tombstoneDir = requireValue(argv, ++i, arg);
|
|
3621
|
+
} else if (arg === "--reason") {
|
|
3622
|
+
tombstoneReason = requireValue(argv, ++i, arg);
|
|
3623
|
+
} else if (arg === "--replaced-by") {
|
|
3624
|
+
tombstoneReplacedBy = requireValue(argv, ++i, arg);
|
|
2907
3625
|
} else if (arg === "--setup" || arg === "--setup-token") {
|
|
2908
3626
|
options.setupToken = requireValue(argv, ++i, arg);
|
|
2909
3627
|
} else if (arg === "--api" || arg === "--api-url") {
|
|
2910
3628
|
options.apiBaseUrl = requireValue(argv, ++i, arg);
|
|
2911
3629
|
} else if (arg === "--runtime") {
|
|
2912
3630
|
options.runtime = requireValue(argv, ++i, arg);
|
|
3631
|
+
} else if (arg === "--runtime-force") {
|
|
3632
|
+
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
2913
3633
|
} else if (arg === "--credentials-dir") {
|
|
2914
3634
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
3635
|
+
} else if (arg === "--name") {
|
|
3636
|
+
options.serverName = requireValue(argv, ++i, arg);
|
|
3637
|
+
assertValidServerSlug(options.serverName);
|
|
2915
3638
|
} else if (arg === "--environment-label") {
|
|
2916
3639
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
2917
3640
|
} else if (arg === "--ack-local-tools") {
|
|
@@ -2929,14 +3652,21 @@ function parseArgs(argv, env = process.env) {
|
|
|
2929
3652
|
throw new Error(`Unknown option: ${arg}`);
|
|
2930
3653
|
}
|
|
2931
3654
|
}
|
|
3655
|
+
const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
|
|
2932
3656
|
if (help) {
|
|
2933
|
-
return { options, help, json, doctor, repair };
|
|
3657
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
3658
|
+
}
|
|
3659
|
+
if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
|
|
3660
|
+
throw new Error("--reason and --replaced-by require --tombstone <dir>.");
|
|
3661
|
+
}
|
|
3662
|
+
if (tombstone) {
|
|
3663
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2934
3664
|
}
|
|
2935
3665
|
if (doctor || repair) {
|
|
2936
3666
|
if (!options.runtime) {
|
|
2937
3667
|
throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
|
|
2938
3668
|
}
|
|
2939
|
-
return { options, help, json, doctor, repair };
|
|
3669
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2940
3670
|
}
|
|
2941
3671
|
if (!options.setupToken) {
|
|
2942
3672
|
throw new Error("Missing --setup <hv_setup_...> setup token.");
|
|
@@ -2945,7 +3675,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
2945
3675
|
throw new Error("Missing --api <Haven API URL>.");
|
|
2946
3676
|
}
|
|
2947
3677
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
2948
|
-
return { options, help, json, doctor, repair };
|
|
3678
|
+
return { options, help, json, doctor, repair, tombstone };
|
|
2949
3679
|
}
|
|
2950
3680
|
function helpText() {
|
|
2951
3681
|
return [
|
|
@@ -2955,14 +3685,23 @@ function helpText() {
|
|
|
2955
3685
|
"sends Haven only the public signing address plus a proof signature.",
|
|
2956
3686
|
"",
|
|
2957
3687
|
"Usage:",
|
|
2958
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools
|
|
3688
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
|
|
2959
3689
|
"",
|
|
2960
3690
|
"Options:",
|
|
2961
3691
|
" --setup <token> Short-lived setup token from Haven.",
|
|
2962
3692
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
2963
3693
|
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
|
|
3694
|
+
" Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
|
|
3695
|
+
" that contradicts this hint wins (with a printed notice). When nothing is detected, an",
|
|
3696
|
+
" interactive terminal is offered the agent clients installed on this machine; this flag is",
|
|
3697
|
+
" how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
|
|
3698
|
+
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
2964
3699
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
2965
3700
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
3701
|
+
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
3702
|
+
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
3703
|
+
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
3704
|
+
" single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
|
|
2966
3705
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
2967
3706
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
2968
3707
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -2973,6 +3712,11 @@ function helpText() {
|
|
|
2973
3712
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
2974
3713
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
2975
3714
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
3715
|
+
" --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
|
|
3716
|
+
" wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
|
|
3717
|
+
" writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
|
|
3718
|
+
" --reason <text> Reason recorded in the tombstone (with --tombstone).",
|
|
3719
|
+
" --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
|
|
2976
3720
|
" --help Show this help.",
|
|
2977
3721
|
"",
|
|
2978
3722
|
"The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
|
|
@@ -2987,6 +3731,7 @@ function requireValue(argv, index, option) {
|
|
|
2987
3731
|
}
|
|
2988
3732
|
|
|
2989
3733
|
// src/cli.ts
|
|
3734
|
+
init_redact();
|
|
2990
3735
|
async function runCli(argv, io = {
|
|
2991
3736
|
stdout: (message) => process.stdout.write(message),
|
|
2992
3737
|
stderr: (message) => process.stderr.write(message)
|
|
@@ -3010,6 +3755,45 @@ async function runCli(argv, io = {
|
|
|
3010
3755
|
`);
|
|
3011
3756
|
return 0;
|
|
3012
3757
|
}
|
|
3758
|
+
if (parsed.tombstone) {
|
|
3759
|
+
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
3760
|
+
const { readFile: readFile11 } = await import('fs/promises');
|
|
3761
|
+
const { join: join10 } = await import('path');
|
|
3762
|
+
try {
|
|
3763
|
+
let agentId = "unknown";
|
|
3764
|
+
try {
|
|
3765
|
+
const identity = JSON.parse(
|
|
3766
|
+
await readFile11(join10(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
3767
|
+
);
|
|
3768
|
+
agentId = identity.agent_id ?? "unknown";
|
|
3769
|
+
} catch {
|
|
3770
|
+
}
|
|
3771
|
+
const info = await writeAgentTombstone2({
|
|
3772
|
+
directory: parsed.tombstone.directory,
|
|
3773
|
+
agentId,
|
|
3774
|
+
reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
|
|
3775
|
+
replacedBy: parsed.tombstone.replacedBy
|
|
3776
|
+
});
|
|
3777
|
+
if (parsed.json) {
|
|
3778
|
+
io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
|
|
3779
|
+
`);
|
|
3780
|
+
} else {
|
|
3781
|
+
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
3782
|
+
`));
|
|
3783
|
+
io.stdout(
|
|
3784
|
+
"Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
|
|
3785
|
+
);
|
|
3786
|
+
io.stdout(
|
|
3787
|
+
"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"
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
return 0;
|
|
3791
|
+
} catch (err) {
|
|
3792
|
+
io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
|
|
3793
|
+
`);
|
|
3794
|
+
return 1;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3013
3797
|
if (parsed.doctor || parsed.repair) {
|
|
3014
3798
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3015
3799
|
const runtime = parsed.options.runtime ?? "";
|
|
@@ -3032,6 +3816,23 @@ async function runCli(argv, io = {
|
|
|
3032
3816
|
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3033
3817
|
`));
|
|
3034
3818
|
}
|
|
3819
|
+
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
3820
|
+
if (otherAgents.length > 0) {
|
|
3821
|
+
io.stdout("\nOther agents on this machine:\n");
|
|
3822
|
+
for (const agent of otherAgents) {
|
|
3823
|
+
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
3824
|
+
const failed = agent.checks.filter((check) => !check.ok);
|
|
3825
|
+
const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
|
|
3826
|
+
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
|
|
3827
|
+
`));
|
|
3828
|
+
for (const check of failed) {
|
|
3829
|
+
io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
|
|
3830
|
+
`));
|
|
3831
|
+
if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
|
|
3832
|
+
`));
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3035
3836
|
io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
|
|
3036
3837
|
}
|
|
3037
3838
|
return report.ok ? 0 : 1;
|
|
@@ -3043,7 +3844,15 @@ async function runCli(argv, io = {
|
|
|
3043
3844
|
}
|
|
3044
3845
|
try {
|
|
3045
3846
|
const result = await runConnect(
|
|
3046
|
-
{
|
|
3847
|
+
{
|
|
3848
|
+
...parsed.options,
|
|
3849
|
+
waitForApproval: !parsed.json,
|
|
3850
|
+
// #1719: only a human-facing run may be asked which installed client to
|
|
3851
|
+
// configure. --json is the automation contract — it must fail with a
|
|
3852
|
+
// machine-readable code, never block on stdin. runConnect additionally
|
|
3853
|
+
// requires a real TTY before it prompts.
|
|
3854
|
+
interactive: !parsed.json
|
|
3855
|
+
},
|
|
3047
3856
|
{
|
|
3048
3857
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|
|
3049
3858
|
`),
|