@haven_ai/connect 0.9.0-alpha.0 → 0.10.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -24
- package/dist/cli.cjs +847 -568
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +847 -568
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +849 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -7
- package/dist/index.d.ts +36 -7
- package/dist/index.js +848 -569
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import crypto, { createHash } from 'crypto';
|
|
2
2
|
import { Wallet } from 'ethers';
|
|
3
|
-
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, realpath, rename } from 'fs/promises';
|
|
4
|
-
import { homedir, platform } from 'os';
|
|
5
|
-
import { join, resolve, dirname, basename } from 'path';
|
|
6
3
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
7
4
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
8
5
|
import { connectorRerunCommand, isSupportedNodeVersion, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
|
|
9
|
-
import { parseDocument, isMap, stringify } from 'yaml';
|
|
10
6
|
import { spawn, execFile } from 'child_process';
|
|
7
|
+
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, rename, realpath } from 'fs/promises';
|
|
8
|
+
import { homedir, platform } from 'os';
|
|
9
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
11
10
|
import { promisify } from 'util';
|
|
11
|
+
import { parseDocument, isMap, stringify } from 'yaml';
|
|
12
12
|
import { createInterface } from 'readline';
|
|
13
13
|
import { realpathSync } from 'fs';
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -64,6 +64,25 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch, clientIdentity) {
|
|
|
64
64
|
method: "GET",
|
|
65
65
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
66
66
|
}),
|
|
67
|
+
reportRetiredAgents: async (input) => {
|
|
68
|
+
try {
|
|
69
|
+
const body = await send(
|
|
70
|
+
`${root}/agent-connection-setups/${encodeURIComponent(input.setupId)}/retired-agents`,
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { Authorization: `Bearer ${input.apiKey}` },
|
|
74
|
+
body: JSON.stringify({ retired_agent_ids: input.retiredAgentIds })
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
const recorded = body?.recorded_agent_ids;
|
|
78
|
+
return {
|
|
79
|
+
recordedAgentIds: Array.isArray(recorded) ? recorded.filter((id) => typeof id === "string") : []
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof ConnectRequestError && err.status === 404) return null;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
67
86
|
updateInstallStatus: async (setupId, apiKey, input) => {
|
|
68
87
|
await send(`${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
|
|
69
88
|
method: "POST",
|
|
@@ -158,81 +177,574 @@ var init_key = __esm({
|
|
|
158
177
|
}
|
|
159
178
|
});
|
|
160
179
|
|
|
161
|
-
// src/redact.ts
|
|
162
|
-
function redactSecrets(value) {
|
|
163
|
-
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
180
|
+
// src/redact.ts
|
|
181
|
+
function redactSecrets(value) {
|
|
182
|
+
return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
|
|
183
|
+
}
|
|
184
|
+
function redactForAutomation(value) {
|
|
185
|
+
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
186
|
+
}
|
|
187
|
+
function withoutUserinfo(url) {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = new URL(url);
|
|
190
|
+
parsed.username = "";
|
|
191
|
+
parsed.password = "";
|
|
192
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
193
|
+
} catch {
|
|
194
|
+
return url;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function shortAddress(address) {
|
|
198
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
199
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
200
|
+
}
|
|
201
|
+
var API_KEY_RE, PRIVATE_KEY_RE;
|
|
202
|
+
var init_redact = __esm({
|
|
203
|
+
"src/redact.ts"() {
|
|
204
|
+
API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
|
|
205
|
+
PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// src/server-names.ts
|
|
210
|
+
function assertValidServerSlug(slug) {
|
|
211
|
+
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (slug === "haven" || slug === "haven-signer") {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (slug === "signer" || slug.startsWith("signer-")) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function serverNamesFor(slug) {
|
|
228
|
+
if (slug === void 0) {
|
|
229
|
+
return {
|
|
230
|
+
hosted: "haven",
|
|
231
|
+
signer: "haven-signer",
|
|
232
|
+
// Historical Codex table names — every wired Codex host has these.
|
|
233
|
+
codexHosted: "haven",
|
|
234
|
+
codexSigner: "haven_signer",
|
|
235
|
+
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
assertValidServerSlug(slug);
|
|
239
|
+
return {
|
|
240
|
+
hosted: `haven-${slug}`,
|
|
241
|
+
signer: `haven-signer-${slug}`,
|
|
242
|
+
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
243
|
+
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
244
|
+
codexHosted: `haven-${slug}`,
|
|
245
|
+
codexSigner: `haven-signer-${slug}`,
|
|
246
|
+
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
var SLUG_RE;
|
|
250
|
+
var init_server_names = __esm({
|
|
251
|
+
"src/server-names.ts"() {
|
|
252
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
function mcpPackageSpec() {
|
|
256
|
+
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
257
|
+
}
|
|
258
|
+
function sdkPackageSpec() {
|
|
259
|
+
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
260
|
+
}
|
|
261
|
+
function signerPackageSpec() {
|
|
262
|
+
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
263
|
+
}
|
|
264
|
+
var MCP_RUNTIME_MANIFEST;
|
|
265
|
+
var init_runtime_manifest = __esm({
|
|
266
|
+
"src/runtime-manifest.ts"() {
|
|
267
|
+
MCP_RUNTIME_MANIFEST = {
|
|
268
|
+
mcpPackage: "@haven_ai/mcp",
|
|
269
|
+
mcpVersion: MCP_VERSION,
|
|
270
|
+
sdkPackage: "@haven_ai/sdk",
|
|
271
|
+
sdkVersion: "0.10.0-alpha.0",
|
|
272
|
+
signerPackage: "@haven_ai/signer",
|
|
273
|
+
signerVersion: "0.10.0-alpha.0",
|
|
274
|
+
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
275
|
+
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
276
|
+
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
277
|
+
// — including on the `--local` path where it does run. A hand-maintained
|
|
278
|
+
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
279
|
+
// this against `package.json`'s `engines.node`.
|
|
280
|
+
minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
|
|
281
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
282
|
+
requiredTools: registeredToolNames(),
|
|
283
|
+
/**
|
|
284
|
+
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
285
|
+
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
286
|
+
* literal list here would rot the first time the signer gains a tool.
|
|
287
|
+
* The handshake probe requires all of them.
|
|
288
|
+
*/
|
|
289
|
+
requiredSignerTools: Object.keys(toolSchemas)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
function resolveRuntimeSpecOverride(env) {
|
|
294
|
+
const override = {};
|
|
295
|
+
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
296
|
+
const variable = RUNTIME_SPEC_ENV[pkg];
|
|
297
|
+
const raw = env[variable];
|
|
298
|
+
if (raw === void 0) continue;
|
|
299
|
+
override[pkg] = validateSpec(variable, raw);
|
|
300
|
+
}
|
|
301
|
+
return Object.keys(override).length > 0 ? override : void 0;
|
|
302
|
+
}
|
|
303
|
+
function validateSpec(variable, raw) {
|
|
304
|
+
if (raw.length === 0 || raw.trim().length === 0) {
|
|
305
|
+
throw new RuntimeSpecOverrideError(variable, "it is empty");
|
|
306
|
+
}
|
|
307
|
+
if (/\s/.test(raw)) {
|
|
308
|
+
throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
|
|
309
|
+
}
|
|
310
|
+
if (/[\u0000-\u001f\u007f]/.test(raw)) {
|
|
311
|
+
throw new RuntimeSpecOverrideError(variable, "it contains a control character");
|
|
312
|
+
}
|
|
313
|
+
const meta = SHELL_METACHARACTERS.exec(raw);
|
|
314
|
+
if (meta) {
|
|
315
|
+
throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
|
|
316
|
+
}
|
|
317
|
+
return raw;
|
|
318
|
+
}
|
|
319
|
+
function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
|
|
320
|
+
const digest = createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
|
|
321
|
+
return `override-${digest.slice(0, 12)}`;
|
|
322
|
+
}
|
|
323
|
+
function overrideApplies(override, packages) {
|
|
324
|
+
return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
|
|
325
|
+
}
|
|
326
|
+
function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
|
|
327
|
+
const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
|
|
328
|
+
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
329
|
+
const spec = override[pkg];
|
|
330
|
+
if (spec === void 0) continue;
|
|
331
|
+
const pin = pinned[pkg];
|
|
332
|
+
lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
|
|
333
|
+
}
|
|
334
|
+
lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
|
|
335
|
+
lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
|
|
336
|
+
return lines;
|
|
337
|
+
}
|
|
338
|
+
function describeRuntimeSpecOverride(override) {
|
|
339
|
+
return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
|
|
340
|
+
}
|
|
341
|
+
var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
|
|
342
|
+
var init_runtime_spec_override = __esm({
|
|
343
|
+
"src/runtime-spec-override.ts"() {
|
|
344
|
+
RUNTIME_SPEC_ENV = {
|
|
345
|
+
signer: "HAVEN_SIGNER_SPEC",
|
|
346
|
+
sdk: "HAVEN_SDK_SPEC",
|
|
347
|
+
mcp: "HAVEN_MCP_SPEC"
|
|
348
|
+
};
|
|
349
|
+
SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
|
|
350
|
+
RuntimeSpecOverrideError = class extends Error {
|
|
351
|
+
code = "runtime_spec_override_invalid";
|
|
352
|
+
variable;
|
|
353
|
+
constructor(variable, reason) {
|
|
354
|
+
super(`${variable} is set but not usable as an npm package spec: ${reason}. Unset it, or set it to a spec npm install accepts (file:/abs/path, a .tgz, or @haven_ai/<pkg>@<version>).`);
|
|
355
|
+
this.name = "RuntimeSpecOverrideError";
|
|
356
|
+
this.variable = variable;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
async function prepareSignerRuntime(input, deps = {}) {
|
|
362
|
+
const homeDir = input.homeDir ?? homedir();
|
|
363
|
+
const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
|
|
364
|
+
const signerSpec = override?.signer ?? signerPackageSpec();
|
|
365
|
+
const sdkSpec = override?.sdk ?? sdkPackageSpec();
|
|
366
|
+
const resolvedSpecs = [signerSpec, sdkSpec];
|
|
367
|
+
const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
|
|
368
|
+
const runtimeDirectory = resolve(
|
|
369
|
+
homeDir,
|
|
370
|
+
".haven",
|
|
371
|
+
"signer-runtime",
|
|
372
|
+
overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.signerVersion
|
|
373
|
+
);
|
|
374
|
+
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
375
|
+
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
|
|
376
|
+
const messages = [];
|
|
377
|
+
if (override) {
|
|
378
|
+
messages.push(...runtimeSpecOverrideNotice(
|
|
379
|
+
"signer runtime",
|
|
380
|
+
override,
|
|
381
|
+
{ signer: signerPackageSpec(), sdk: sdkPackageSpec() },
|
|
382
|
+
runtimeDirectory
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
386
|
+
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
387
|
+
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
388
|
+
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
389
|
+
if (override) {
|
|
390
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
391
|
+
messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
|
|
392
|
+
} else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
393
|
+
messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
|
|
394
|
+
} else {
|
|
395
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
396
|
+
messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
|
|
397
|
+
}
|
|
398
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
399
|
+
const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
|
|
400
|
+
const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
|
|
401
|
+
await writeWrapper({
|
|
402
|
+
wrapperPath,
|
|
403
|
+
cliPath,
|
|
404
|
+
signerPath: input.signerPath,
|
|
405
|
+
overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
|
|
406
|
+
});
|
|
407
|
+
await writeRuntimeSidecar({
|
|
408
|
+
path: join(input.credentialDirectory, "signer-runtime.json"),
|
|
409
|
+
wrapperPath,
|
|
410
|
+
runtimeDirectory,
|
|
411
|
+
npmCacheDirectory,
|
|
412
|
+
cliPath,
|
|
413
|
+
serverName: input.serverName,
|
|
414
|
+
override: overrideRecord,
|
|
415
|
+
installedVersions
|
|
416
|
+
});
|
|
417
|
+
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
418
|
+
return {
|
|
419
|
+
command: wrapperPath,
|
|
420
|
+
args: [],
|
|
421
|
+
wrapperPath,
|
|
422
|
+
runtimeDirectory,
|
|
423
|
+
npmCacheDirectory,
|
|
424
|
+
cliPath,
|
|
425
|
+
messages,
|
|
426
|
+
...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function resolveSignerRuntimeOverride(env) {
|
|
430
|
+
const override = resolveRuntimeSpecOverride(env);
|
|
431
|
+
if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
|
|
432
|
+
const { signer, sdk } = override;
|
|
433
|
+
return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
|
|
434
|
+
}
|
|
435
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
|
|
436
|
+
const { runCommand, onProgress } = deps;
|
|
437
|
+
const baseArgs = [
|
|
438
|
+
"install",
|
|
439
|
+
"--prefix",
|
|
440
|
+
runtimeDirectory,
|
|
441
|
+
"--no-audit",
|
|
442
|
+
"--no-fund",
|
|
443
|
+
"--omit=dev",
|
|
444
|
+
"--prefer-offline",
|
|
445
|
+
...packageSpecs
|
|
446
|
+
];
|
|
447
|
+
const run = async (args) => {
|
|
448
|
+
const startedAt = Date.now();
|
|
449
|
+
const heartbeat = setInterval(() => {
|
|
450
|
+
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
451
|
+
onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
452
|
+
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
453
|
+
heartbeat.unref?.();
|
|
454
|
+
try {
|
|
455
|
+
if (runCommand) await runCommand("npm", args);
|
|
456
|
+
else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
457
|
+
} finally {
|
|
458
|
+
clearInterval(heartbeat);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
try {
|
|
462
|
+
await run(baseArgs);
|
|
463
|
+
} catch {
|
|
464
|
+
try {
|
|
465
|
+
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
474
|
+
return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
|
|
475
|
+
signerVersion: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
476
|
+
sdkVersion: MCP_RUNTIME_MANIFEST.sdkVersion
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
|
|
480
|
+
try {
|
|
481
|
+
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
482
|
+
const installed = await readInstalledVersions(runtimeDirectory);
|
|
483
|
+
return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function readInstalledVersions(runtimeDirectory) {
|
|
489
|
+
const [signerPackage, sdkPackage] = await Promise.all([
|
|
490
|
+
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
|
|
491
|
+
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
492
|
+
]);
|
|
493
|
+
return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
|
|
494
|
+
}
|
|
495
|
+
async function readPackageJson(path) {
|
|
496
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
497
|
+
}
|
|
498
|
+
async function writeWrapper(input) {
|
|
499
|
+
await mkdir(dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
500
|
+
await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
501
|
+
const source = [
|
|
502
|
+
"#!/usr/bin/env node",
|
|
503
|
+
...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
|
|
504
|
+
"import { spawn } from 'node:child_process'",
|
|
505
|
+
"",
|
|
506
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
507
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
508
|
+
"",
|
|
509
|
+
"const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
|
|
510
|
+
" stdio: 'inherit',",
|
|
511
|
+
"})",
|
|
512
|
+
"",
|
|
513
|
+
"child.on('exit', (code, signal) => {",
|
|
514
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
515
|
+
" else process.exit(code ?? 1)",
|
|
516
|
+
"})",
|
|
517
|
+
""
|
|
518
|
+
].join("\n");
|
|
519
|
+
await writeFile(input.wrapperPath, source, { mode: 448 });
|
|
520
|
+
await chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
521
|
+
}
|
|
522
|
+
async function readRuntimeSidecar(credentialDirectory) {
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(
|
|
525
|
+
await readFile(join(credentialDirectory, "signer-runtime.json"), "utf8")
|
|
526
|
+
);
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function writeRuntimeSidecar(input) {
|
|
532
|
+
const value = {
|
|
533
|
+
...input.serverName ? { server_name: input.serverName } : {},
|
|
534
|
+
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
535
|
+
signer_version: input.installedVersions?.signerVersion ?? MCP_RUNTIME_MANIFEST.signerVersion,
|
|
536
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
537
|
+
sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
538
|
+
wrapper_path: input.wrapperPath,
|
|
539
|
+
runtime_directory: input.runtimeDirectory,
|
|
540
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
541
|
+
cli_path: input.cliPath,
|
|
542
|
+
...input.override ? { runtime_spec_override: input.override } : {}
|
|
543
|
+
};
|
|
544
|
+
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
545
|
+
`, { mode: 384 });
|
|
546
|
+
await chmod(input.path, 384).catch(() => void 0);
|
|
547
|
+
}
|
|
548
|
+
async function assertFileExists(path, label) {
|
|
549
|
+
try {
|
|
550
|
+
await access(path);
|
|
551
|
+
} catch {
|
|
552
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
|
|
556
|
+
var init_signer_runtime = __esm({
|
|
557
|
+
"src/signer-runtime.ts"() {
|
|
558
|
+
init_runtime_manifest();
|
|
559
|
+
init_runtime_spec_override();
|
|
560
|
+
execFileAsync = promisify(execFile);
|
|
561
|
+
SIGNER_INSTALL_TIMEOUT_MS = 6e5;
|
|
562
|
+
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// src/connect-error.ts
|
|
567
|
+
function isConnectError(err) {
|
|
568
|
+
return err instanceof ConnectError;
|
|
569
|
+
}
|
|
570
|
+
var ConnectError;
|
|
571
|
+
var init_connect_error = __esm({
|
|
572
|
+
"src/connect-error.ts"() {
|
|
573
|
+
ConnectError = class extends Error {
|
|
574
|
+
code;
|
|
575
|
+
nextAction;
|
|
576
|
+
details;
|
|
577
|
+
constructor(code, message, nextAction2, details = {}) {
|
|
578
|
+
super(message);
|
|
579
|
+
this.name = "ConnectError";
|
|
580
|
+
this.code = code;
|
|
581
|
+
this.nextAction = nextAction2;
|
|
582
|
+
this.details = details;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// src/tombstone.ts
|
|
589
|
+
var tombstone_exports = {};
|
|
590
|
+
__export(tombstone_exports, {
|
|
591
|
+
ROOT_TOMBSTONES_DIRNAME: () => ROOT_TOMBSTONES_DIRNAME,
|
|
592
|
+
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
593
|
+
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
594
|
+
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
595
|
+
readAgentTombstone: () => readAgentTombstone,
|
|
596
|
+
readTombstoneRecords: () => readTombstoneRecords,
|
|
597
|
+
tombstonesDirForAgentDirectory: () => tombstonesDirForAgentDirectory,
|
|
598
|
+
tombstonesDirForCredentialRoot: () => tombstonesDirForCredentialRoot,
|
|
599
|
+
writeAgentTombstone: () => writeAgentTombstone
|
|
600
|
+
});
|
|
601
|
+
function defaultTombstonesDir(baseDir) {
|
|
602
|
+
return join(baseDir ?? join(homedir(), ".haven"), "tombstones");
|
|
164
603
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
604
|
+
function tombstonesDirForCredentialRoot(credentialRoot, homeDir = homedir()) {
|
|
605
|
+
if (!credentialRoot) return defaultTombstonesDir(join(homeDir, ".haven"));
|
|
606
|
+
const root = resolve(credentialRoot);
|
|
607
|
+
if (root === resolve(homeDir, ".haven", "agents")) return defaultTombstonesDir(join(homeDir, ".haven"));
|
|
608
|
+
return join(root, ROOT_TOMBSTONES_DIRNAME);
|
|
167
609
|
}
|
|
168
|
-
function
|
|
610
|
+
function tombstonesDirForAgentDirectory(directory, homeDir = homedir()) {
|
|
611
|
+
return tombstonesDirForCredentialRoot(dirname(resolve(directory)), homeDir);
|
|
612
|
+
}
|
|
613
|
+
async function readTombstoneRecords(tombstonesDir) {
|
|
614
|
+
const root = tombstonesDir ?? defaultTombstonesDir();
|
|
615
|
+
let entries = [];
|
|
169
616
|
try {
|
|
170
|
-
|
|
171
|
-
parsed.username = "";
|
|
172
|
-
parsed.password = "";
|
|
173
|
-
return parsed.toString().replace(/\/+$/, "");
|
|
617
|
+
entries = await readdir(root);
|
|
174
618
|
} catch {
|
|
175
|
-
return
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
const records = [];
|
|
622
|
+
for (const entry of entries) {
|
|
623
|
+
if (!entry.endsWith(".json")) continue;
|
|
624
|
+
const recordPath = join(root, entry);
|
|
625
|
+
try {
|
|
626
|
+
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
627
|
+
if (typeof parsed?.agent_id === "string") {
|
|
628
|
+
records.push({ ...parsed, recordPath });
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
176
632
|
}
|
|
633
|
+
return records;
|
|
177
634
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
635
|
+
function tombstoneScript(info) {
|
|
636
|
+
const lines = [
|
|
637
|
+
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
638
|
+
"",
|
|
639
|
+
` agent: ${info.agent_id}`,
|
|
640
|
+
` retired at: ${info.retired_at}`,
|
|
641
|
+
` reason: ${info.reason}`,
|
|
642
|
+
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
643
|
+
"",
|
|
644
|
+
"This process is running with a wiring snapshot that predates the",
|
|
645
|
+
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
646
|
+
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
647
|
+
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
648
|
+
"one of them: each holds the snapshot from its own start time, so after",
|
|
649
|
+
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
650
|
+
"",
|
|
651
|
+
`Then verify with: ${connectorRerunCommand("--doctor --runtime <runtime>")}`
|
|
652
|
+
];
|
|
653
|
+
return [
|
|
654
|
+
"#!/usr/bin/env node",
|
|
655
|
+
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
656
|
+
"// once every long-lived MCP host on this machine has been restarted.",
|
|
657
|
+
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
658
|
+
"process.exit(1)",
|
|
659
|
+
""
|
|
660
|
+
].join("\n");
|
|
181
661
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// src/server-names.ts
|
|
191
|
-
function assertValidServerSlug(slug) {
|
|
192
|
-
if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
if (slug === "haven" || slug === "haven-signer") {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
|
|
662
|
+
async function writeAgentTombstone(input) {
|
|
663
|
+
const dirStat = await stat(input.directory).catch(() => null);
|
|
664
|
+
if (!dirStat?.isDirectory()) {
|
|
665
|
+
throw new ConnectError(
|
|
666
|
+
"tombstone_directory_not_found",
|
|
667
|
+
`Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
|
|
668
|
+
"retry_with_an_existing_agent_directory"
|
|
200
669
|
);
|
|
201
670
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
671
|
+
const info = {
|
|
672
|
+
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
673
|
+
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
674
|
+
// like every other output path, at the write layer so any future caller
|
|
675
|
+
// inherits it. (#1681 review, finding 1)
|
|
676
|
+
agent_id: input.agentId,
|
|
677
|
+
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
678
|
+
reason: redactSecrets(input.reason),
|
|
679
|
+
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
680
|
+
};
|
|
681
|
+
const binDir = join(input.directory, "bin");
|
|
682
|
+
await mkdir(binDir, { recursive: true });
|
|
683
|
+
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
684
|
+
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
685
|
+
await chmod(wrapperPath, 493);
|
|
686
|
+
const record = JSON.stringify(info, null, 2) + "\n";
|
|
687
|
+
await writeFile(join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
|
|
688
|
+
const root = input.tombstonesDir ?? defaultTombstonesDir();
|
|
689
|
+
const recordPath = join(root, `${info.agent_id}.json`);
|
|
690
|
+
try {
|
|
691
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
692
|
+
await writeFile(recordPath, record, { mode: MIRROR_MODE });
|
|
693
|
+
} catch (err) {
|
|
694
|
+
const code = err?.code;
|
|
695
|
+
return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
|
|
206
696
|
}
|
|
697
|
+
return { ...info, recordPath };
|
|
207
698
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
codexSigner: "haven_signer",
|
|
216
|
-
hermesEnvKey: "MCP_HAVEN_API_KEY"
|
|
217
|
-
};
|
|
699
|
+
async function readAgentTombstone(directory) {
|
|
700
|
+
try {
|
|
701
|
+
const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
702
|
+
if (typeof parsed?.agent_id !== "string") return null;
|
|
703
|
+
return parsed;
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
218
706
|
}
|
|
219
|
-
assertValidServerSlug(slug);
|
|
220
|
-
return {
|
|
221
|
-
hosted: `haven-${slug}`,
|
|
222
|
-
signer: `haven-signer-${slug}`,
|
|
223
|
-
// Hyphens are valid TOML bare keys, so named Codex tables match the
|
|
224
|
-
// JSON/YAML names instead of inheriting the legacy underscore.
|
|
225
|
-
codexHosted: `haven-${slug}`,
|
|
226
|
-
codexSigner: `haven-signer-${slug}`,
|
|
227
|
-
hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
|
|
228
|
-
};
|
|
229
707
|
}
|
|
230
|
-
var
|
|
231
|
-
var
|
|
232
|
-
"src/
|
|
233
|
-
|
|
708
|
+
var TOMBSTONE_FILENAME, ROOT_TOMBSTONES_DIRNAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
709
|
+
var init_tombstone = __esm({
|
|
710
|
+
"src/tombstone.ts"() {
|
|
711
|
+
init_redact();
|
|
712
|
+
init_connect_error();
|
|
713
|
+
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
714
|
+
ROOT_TOMBSTONES_DIRNAME = ".tombstones";
|
|
715
|
+
MIRROR_MODE = 384;
|
|
716
|
+
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
234
717
|
}
|
|
235
718
|
});
|
|
719
|
+
|
|
720
|
+
// src/storage.ts
|
|
721
|
+
var storage_exports = {};
|
|
722
|
+
__export(storage_exports, {
|
|
723
|
+
CONNECT_OUTCOME_FILENAME: () => CONNECT_OUTCOME_FILENAME,
|
|
724
|
+
MCP_SERVER_BINDING_FILENAME: () => MCP_SERVER_BINDING_FILENAME,
|
|
725
|
+
REKEY_PENDING_FILENAME: () => REKEY_PENDING_FILENAME,
|
|
726
|
+
REKEY_PENDING_TTL_MS: () => REKEY_PENDING_TTL_MS,
|
|
727
|
+
assertDirectoryRecordsSlug: () => assertDirectoryRecordsSlug,
|
|
728
|
+
assertServerSlugAvailable: () => assertServerSlugAvailable,
|
|
729
|
+
clearMcpServerBinding: () => clearMcpServerBinding,
|
|
730
|
+
clearRekeyPending: () => clearRekeyPending,
|
|
731
|
+
defaultAgentDirectory: () => defaultAgentDirectory,
|
|
732
|
+
defaultCredentialRoot: () => defaultCredentialRoot,
|
|
733
|
+
inspectRekeyPending: () => inspectRekeyPending,
|
|
734
|
+
listMcpServerBindings: () => listMcpServerBindings,
|
|
735
|
+
preflightCredentialStorage: () => preflightCredentialStorage,
|
|
736
|
+
readConnectOutcomeRuntime: () => readConnectOutcomeRuntime,
|
|
737
|
+
readMcpServerBinding: () => readMcpServerBinding,
|
|
738
|
+
readRekeyPending: () => readRekeyPending,
|
|
739
|
+
readStoredAccountAddress: () => readStoredAccountAddress,
|
|
740
|
+
readStoredCredentials: () => readStoredCredentials,
|
|
741
|
+
resolveAgentDirectoryBySlug: () => resolveAgentDirectoryBySlug,
|
|
742
|
+
rewriteCredentialFiles: () => rewriteCredentialFiles,
|
|
743
|
+
writeConnectOutcomeRecord: () => writeConnectOutcomeRecord,
|
|
744
|
+
writeCredentialFiles: () => writeCredentialFiles,
|
|
745
|
+
writeMcpServerBinding: () => writeMcpServerBinding,
|
|
746
|
+
writeRekeyPending: () => writeRekeyPending
|
|
747
|
+
});
|
|
236
748
|
async function preflightCredentialStorage(input = {}) {
|
|
237
749
|
const directory = defaultCredentialRoot(input.baseDir);
|
|
238
750
|
await mkdir(directory, { recursive: true, mode: 448 });
|
|
@@ -309,8 +821,7 @@ function agentPayload(input) {
|
|
|
309
821
|
};
|
|
310
822
|
}
|
|
311
823
|
async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
312
|
-
const
|
|
313
|
-
const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
|
|
824
|
+
const directory = serverName ? await resolveAgentDirectoryBySlug(serverName, { root: baseDir }) : agentIdOrSlug ? defaultAgentDirectory(agentIdOrSlug, baseDir) : await discoverSoleAgentDirectory(baseDir);
|
|
314
825
|
const identity = await readJsonFile(join(directory, "identity.json"));
|
|
315
826
|
if (!identity) {
|
|
316
827
|
throw new Error(
|
|
@@ -318,7 +829,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
|
318
829
|
);
|
|
319
830
|
}
|
|
320
831
|
let discoveredSlug;
|
|
321
|
-
if (!
|
|
832
|
+
if (!serverName && !agentIdOrSlug) {
|
|
322
833
|
const binding = await readJsonFile(join(directory, "mcp-server-binding.json"));
|
|
323
834
|
const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
|
|
324
835
|
if (hostedName && hostedName.startsWith("haven-")) {
|
|
@@ -389,6 +900,65 @@ async function discoverSoleAgentDirectory(baseDir) {
|
|
|
389
900
|
` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
|
|
390
901
|
);
|
|
391
902
|
}
|
|
903
|
+
async function assertDirectoryRecordsSlug(directory, slug) {
|
|
904
|
+
assertValidServerSlug(slug);
|
|
905
|
+
const sidecar = await readRuntimeSidecar(directory);
|
|
906
|
+
const bindingRecord = await readJsonFile(join(directory, "mcp-server-binding.json"));
|
|
907
|
+
const bindingSlug = bindingRecord ? asString(bindingRecord.server_name) : void 0;
|
|
908
|
+
const claimed = sidecar?.server_name ?? bindingSlug;
|
|
909
|
+
if (claimed === slug) return;
|
|
910
|
+
if (!claimed && basename(directory) === slug) return;
|
|
911
|
+
throw new Error(
|
|
912
|
+
`--name ${slug} and --credentials-dir ${directory} disagree: that directory records ` + (claimed ? `the pair 'haven-${claimed}'` : "no named pair") + `. Pass one of them, not both \u2014 or point --credentials-dir at the directory that records 'haven-${slug}'.`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
async function resolveAgentDirectoryBySlug(slug, opts = {}) {
|
|
916
|
+
assertValidServerSlug(slug);
|
|
917
|
+
const root = defaultCredentialRoot(opts.root);
|
|
918
|
+
let entries = [];
|
|
919
|
+
try {
|
|
920
|
+
entries = await readdir(root);
|
|
921
|
+
} catch {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`No agent directories under ${root} \u2014 nothing records the pair 'haven-${slug}'. Connect this agent first, or pass --credentials-dir <path> for the exact directory.`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const claims = [];
|
|
927
|
+
const recordedSlugs = [];
|
|
928
|
+
let barePairCount = 0;
|
|
929
|
+
let legacyFolder;
|
|
930
|
+
for (const entry of entries.sort()) {
|
|
931
|
+
const directory = join(root, entry);
|
|
932
|
+
const retired = await readJsonFile(join(directory, TOMBSTONE_FILENAME)) !== null;
|
|
933
|
+
const sidecar = await readRuntimeSidecar(directory);
|
|
934
|
+
const bindingRecord = await readJsonFile(join(directory, "mcp-server-binding.json"));
|
|
935
|
+
const bindingSlug = bindingRecord ? asString(bindingRecord.server_name) : void 0;
|
|
936
|
+
const claimedSlug = sidecar?.server_name ?? bindingSlug;
|
|
937
|
+
if (claimedSlug && !recordedSlugs.includes(claimedSlug)) recordedSlugs.push(claimedSlug);
|
|
938
|
+
if (sidecar && !sidecar.server_name) barePairCount++;
|
|
939
|
+
if (retired && !opts.allowRetired) continue;
|
|
940
|
+
if (claimedSlug === slug) {
|
|
941
|
+
const identity = await readJsonFile(join(directory, "identity.json"));
|
|
942
|
+
claims.push({ directory, agentId: identity ? asString(identity.agent_id) ?? null : null, retired });
|
|
943
|
+
}
|
|
944
|
+
if (!claimedSlug && !retired && entry === slug) legacyFolder = directory;
|
|
945
|
+
}
|
|
946
|
+
if (claims.length > 1) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`${claims.length} agent directories on this machine record the pair 'haven-${slug}' \u2014 the MCP server name changed hands (mcp_server_name_rebound):
|
|
949
|
+
` + claims.map((c) => ` \u2022 ${c.agentId ? `agent ${c.agentId}` : "agent (no agent_id recorded)"} \u2014 ${c.directory}${c.retired ? " (retired)" : ""}`).join("\n") + "\nRetire the earlier one with --unwire, or name the directory exactly with --credentials-dir <path>. Nothing was changed."
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (claims.length === 1) return claims[0].directory;
|
|
953
|
+
if (legacyFolder) return legacyFolder;
|
|
954
|
+
const known = [
|
|
955
|
+
...recordedSlugs.map((s) => `'haven-${s}'`),
|
|
956
|
+
...barePairCount > 0 ? [`${barePairCount} unnamed 'haven' / 'haven-signer' pair${barePairCount === 1 ? "" : "s"} (no --name)`] : []
|
|
957
|
+
];
|
|
958
|
+
throw new Error(
|
|
959
|
+
`No agent directory on this machine records the pair 'haven-${slug}'.` + (known.length > 0 ? ` Directories under ${root} record: ${known.join(", ")}.` : ` Nothing under ${root} records a named pair.`) + " Check the agent id against the Haven agent page, or name the directory exactly with --credentials-dir <path>."
|
|
960
|
+
);
|
|
961
|
+
}
|
|
392
962
|
async function rewriteCredentialFiles(input) {
|
|
393
963
|
const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
|
|
394
964
|
const identityPath = join(directory, "identity.json");
|
|
@@ -598,6 +1168,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
|
598
1168
|
const out = [];
|
|
599
1169
|
for (const entry of entries) {
|
|
600
1170
|
const directory = join(root, entry);
|
|
1171
|
+
if (excludeDirectory && directory === excludeDirectory) continue;
|
|
601
1172
|
const binding = await readMcpServerBinding(directory);
|
|
602
1173
|
if (binding) out.push({ directory, binding });
|
|
603
1174
|
}
|
|
@@ -607,48 +1178,12 @@ var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_
|
|
|
607
1178
|
var init_storage = __esm({
|
|
608
1179
|
"src/storage.ts"() {
|
|
609
1180
|
init_server_names();
|
|
1181
|
+
init_signer_runtime();
|
|
1182
|
+
init_tombstone();
|
|
610
1183
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
611
1184
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
612
1185
|
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
613
|
-
MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
function mcpPackageSpec() {
|
|
617
|
-
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
618
|
-
}
|
|
619
|
-
function sdkPackageSpec() {
|
|
620
|
-
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
621
|
-
}
|
|
622
|
-
function signerPackageSpec() {
|
|
623
|
-
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
624
|
-
}
|
|
625
|
-
var MCP_RUNTIME_MANIFEST;
|
|
626
|
-
var init_runtime_manifest = __esm({
|
|
627
|
-
"src/runtime-manifest.ts"() {
|
|
628
|
-
MCP_RUNTIME_MANIFEST = {
|
|
629
|
-
mcpPackage: "@haven_ai/mcp",
|
|
630
|
-
mcpVersion: MCP_VERSION,
|
|
631
|
-
sdkPackage: "@haven_ai/sdk",
|
|
632
|
-
sdkVersion: "0.9.0-alpha.0",
|
|
633
|
-
signerPackage: "@haven_ai/signer",
|
|
634
|
-
signerVersion: "0.9.0-alpha.0",
|
|
635
|
-
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
636
|
-
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
637
|
-
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
638
|
-
// — including on the `--local` path where it does run. A hand-maintained
|
|
639
|
-
// second copy of a number is a drift waiting to happen; a guard test pins
|
|
640
|
-
// this against `package.json`'s `engines.node`.
|
|
641
|
-
minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
|
|
642
|
-
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
643
|
-
requiredTools: registeredToolNames(),
|
|
644
|
-
/**
|
|
645
|
-
* The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
|
|
646
|
-
* package (#1587) — same anti-drift rule as `requiredTools` above: a
|
|
647
|
-
* literal list here would rot the first time the signer gains a tool.
|
|
648
|
-
* The handshake probe requires all of them.
|
|
649
|
-
*/
|
|
650
|
-
requiredSignerTools: Object.keys(toolSchemas)
|
|
651
|
-
};
|
|
1186
|
+
MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
|
|
652
1187
|
}
|
|
653
1188
|
});
|
|
654
1189
|
async function writeRuntimeConfig(input, deps = {}) {
|
|
@@ -1685,295 +2220,23 @@ function parseJsonRpcPayload(raw) {
|
|
|
1685
2220
|
const dataLines = trimmed.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
|
|
1686
2221
|
for (let i = dataLines.length - 1; i >= 0; i -= 1) {
|
|
1687
2222
|
try {
|
|
1688
|
-
return JSON.parse(dataLines[i]);
|
|
1689
|
-
} catch {
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
async function fetchWithTimeout(fetchImpl, url, init) {
|
|
1695
|
-
const controller = new AbortController();
|
|
1696
|
-
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1697
|
-
try {
|
|
1698
|
-
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
1699
|
-
} finally {
|
|
1700
|
-
clearTimeout(timeout);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
var init_probes = __esm({
|
|
1704
|
-
"src/probes.ts"() {
|
|
1705
|
-
}
|
|
1706
|
-
});
|
|
1707
|
-
function resolveRuntimeSpecOverride(env) {
|
|
1708
|
-
const override = {};
|
|
1709
|
-
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
1710
|
-
const variable = RUNTIME_SPEC_ENV[pkg];
|
|
1711
|
-
const raw = env[variable];
|
|
1712
|
-
if (raw === void 0) continue;
|
|
1713
|
-
override[pkg] = validateSpec(variable, raw);
|
|
1714
|
-
}
|
|
1715
|
-
return Object.keys(override).length > 0 ? override : void 0;
|
|
1716
|
-
}
|
|
1717
|
-
function validateSpec(variable, raw) {
|
|
1718
|
-
if (raw.length === 0 || raw.trim().length === 0) {
|
|
1719
|
-
throw new RuntimeSpecOverrideError(variable, "it is empty");
|
|
1720
|
-
}
|
|
1721
|
-
if (/\s/.test(raw)) {
|
|
1722
|
-
throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
|
|
1723
|
-
}
|
|
1724
|
-
if (/[\u0000-\u001f\u007f]/.test(raw)) {
|
|
1725
|
-
throw new RuntimeSpecOverrideError(variable, "it contains a control character");
|
|
1726
|
-
}
|
|
1727
|
-
const meta = SHELL_METACHARACTERS.exec(raw);
|
|
1728
|
-
if (meta) {
|
|
1729
|
-
throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
|
|
1730
|
-
}
|
|
1731
|
-
return raw;
|
|
1732
|
-
}
|
|
1733
|
-
function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
|
|
1734
|
-
const digest = createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
|
|
1735
|
-
return `override-${digest.slice(0, 12)}`;
|
|
1736
|
-
}
|
|
1737
|
-
function overrideApplies(override, packages) {
|
|
1738
|
-
return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
|
|
1739
|
-
}
|
|
1740
|
-
function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
|
|
1741
|
-
const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
|
|
1742
|
-
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
1743
|
-
const spec = override[pkg];
|
|
1744
|
-
if (spec === void 0) continue;
|
|
1745
|
-
const pin = pinned[pkg];
|
|
1746
|
-
lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
|
|
1747
|
-
}
|
|
1748
|
-
lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
|
|
1749
|
-
lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
|
|
1750
|
-
return lines;
|
|
1751
|
-
}
|
|
1752
|
-
function describeRuntimeSpecOverride(override) {
|
|
1753
|
-
return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
|
|
1754
|
-
}
|
|
1755
|
-
var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
|
|
1756
|
-
var init_runtime_spec_override = __esm({
|
|
1757
|
-
"src/runtime-spec-override.ts"() {
|
|
1758
|
-
RUNTIME_SPEC_ENV = {
|
|
1759
|
-
signer: "HAVEN_SIGNER_SPEC",
|
|
1760
|
-
sdk: "HAVEN_SDK_SPEC",
|
|
1761
|
-
mcp: "HAVEN_MCP_SPEC"
|
|
1762
|
-
};
|
|
1763
|
-
SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
|
|
1764
|
-
RuntimeSpecOverrideError = class extends Error {
|
|
1765
|
-
code = "runtime_spec_override_invalid";
|
|
1766
|
-
variable;
|
|
1767
|
-
constructor(variable, reason) {
|
|
1768
|
-
super(`${variable} is set but not usable as an npm package spec: ${reason}. Unset it, or set it to a spec npm install accepts (file:/abs/path, a .tgz, or @haven_ai/<pkg>@<version>).`);
|
|
1769
|
-
this.name = "RuntimeSpecOverrideError";
|
|
1770
|
-
this.variable = variable;
|
|
1771
|
-
}
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
});
|
|
1775
|
-
async function prepareSignerRuntime(input, deps = {}) {
|
|
1776
|
-
const homeDir = input.homeDir ?? homedir();
|
|
1777
|
-
const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
|
|
1778
|
-
const signerSpec = override?.signer ?? signerPackageSpec();
|
|
1779
|
-
const sdkSpec = override?.sdk ?? sdkPackageSpec();
|
|
1780
|
-
const resolvedSpecs = [signerSpec, sdkSpec];
|
|
1781
|
-
const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
|
|
1782
|
-
const runtimeDirectory = resolve(
|
|
1783
|
-
homeDir,
|
|
1784
|
-
".haven",
|
|
1785
|
-
"signer-runtime",
|
|
1786
|
-
overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.signerVersion
|
|
1787
|
-
);
|
|
1788
|
-
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1789
|
-
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
|
|
1790
|
-
const messages = [];
|
|
1791
|
-
if (override) {
|
|
1792
|
-
messages.push(...runtimeSpecOverrideNotice(
|
|
1793
|
-
"signer runtime",
|
|
1794
|
-
override,
|
|
1795
|
-
{ signer: signerPackageSpec(), sdk: sdkPackageSpec() },
|
|
1796
|
-
runtimeDirectory
|
|
1797
|
-
));
|
|
1798
|
-
}
|
|
1799
|
-
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1800
|
-
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1801
|
-
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1802
|
-
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1803
|
-
if (override) {
|
|
1804
|
-
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1805
|
-
messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
|
|
1806
|
-
} else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
1807
|
-
messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1808
|
-
} else {
|
|
1809
|
-
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1810
|
-
messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1811
|
-
}
|
|
1812
|
-
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
1813
|
-
const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
|
|
1814
|
-
const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
|
|
1815
|
-
await writeWrapper({
|
|
1816
|
-
wrapperPath,
|
|
1817
|
-
cliPath,
|
|
1818
|
-
signerPath: input.signerPath,
|
|
1819
|
-
overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
|
|
1820
|
-
});
|
|
1821
|
-
await writeRuntimeSidecar({
|
|
1822
|
-
path: join(input.credentialDirectory, "signer-runtime.json"),
|
|
1823
|
-
wrapperPath,
|
|
1824
|
-
runtimeDirectory,
|
|
1825
|
-
npmCacheDirectory,
|
|
1826
|
-
cliPath,
|
|
1827
|
-
serverName: input.serverName,
|
|
1828
|
-
override: overrideRecord,
|
|
1829
|
-
installedVersions
|
|
1830
|
-
});
|
|
1831
|
-
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
1832
|
-
return {
|
|
1833
|
-
command: wrapperPath,
|
|
1834
|
-
args: [],
|
|
1835
|
-
wrapperPath,
|
|
1836
|
-
runtimeDirectory,
|
|
1837
|
-
npmCacheDirectory,
|
|
1838
|
-
cliPath,
|
|
1839
|
-
messages,
|
|
1840
|
-
...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
function resolveSignerRuntimeOverride(env) {
|
|
1844
|
-
const override = resolveRuntimeSpecOverride(env);
|
|
1845
|
-
if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
|
|
1846
|
-
const { signer, sdk } = override;
|
|
1847
|
-
return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
|
|
1848
|
-
}
|
|
1849
|
-
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
|
|
1850
|
-
const { runCommand, onProgress } = deps;
|
|
1851
|
-
const baseArgs = [
|
|
1852
|
-
"install",
|
|
1853
|
-
"--prefix",
|
|
1854
|
-
runtimeDirectory,
|
|
1855
|
-
"--no-audit",
|
|
1856
|
-
"--no-fund",
|
|
1857
|
-
"--omit=dev",
|
|
1858
|
-
"--prefer-offline",
|
|
1859
|
-
...packageSpecs
|
|
1860
|
-
];
|
|
1861
|
-
const run = async (args) => {
|
|
1862
|
-
const startedAt = Date.now();
|
|
1863
|
-
const heartbeat = setInterval(() => {
|
|
1864
|
-
const seconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
1865
|
-
onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
|
|
1866
|
-
}, SIGNER_INSTALL_HEARTBEAT_MS);
|
|
1867
|
-
heartbeat.unref?.();
|
|
1868
|
-
try {
|
|
1869
|
-
if (runCommand) await runCommand("npm", args);
|
|
1870
|
-
else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
|
|
1871
|
-
} finally {
|
|
1872
|
-
clearInterval(heartbeat);
|
|
1873
|
-
}
|
|
1874
|
-
};
|
|
1875
|
-
try {
|
|
1876
|
-
await run(baseArgs);
|
|
1877
|
-
} catch {
|
|
1878
|
-
try {
|
|
1879
|
-
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1880
|
-
} catch (err) {
|
|
1881
|
-
throw new Error(
|
|
1882
|
-
`Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
1888
|
-
return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
|
|
1889
|
-
signerVersion: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1890
|
-
sdkVersion: MCP_RUNTIME_MANIFEST.sdkVersion
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
|
|
1894
|
-
try {
|
|
1895
|
-
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
1896
|
-
const installed = await readInstalledVersions(runtimeDirectory);
|
|
1897
|
-
return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
|
|
1898
|
-
} catch {
|
|
1899
|
-
return false;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
async function readInstalledVersions(runtimeDirectory) {
|
|
1903
|
-
const [signerPackage, sdkPackage] = await Promise.all([
|
|
1904
|
-
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
|
|
1905
|
-
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1906
|
-
]);
|
|
1907
|
-
return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
|
|
1908
|
-
}
|
|
1909
|
-
async function readPackageJson(path) {
|
|
1910
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
1911
|
-
}
|
|
1912
|
-
async function writeWrapper(input) {
|
|
1913
|
-
await mkdir(dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
1914
|
-
await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1915
|
-
const source = [
|
|
1916
|
-
"#!/usr/bin/env node",
|
|
1917
|
-
...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
|
|
1918
|
-
"import { spawn } from 'node:child_process'",
|
|
1919
|
-
"",
|
|
1920
|
-
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1921
|
-
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1922
|
-
"",
|
|
1923
|
-
"const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
|
|
1924
|
-
" stdio: 'inherit',",
|
|
1925
|
-
"})",
|
|
1926
|
-
"",
|
|
1927
|
-
"child.on('exit', (code, signal) => {",
|
|
1928
|
-
" if (signal) process.kill(process.pid, signal)",
|
|
1929
|
-
" else process.exit(code ?? 1)",
|
|
1930
|
-
"})",
|
|
1931
|
-
""
|
|
1932
|
-
].join("\n");
|
|
1933
|
-
await writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1934
|
-
await chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1935
|
-
}
|
|
1936
|
-
async function readRuntimeSidecar(credentialDirectory) {
|
|
1937
|
-
try {
|
|
1938
|
-
return JSON.parse(
|
|
1939
|
-
await readFile(join(credentialDirectory, "signer-runtime.json"), "utf8")
|
|
1940
|
-
);
|
|
1941
|
-
} catch {
|
|
1942
|
-
return null;
|
|
2223
|
+
return JSON.parse(dataLines[i]);
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
1943
2226
|
}
|
|
2227
|
+
return null;
|
|
1944
2228
|
}
|
|
1945
|
-
async function
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1949
|
-
signer_version: input.installedVersions?.signerVersion ?? MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1950
|
-
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1951
|
-
sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1952
|
-
wrapper_path: input.wrapperPath,
|
|
1953
|
-
runtime_directory: input.runtimeDirectory,
|
|
1954
|
-
npm_cache_directory: input.npmCacheDirectory,
|
|
1955
|
-
cli_path: input.cliPath,
|
|
1956
|
-
...input.override ? { runtime_spec_override: input.override } : {}
|
|
1957
|
-
};
|
|
1958
|
-
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1959
|
-
`, { mode: 384 });
|
|
1960
|
-
await chmod(input.path, 384).catch(() => void 0);
|
|
1961
|
-
}
|
|
1962
|
-
async function assertFileExists(path, label) {
|
|
2229
|
+
async function fetchWithTimeout(fetchImpl, url, init) {
|
|
2230
|
+
const controller = new AbortController();
|
|
2231
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1963
2232
|
try {
|
|
1964
|
-
await
|
|
1965
|
-
}
|
|
1966
|
-
|
|
2233
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
2234
|
+
} finally {
|
|
2235
|
+
clearTimeout(timeout);
|
|
1967
2236
|
}
|
|
1968
2237
|
}
|
|
1969
|
-
var
|
|
1970
|
-
|
|
1971
|
-
"src/signer-runtime.ts"() {
|
|
1972
|
-
init_runtime_manifest();
|
|
1973
|
-
init_runtime_spec_override();
|
|
1974
|
-
execFileAsync = promisify(execFile);
|
|
1975
|
-
SIGNER_INSTALL_TIMEOUT_MS = 6e5;
|
|
1976
|
-
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
2238
|
+
var init_probes = __esm({
|
|
2239
|
+
"src/probes.ts"() {
|
|
1977
2240
|
}
|
|
1978
2241
|
});
|
|
1979
2242
|
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
@@ -2291,28 +2554,6 @@ var init_skill_install = __esm({
|
|
|
2291
2554
|
}
|
|
2292
2555
|
});
|
|
2293
2556
|
|
|
2294
|
-
// src/connect-error.ts
|
|
2295
|
-
function isConnectError(err) {
|
|
2296
|
-
return err instanceof ConnectError;
|
|
2297
|
-
}
|
|
2298
|
-
var ConnectError;
|
|
2299
|
-
var init_connect_error = __esm({
|
|
2300
|
-
"src/connect-error.ts"() {
|
|
2301
|
-
ConnectError = class extends Error {
|
|
2302
|
-
code;
|
|
2303
|
-
nextAction;
|
|
2304
|
-
details;
|
|
2305
|
-
constructor(code, message, nextAction2, details = {}) {
|
|
2306
|
-
super(message);
|
|
2307
|
-
this.name = "ConnectError";
|
|
2308
|
-
this.code = code;
|
|
2309
|
-
this.nextAction = nextAction2;
|
|
2310
|
-
this.details = details;
|
|
2311
|
-
}
|
|
2312
|
-
};
|
|
2313
|
-
}
|
|
2314
|
-
});
|
|
2315
|
-
|
|
2316
2557
|
// src/runtime-registry.ts
|
|
2317
2558
|
function runtimeProfile(runtime, env = process.env) {
|
|
2318
2559
|
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
@@ -3234,138 +3475,6 @@ var init_installed_clients = __esm({
|
|
|
3234
3475
|
MAX_PROMPT_ATTEMPTS = 3;
|
|
3235
3476
|
}
|
|
3236
3477
|
});
|
|
3237
|
-
|
|
3238
|
-
// src/tombstone.ts
|
|
3239
|
-
var tombstone_exports = {};
|
|
3240
|
-
__export(tombstone_exports, {
|
|
3241
|
-
ROOT_TOMBSTONES_DIRNAME: () => ROOT_TOMBSTONES_DIRNAME,
|
|
3242
|
-
TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
|
|
3243
|
-
TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
|
|
3244
|
-
defaultTombstonesDir: () => defaultTombstonesDir,
|
|
3245
|
-
readAgentTombstone: () => readAgentTombstone,
|
|
3246
|
-
readTombstoneRecords: () => readTombstoneRecords,
|
|
3247
|
-
tombstonesDirForAgentDirectory: () => tombstonesDirForAgentDirectory,
|
|
3248
|
-
tombstonesDirForCredentialRoot: () => tombstonesDirForCredentialRoot,
|
|
3249
|
-
writeAgentTombstone: () => writeAgentTombstone
|
|
3250
|
-
});
|
|
3251
|
-
function defaultTombstonesDir(baseDir) {
|
|
3252
|
-
return join(baseDir ?? join(homedir(), ".haven"), "tombstones");
|
|
3253
|
-
}
|
|
3254
|
-
function tombstonesDirForCredentialRoot(credentialRoot, homeDir = homedir()) {
|
|
3255
|
-
if (!credentialRoot) return defaultTombstonesDir(join(homeDir, ".haven"));
|
|
3256
|
-
const root = resolve(credentialRoot);
|
|
3257
|
-
if (root === resolve(homeDir, ".haven", "agents")) return defaultTombstonesDir(join(homeDir, ".haven"));
|
|
3258
|
-
return join(root, ROOT_TOMBSTONES_DIRNAME);
|
|
3259
|
-
}
|
|
3260
|
-
function tombstonesDirForAgentDirectory(directory, homeDir = homedir()) {
|
|
3261
|
-
return tombstonesDirForCredentialRoot(dirname(resolve(directory)), homeDir);
|
|
3262
|
-
}
|
|
3263
|
-
async function readTombstoneRecords(tombstonesDir) {
|
|
3264
|
-
const root = tombstonesDir ?? defaultTombstonesDir();
|
|
3265
|
-
let entries = [];
|
|
3266
|
-
try {
|
|
3267
|
-
entries = await readdir(root);
|
|
3268
|
-
} catch {
|
|
3269
|
-
return [];
|
|
3270
|
-
}
|
|
3271
|
-
const records = [];
|
|
3272
|
-
for (const entry of entries) {
|
|
3273
|
-
if (!entry.endsWith(".json")) continue;
|
|
3274
|
-
const recordPath = join(root, entry);
|
|
3275
|
-
try {
|
|
3276
|
-
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
3277
|
-
if (typeof parsed?.agent_id === "string") {
|
|
3278
|
-
records.push({ ...parsed, recordPath });
|
|
3279
|
-
}
|
|
3280
|
-
} catch {
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
return records;
|
|
3284
|
-
}
|
|
3285
|
-
function tombstoneScript(info) {
|
|
3286
|
-
const lines = [
|
|
3287
|
-
`${TOMBSTONE_MARKER}: this Haven agent was retired.`,
|
|
3288
|
-
"",
|
|
3289
|
-
` agent: ${info.agent_id}`,
|
|
3290
|
-
` retired at: ${info.retired_at}`,
|
|
3291
|
-
` reason: ${info.reason}`,
|
|
3292
|
-
...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
|
|
3293
|
-
"",
|
|
3294
|
-
"This process is running with a wiring snapshot that predates the",
|
|
3295
|
-
"retirement \u2014 it loaded its MCP config at startup and has kept it since.",
|
|
3296
|
-
"Restart THIS host to pick up the current wiring. If several long-lived",
|
|
3297
|
-
"hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
|
|
3298
|
-
"one of them: each holds the snapshot from its own start time, so after",
|
|
3299
|
-
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
3300
|
-
"",
|
|
3301
|
-
`Then verify with: ${connectorRerunCommand("--doctor --runtime <runtime>")}`
|
|
3302
|
-
];
|
|
3303
|
-
return [
|
|
3304
|
-
"#!/usr/bin/env node",
|
|
3305
|
-
`// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
|
|
3306
|
-
"// once every long-lived MCP host on this machine has been restarted.",
|
|
3307
|
-
`process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
|
|
3308
|
-
"process.exit(1)",
|
|
3309
|
-
""
|
|
3310
|
-
].join("\n");
|
|
3311
|
-
}
|
|
3312
|
-
async function writeAgentTombstone(input) {
|
|
3313
|
-
const dirStat = await stat(input.directory).catch(() => null);
|
|
3314
|
-
if (!dirStat?.isDirectory()) {
|
|
3315
|
-
throw new ConnectError(
|
|
3316
|
-
"tombstone_directory_not_found",
|
|
3317
|
-
`Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
|
|
3318
|
-
"retry_with_an_existing_agent_directory"
|
|
3319
|
-
);
|
|
3320
|
-
}
|
|
3321
|
-
const info = {
|
|
3322
|
-
// reason / replaced_by are persisted to disk and re-emitted to the host's
|
|
3323
|
-
// MCP stderr log on EVERY stale probe, potentially for months — redact
|
|
3324
|
-
// like every other output path, at the write layer so any future caller
|
|
3325
|
-
// inherits it. (#1681 review, finding 1)
|
|
3326
|
-
agent_id: input.agentId,
|
|
3327
|
-
retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3328
|
-
reason: redactSecrets(input.reason),
|
|
3329
|
-
...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
|
|
3330
|
-
};
|
|
3331
|
-
const binDir = join(input.directory, "bin");
|
|
3332
|
-
await mkdir(binDir, { recursive: true });
|
|
3333
|
-
const wrapperPath = join(binDir, "haven-signer.mjs");
|
|
3334
|
-
await writeFile(wrapperPath, tombstoneScript(info), "utf8");
|
|
3335
|
-
await chmod(wrapperPath, 493);
|
|
3336
|
-
const record = JSON.stringify(info, null, 2) + "\n";
|
|
3337
|
-
await writeFile(join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
|
|
3338
|
-
const root = input.tombstonesDir ?? defaultTombstonesDir();
|
|
3339
|
-
const recordPath = join(root, `${info.agent_id}.json`);
|
|
3340
|
-
try {
|
|
3341
|
-
await mkdir(root, { recursive: true, mode: 448 });
|
|
3342
|
-
await writeFile(recordPath, record, { mode: MIRROR_MODE });
|
|
3343
|
-
} catch (err) {
|
|
3344
|
-
const code = err?.code;
|
|
3345
|
-
return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
|
|
3346
|
-
}
|
|
3347
|
-
return { ...info, recordPath };
|
|
3348
|
-
}
|
|
3349
|
-
async function readAgentTombstone(directory) {
|
|
3350
|
-
try {
|
|
3351
|
-
const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
|
|
3352
|
-
if (typeof parsed?.agent_id !== "string") return null;
|
|
3353
|
-
return parsed;
|
|
3354
|
-
} catch {
|
|
3355
|
-
return null;
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
var TOMBSTONE_FILENAME, ROOT_TOMBSTONES_DIRNAME, MIRROR_MODE, TOMBSTONE_MARKER;
|
|
3359
|
-
var init_tombstone = __esm({
|
|
3360
|
-
"src/tombstone.ts"() {
|
|
3361
|
-
init_redact();
|
|
3362
|
-
init_connect_error();
|
|
3363
|
-
TOMBSTONE_FILENAME = "TOMBSTONE.json";
|
|
3364
|
-
ROOT_TOMBSTONES_DIRNAME = ".tombstones";
|
|
3365
|
-
MIRROR_MODE = 384;
|
|
3366
|
-
TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
|
|
3367
|
-
}
|
|
3368
|
-
});
|
|
3369
3478
|
async function detectWiringCollision(input) {
|
|
3370
3479
|
const root = defaultCredentialRoot(input.credentialsDir);
|
|
3371
3480
|
let entries = [];
|
|
@@ -4081,10 +4190,10 @@ async function executeConnect(options, deps, trace) {
|
|
|
4081
4190
|
} else if (supersededAgentIds.length > 0) {
|
|
4082
4191
|
log("");
|
|
4083
4192
|
log(
|
|
4084
|
-
`Heads-up:
|
|
4193
|
+
`Heads-up: other agent directories on this machine \u2014 ${supersededAgentIds.join(", ")}. This setup created a NEW agent and revoked none of them; any not revoked in Haven keep their own keys and any budgets.`
|
|
4085
4194
|
);
|
|
4086
4195
|
log(
|
|
4087
|
-
`
|
|
4196
|
+
`To retire one you no longer use: use Remove agent\u2026 on its Haven agent page (it ends its live budget), restart any long-lived host (gateways, TUI workers, editors) still wired to it \u2014 each holds the MCP wiring snapshot from its own start time \u2014 and remove its directory under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in its place). To rewire the bare haven / haven-signer pair instead of adding an agent, re-run with --replace. Run ${RERUN_HINT} --doctor to check which keys are still live.`
|
|
4088
4197
|
);
|
|
4089
4198
|
}
|
|
4090
4199
|
} catch {
|
|
@@ -4115,6 +4224,18 @@ async function executeConnect(options, deps, trace) {
|
|
|
4115
4224
|
} catch (err) {
|
|
4116
4225
|
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
4117
4226
|
}
|
|
4227
|
+
const reportableRetiredIds = [...new Set(retiredAgentIds.filter((id) => UUID_SHAPE.test(id)))];
|
|
4228
|
+
if (reportableRetiredIds.length > 0 && api.reportRetiredAgents) {
|
|
4229
|
+
try {
|
|
4230
|
+
await api.reportRetiredAgents({
|
|
4231
|
+
setupId: registration.setup_id,
|
|
4232
|
+
apiKey: localApiKey,
|
|
4233
|
+
retiredAgentIds: reportableRetiredIds
|
|
4234
|
+
});
|
|
4235
|
+
} catch (err) {
|
|
4236
|
+
log(`Could not tell Haven which agents were retired: ${err instanceof Error ? err.message : String(err)}`);
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4118
4239
|
let approval;
|
|
4119
4240
|
const narrateApprovalWait = options.waitForApproval === true || options.waitForApproval === void 0 && (deps.isStdoutTty ?? Boolean(process.stdout.isTTY));
|
|
4120
4241
|
if (narrateApprovalWait && !runtimeInstall.errorCode) {
|
|
@@ -4557,7 +4678,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
|
4557
4678
|
function printNextSteps(result, log, approval, approvalUrl) {
|
|
4558
4679
|
for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
|
|
4559
4680
|
}
|
|
4560
|
-
var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4681
|
+
var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, UUID_SHAPE, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4561
4682
|
var init_runtime = __esm({
|
|
4562
4683
|
"src/runtime.ts"() {
|
|
4563
4684
|
init_api();
|
|
@@ -4574,8 +4695,9 @@ var init_runtime = __esm({
|
|
|
4574
4695
|
init_tombstone();
|
|
4575
4696
|
init_local_mcp_runtime();
|
|
4576
4697
|
init_runtime_manifest();
|
|
4577
|
-
CONNECTOR_VERSION = "0.
|
|
4698
|
+
CONNECTOR_VERSION = "0.10.0-alpha.0";
|
|
4578
4699
|
PRODUCTION_CHAIN_ID = 8453;
|
|
4700
|
+
UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4579
4701
|
CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
|
|
4580
4702
|
CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4581
4703
|
failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
@@ -5056,6 +5178,40 @@ function rollUpLevel(checks) {
|
|
|
5056
5178
|
if (checks.some((check) => check.level === "advisory")) return "advisory";
|
|
5057
5179
|
return "ok";
|
|
5058
5180
|
}
|
|
5181
|
+
function runWrapperAck(wrapperPath, timeoutMs = 3e4) {
|
|
5182
|
+
return new Promise((resolve11, reject) => {
|
|
5183
|
+
const child = spawn(wrapperPath, ["--ack"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
5184
|
+
let stdout = "";
|
|
5185
|
+
let settled = false;
|
|
5186
|
+
const timer = setTimeout(() => {
|
|
5187
|
+
if (settled) return;
|
|
5188
|
+
settled = true;
|
|
5189
|
+
child.kill();
|
|
5190
|
+
reject(new Error(`signer --ack timed out after ${Math.round(timeoutMs / 1e3)}s`));
|
|
5191
|
+
}, timeoutMs);
|
|
5192
|
+
child.stdout?.on("data", (chunk) => {
|
|
5193
|
+
stdout += chunk.toString("utf8");
|
|
5194
|
+
});
|
|
5195
|
+
child.on("error", (err) => {
|
|
5196
|
+
if (settled) return;
|
|
5197
|
+
settled = true;
|
|
5198
|
+
clearTimeout(timer);
|
|
5199
|
+
reject(err);
|
|
5200
|
+
});
|
|
5201
|
+
child.on("close", (code) => {
|
|
5202
|
+
if (settled) return;
|
|
5203
|
+
settled = true;
|
|
5204
|
+
clearTimeout(timer);
|
|
5205
|
+
if (code === 0) resolve11({ stdout });
|
|
5206
|
+
else reject(new Error(`signer --ack exited ${code ?? "abnormally"}`));
|
|
5207
|
+
});
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
function repairCommandNamedFor(runtime, slug) {
|
|
5211
|
+
const nameFlag = ` --name ${slug}`;
|
|
5212
|
+
if (normalizeRuntimeName(runtime)) return `Run: ${RERUN} --doctor --repair --runtime ${runtime}${nameFlag}`;
|
|
5213
|
+
return `Run: ${RERUN} --doctor --repair --runtime <runtime>${nameFlag} \u2014 --repair needs the runtime named; one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`;
|
|
5214
|
+
}
|
|
5059
5215
|
function runtimeFlagFor(runtime) {
|
|
5060
5216
|
return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
|
|
5061
5217
|
}
|
|
@@ -5137,6 +5293,35 @@ async function discoverCredentialDirectory(homeDir, explicit) {
|
|
|
5137
5293
|
identityDirectories
|
|
5138
5294
|
};
|
|
5139
5295
|
}
|
|
5296
|
+
async function readClaudeCodeWiringEvidence(homeDir) {
|
|
5297
|
+
let parsed;
|
|
5298
|
+
try {
|
|
5299
|
+
parsed = JSON.parse(await readFile(join(homeDir, ".claude.json"), "utf8"));
|
|
5300
|
+
} catch {
|
|
5301
|
+
return null;
|
|
5302
|
+
}
|
|
5303
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
5304
|
+
const blocks = [parsed.mcpServers];
|
|
5305
|
+
const projects = parsed.projects;
|
|
5306
|
+
if (typeof projects === "object" && projects !== null) {
|
|
5307
|
+
for (const project of Object.values(projects)) {
|
|
5308
|
+
if (typeof project === "object" && project !== null) blocks.push(project.mcpServers);
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
const lines = [];
|
|
5312
|
+
for (const block of blocks) {
|
|
5313
|
+
if (typeof block !== "object" || block === null) continue;
|
|
5314
|
+
for (const [name, server] of Object.entries(block)) {
|
|
5315
|
+
const entry = typeof server === "object" && server !== null ? server : {};
|
|
5316
|
+
const parts = [JSON.stringify(name) + ":"];
|
|
5317
|
+
if (typeof entry.command === "string") parts.push(entry.command);
|
|
5318
|
+
if (Array.isArray(entry.args)) parts.push(...entry.args.filter((arg) => typeof arg === "string"));
|
|
5319
|
+
if (typeof entry.url === "string") parts.push(entry.url);
|
|
5320
|
+
lines.push(parts.join(" "));
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
5324
|
+
}
|
|
5140
5325
|
function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
|
|
5141
5326
|
if (configText === null) return isPrimary;
|
|
5142
5327
|
if (slug) {
|
|
@@ -5382,8 +5567,11 @@ async function verdictsForAgent(entry, input, deps) {
|
|
|
5382
5567
|
id: "signer_process",
|
|
5383
5568
|
label: "Signer stdio handshake",
|
|
5384
5569
|
level: "failed",
|
|
5385
|
-
|
|
5386
|
-
|
|
5570
|
+
// #3799: honest about what the mismatch does and does not prove. The
|
|
5571
|
+
// stored ack covers a tool SET; a doctor bundled with a newer signer
|
|
5572
|
+
// reports exactly this against an older, still-working install.
|
|
5573
|
+
detail: "The stored local-tools consent does not match the tool set this connector bundles \u2014 by design a signer with a different tool set refuses to start. This does not prove the currently running signer is broken: a doctor newer than the installed signer reports exactly this mismatch.",
|
|
5574
|
+
repair: `Run: ${sidecar.wrapper_path} --ack < /dev/null \u2014 re-acknowledge consent with the installed signer, no setup token needed. Or re-run repair with --ack-local-tools: ${repairCommandFor(input.runtime, " --ack-local-tools", input.repairDir)}.`
|
|
5387
5575
|
});
|
|
5388
5576
|
} else {
|
|
5389
5577
|
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
@@ -5414,12 +5602,21 @@ async function verdictsForAgent(entry, input, deps) {
|
|
|
5414
5602
|
}
|
|
5415
5603
|
return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
|
|
5416
5604
|
}
|
|
5605
|
+
async function resolveNamedDirectory(input, homeDir) {
|
|
5606
|
+
if (!input.serverName) return input.credentialsDir;
|
|
5607
|
+
if (input.credentialsDir) {
|
|
5608
|
+
await assertDirectoryRecordsSlug(input.credentialsDir, input.serverName);
|
|
5609
|
+
return input.credentialsDir;
|
|
5610
|
+
}
|
|
5611
|
+
return resolveAgentDirectoryBySlug(input.serverName, { root: join(homeDir, ".haven", "agents") });
|
|
5612
|
+
}
|
|
5417
5613
|
async function runDoctor(input, deps = {}) {
|
|
5418
5614
|
const homeDir = deps.homeDir ?? homedir();
|
|
5419
5615
|
const checks = [];
|
|
5420
5616
|
let signerCapabilities;
|
|
5421
|
-
const
|
|
5422
|
-
const
|
|
5617
|
+
const namedDir = await resolveNamedDirectory(input, homeDir);
|
|
5618
|
+
const { directory, others, parkedOnly, identityDirectories } = await discoverCredentialDirectory(homeDir, namedDir);
|
|
5619
|
+
const nameRepairDir = identityDirectories.length > 1 || Boolean(namedDir);
|
|
5423
5620
|
const resolution = await resolveDoctorRuntime(input, directory);
|
|
5424
5621
|
const runtime = resolution.runtime;
|
|
5425
5622
|
const input2 = { ...input, runtime };
|
|
@@ -5433,6 +5630,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
5433
5630
|
configText = null;
|
|
5434
5631
|
}
|
|
5435
5632
|
}
|
|
5633
|
+
const claudeCodeWiring = configText === null && normalizedRuntime === "claude-code" ? await readClaudeCodeWiringEvidence(homeDir) : null;
|
|
5436
5634
|
const allDirectories = directory ? [directory, ...others] : others;
|
|
5437
5635
|
let bareOwnerExists = false;
|
|
5438
5636
|
for (const dir of allDirectories) {
|
|
@@ -5466,7 +5664,10 @@ async function runDoctor(input, deps = {}) {
|
|
|
5466
5664
|
});
|
|
5467
5665
|
continue;
|
|
5468
5666
|
}
|
|
5469
|
-
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists)
|
|
5667
|
+
const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists) || // Additive only: a name or wrapper path in ~/.claude.json proves this
|
|
5668
|
+
// directory is wired; its absence proves nothing (#3830), so the
|
|
5669
|
+
// fallback verdict above is never downgraded.
|
|
5670
|
+
claudeCodeWiring !== null && agentIsWired(claudeCodeWiring, names, slug, identity, sidecar, false, false);
|
|
5470
5671
|
const entry = {
|
|
5471
5672
|
...slug ? { slug } : {},
|
|
5472
5673
|
...identity.agent_id ? { agentId: identity.agent_id } : {},
|
|
@@ -5534,15 +5735,20 @@ async function runDoctor(input, deps = {}) {
|
|
|
5534
5735
|
id: "runtime_config",
|
|
5535
5736
|
label: "Runtime MCP config",
|
|
5536
5737
|
level: "failed",
|
|
5537
|
-
detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ??
|
|
5738
|
+
detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? namedDir ?? "~/.haven/agents"} carries no resolvable ${CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
|
|
5538
5739
|
repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
|
|
5539
5740
|
});
|
|
5540
5741
|
} else if (configPath === null) {
|
|
5742
|
+
const claudePairSuffix = input2.runtime === "claude-code" && primaryDirectory ? await (async () => {
|
|
5743
|
+
const sidecar = await readRuntimeSidecar(primaryDirectory);
|
|
5744
|
+
const names = serverNamesFor(sidecar?.server_name);
|
|
5745
|
+
return ` The MCP pair recorded for this folder is ${names.hosted} / ${names.signer} \u2014 confirm with \`claude mcp get ${names.signer}\`.`;
|
|
5746
|
+
})() : "";
|
|
5541
5747
|
checks.push({
|
|
5542
5748
|
id: "runtime_config",
|
|
5543
5749
|
label: "Runtime MCP config",
|
|
5544
5750
|
level: "ok",
|
|
5545
|
-
detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check
|
|
5751
|
+
detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.${claudePairSuffix}`
|
|
5546
5752
|
});
|
|
5547
5753
|
} else if (configText === null) {
|
|
5548
5754
|
checks.push({
|
|
@@ -5616,9 +5822,13 @@ async function runDoctor(input, deps = {}) {
|
|
|
5616
5822
|
const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
|
|
5617
5823
|
checks.push({
|
|
5618
5824
|
id: "superseded_agents",
|
|
5619
|
-
|
|
5825
|
+
// #3830: an advisory means the connector could not tell wired from
|
|
5826
|
+
// superseded, so its label must not assert "superseded" either. The
|
|
5827
|
+
// inventory `classification` stays as is (#3121 decision 5); the label
|
|
5828
|
+
// is what a person reads.
|
|
5829
|
+
label: supersededLevel === "advisory" ? "Other agent credentials (wiring not verifiable)" : "Superseded agent credentials",
|
|
5620
5830
|
level: supersededLevel,
|
|
5621
|
-
detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of
|
|
5831
|
+
detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector writes, and none it can read names these agents, so which of them are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
5622
5832
|
...supersededLevel === "failed" ? {
|
|
5623
5833
|
repair: `Use Remove agent\u2026 on the Haven agent page for ${supersededLive.join(", ")} (it ends each one\u2019s live budgets), then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
5624
5834
|
} : supersededLevel === "advisory" ? {
|
|
@@ -5647,7 +5857,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
5647
5857
|
label: "MCP server names bound more than once",
|
|
5648
5858
|
level: "advisory",
|
|
5649
5859
|
detail: `${rebound.length} MCP server name${rebound.length === 1 ? "" : "s"} changed hands on this machine (locally recorded bindings; the backend's own record is the authority for the same backend): ${parts.join("; ")}. A saved session, script or document naming the server may still mean the earlier agent.`,
|
|
5650
|
-
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
5860
|
+
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name \u2014 --doctor --repair --name <slug> now selects the pair directly.`
|
|
5651
5861
|
});
|
|
5652
5862
|
}
|
|
5653
5863
|
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
@@ -5707,20 +5917,24 @@ async function runDoctor(input, deps = {}) {
|
|
|
5707
5917
|
async function runRepair(input, deps = {}) {
|
|
5708
5918
|
const homeDir = deps.homeDir ?? homedir();
|
|
5709
5919
|
const messages = [];
|
|
5710
|
-
const
|
|
5711
|
-
|
|
5920
|
+
const namedDir = await resolveNamedDirectory(input, homeDir);
|
|
5921
|
+
const { directory, others, identityDirectories } = await discoverCredentialDirectory(homeDir, namedDir);
|
|
5922
|
+
if (!namedDir && identityDirectories.length > 1) {
|
|
5712
5923
|
const lines = await Promise.all(
|
|
5713
5924
|
identityDirectories.map(async (dir) => {
|
|
5714
5925
|
const identity2 = await readIdentity(dir);
|
|
5715
5926
|
const who = identity2?.agent_id ? `agent ${identity2.agent_id}` : "agent (no agent_id recorded)";
|
|
5716
5927
|
const retired = !identity2?.api_key || await pathExists3(join(dir, TOMBSTONE_FILENAME));
|
|
5717
|
-
|
|
5928
|
+
const sidecar = await readRuntimeSidecar(dir);
|
|
5929
|
+
const slug = sidecar?.server_name;
|
|
5930
|
+
const pair = slug ? `${serverNamesFor(slug).hosted} / ${serverNamesFor(slug).signer}` : "the bare haven / haven-signer pair";
|
|
5931
|
+
return retired ? ` \u2022 ${who} \u2014 ${pair} (${dir}): retired \u2014 not a repair target` : slug ? ` \u2022 ${who} \u2014 owns ${pair}: ${repairCommandNamedFor(input.runtime, slug).replace(/^Run: /, "")}` : ` \u2022 ${who} \u2014 owns ${pair}: ${repairCommandFor(input.runtime, "", dir).replace(/^Run: /, "")}`;
|
|
5718
5932
|
})
|
|
5719
5933
|
);
|
|
5720
5934
|
return {
|
|
5721
5935
|
ok: false,
|
|
5722
5936
|
messages: [
|
|
5723
|
-
`${identityDirectories.length} agent credential directories exist on this machine, and a repair rewrites the runtime config from ONE of them. Name the agent to repair with --credentials-dir \u2014 nothing was changed:`,
|
|
5937
|
+
`${identityDirectories.length} agent credential directories exist on this machine, and a repair rewrites the runtime config from ONE of them. Name the agent to repair with --name <slug> (its recorded pair) or --credentials-dir \u2014 nothing was changed:`,
|
|
5724
5938
|
...lines,
|
|
5725
5939
|
"Not sure which agent is which? Compare the agent ids against the Haven agent page, or run --doctor first."
|
|
5726
5940
|
]
|
|
@@ -5778,13 +5992,62 @@ async function runRepair(input, deps = {}) {
|
|
|
5778
5992
|
const existingSidecar = await readRuntimeSidecar(directory);
|
|
5779
5993
|
const serverName = existingSidecar?.server_name;
|
|
5780
5994
|
const signerPath = join(directory, "signer.json");
|
|
5995
|
+
const consent = await getLocalSignerConsentStatus(signerPath);
|
|
5996
|
+
const consentMismatch = !consent.acknowledged && (consent.reason === "ack_file_missing" || consent.reason === "ack_file_mismatch");
|
|
5997
|
+
if (consentMismatch && !input.ackLocalTools) {
|
|
5998
|
+
const wrapperPath = existingSidecar?.wrapper_path ?? join(directory, "bin", "haven-signer.mjs");
|
|
5999
|
+
return {
|
|
6000
|
+
ok: false,
|
|
6001
|
+
messages: [
|
|
6002
|
+
"Repair stopped before writing anything: the signer it installs would refuse to start until local-tools consent is re-acknowledged \u2014 the stored acknowledgement does not match the tool set of the signer this connector pins.",
|
|
6003
|
+
"The mismatch does not prove the currently running signer is broken; a newer tool set is enough to explain it.",
|
|
6004
|
+
`Re-consent without a setup token, produced by the installed signer: ${wrapperPath} --ack < /dev/null`,
|
|
6005
|
+
`Or re-run repair with --ack-local-tools to re-consent during the repair: ${repairCommandFor(input2.runtime, " --ack-local-tools", directory)}`
|
|
6006
|
+
]
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
5781
6009
|
const prepared = await prepareSignerRuntime(
|
|
5782
6010
|
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
5783
6011
|
{ runCommand: deps.runCommand, env: deps.env }
|
|
5784
6012
|
);
|
|
5785
6013
|
messages.push(...prepared.messages);
|
|
5786
6014
|
const names = serverNamesFor(serverName);
|
|
5787
|
-
|
|
6015
|
+
const rewritesConfig = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir) !== null;
|
|
6016
|
+
if (rewritesConfig) {
|
|
6017
|
+
messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
|
|
6018
|
+
} else {
|
|
6019
|
+
messages.push(
|
|
6020
|
+
`Re-pointed ${prepared.wrapperPath} at signer ${signerPackageSpec()}. ` + (input2.runtime === "claude-code" ? `Claude Code's MCP entries were not touched \u2014 the entry that launches this wrapper uses the new signer after a restart. Confirm with \`claude mcp get ${names.signer}\`.` : `No MCP config was written \u2014 '${input2.runtime}' is configured through its own CLI or by hand; the entry that launches this wrapper uses the new signer after a restart.`)
|
|
6021
|
+
);
|
|
6022
|
+
}
|
|
6023
|
+
if (consentMismatch && input.ackLocalTools) {
|
|
6024
|
+
messages.push(`Re-acknowledging local-tools consent with the installed signer (${signerPackageSpec()}):`);
|
|
6025
|
+
try {
|
|
6026
|
+
const ack = await (deps.runSignerAck ?? runWrapperAck)(prepared.wrapperPath);
|
|
6027
|
+
for (const line of ack.stdout.split("\n")) {
|
|
6028
|
+
if (line.trim()) messages.push(` ${line}`);
|
|
6029
|
+
}
|
|
6030
|
+
} catch (err) {
|
|
6031
|
+
return {
|
|
6032
|
+
ok: false,
|
|
6033
|
+
messages: [
|
|
6034
|
+
...messages,
|
|
6035
|
+
`The re-acknowledgement failed: ${err instanceof Error ? err.message : String(err)} \u2014 consent was NOT written; the runtime config was not rewritten.`
|
|
6036
|
+
]
|
|
6037
|
+
};
|
|
6038
|
+
}
|
|
6039
|
+
const after = await getLocalSignerConsentStatus(signerPath);
|
|
6040
|
+
if (!after.acknowledged) {
|
|
6041
|
+
return {
|
|
6042
|
+
ok: false,
|
|
6043
|
+
messages: [
|
|
6044
|
+
...messages,
|
|
6045
|
+
`Consent still does not match after the re-acknowledgement (${after.reason ?? after.error}). The runtime config was not rewritten.`
|
|
6046
|
+
]
|
|
6047
|
+
};
|
|
6048
|
+
}
|
|
6049
|
+
messages.push("Local-tools consent acknowledged for the installed signer.");
|
|
6050
|
+
}
|
|
5788
6051
|
const configResult = await writeRuntimeConfig({
|
|
5789
6052
|
// Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
|
|
5790
6053
|
// switches on the id, and the raw alias fell to its "manual runtime" arm
|
|
@@ -6004,6 +6267,11 @@ function parseArgs(argv, env = process.env) {
|
|
|
6004
6267
|
"--repair needs --runtime <runtime>: it rewrites that runtime's config, so the runtime is named explicitly rather than inherited from the setup record. --doctor alone resolves it from the record."
|
|
6005
6268
|
);
|
|
6006
6269
|
}
|
|
6270
|
+
if (options.ackLocalTools && !repair) {
|
|
6271
|
+
throw new Error(
|
|
6272
|
+
"--ack-local-tools does nothing with --doctor alone: the doctor is read-only. Re-consent during a repair (--repair --ack-local-tools), or token-free with the recorded wrapper: <agent dir>/bin/haven-signer.mjs --ack"
|
|
6273
|
+
);
|
|
6274
|
+
}
|
|
6007
6275
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
6008
6276
|
}
|
|
6009
6277
|
if (!options.setupToken) {
|
|
@@ -6055,7 +6323,9 @@ function helpText() {
|
|
|
6055
6323
|
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
6056
6324
|
" single hyphens; immutable once wired. Without it the pair is named from the",
|
|
6057
6325
|
" agent's display name; --bare wires the bare haven / haven-signer pair.",
|
|
6058
|
-
" --ack-local-tools Write the
|
|
6326
|
+
" --ack-local-tools Write the local Haven tools acknowledgement: during setup, or (with --repair)",
|
|
6327
|
+
" re-acknowledge consent for the signer the repair installs \u2014 no setup token.",
|
|
6328
|
+
" With a bare --doctor it is refused: --doctor is read-only.",
|
|
6059
6329
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
6060
6330
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
6061
6331
|
" Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
|
|
@@ -6064,10 +6334,13 @@ function helpText() {
|
|
|
6064
6334
|
" signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
|
|
6065
6335
|
" --runtime is optional: without it the doctor checks the runtime the setup recorded,",
|
|
6066
6336
|
" and reports a failure (not a pass) when no record names one.",
|
|
6337
|
+
" --name <slug> selects the agent directory by its recorded MCP pair.",
|
|
6067
6338
|
" --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
|
|
6068
6339
|
" runtime, rewrite the wrapper and runtime config from stored credentials.",
|
|
6069
6340
|
" Hosted topology only (refuses to touch a --local config). No keys, no token.",
|
|
6070
6341
|
" Requires --runtime <runtime>: it rewrites that config, so the runtime is never inherited.",
|
|
6342
|
+
" --name <slug> selects the agent to repair by its recorded pair; --ack-local-tools",
|
|
6343
|
+
" re-consents the installed signer (the old token-free path: <agent dir>/bin/haven-signer.mjs --ack).",
|
|
6071
6344
|
" --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
|
|
6072
6345
|
" prints its public address to paste into the Haven agent page. Nothing changes",
|
|
6073
6346
|
" until you finish; the agent keeps working on its old key throughout.",
|
|
@@ -6232,11 +6505,12 @@ async function runCli(argv, io = {
|
|
|
6232
6505
|
if (parsed.unwire) {
|
|
6233
6506
|
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
6234
6507
|
const { tombstonesDirForAgentDirectory: tombstonesDirForAgentDirectory2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
6508
|
+
const { resolveAgentDirectoryBySlug: resolveAgentDirectoryBySlug2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
|
|
6235
6509
|
const { homedir: homedir11 } = await import('os');
|
|
6236
6510
|
const { join: join13 } = await import('path');
|
|
6237
6511
|
const homeDir = homedir11();
|
|
6238
6512
|
const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
|
|
6239
|
-
const directory = parsed.unwireDir ?? (parsed.options.serverName ?
|
|
6513
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? await resolveAgentDirectoryBySlug2(parsed.options.serverName, { root, allowRetired: true }) : root);
|
|
6240
6514
|
const ledgerDir = tombstonesDirForAgentDirectory2(directory, homeDir);
|
|
6241
6515
|
try {
|
|
6242
6516
|
const result = await unwireAgent2({
|
|
@@ -6438,14 +6712,16 @@ async function runCli(argv, io = {
|
|
|
6438
6712
|
const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
6439
6713
|
const runtime = parsed.options.runtime ?? "";
|
|
6440
6714
|
const credentialsDir = parsed.options.credentialsDir;
|
|
6715
|
+
const serverName = parsed.options.serverName;
|
|
6716
|
+
const ackLocalTools = parsed.options.ackLocalTools === true;
|
|
6441
6717
|
try {
|
|
6442
6718
|
if (parsed.repair) {
|
|
6443
|
-
const repair = await runRepair2({ runtime, credentialsDir });
|
|
6719
|
+
const repair = await runRepair2({ runtime, credentialsDir, serverName, ackLocalTools });
|
|
6444
6720
|
for (const message of repair.messages) io.stderr(`${redactSecrets(message)}
|
|
6445
6721
|
`);
|
|
6446
6722
|
if (!repair.ok) return 1;
|
|
6447
6723
|
}
|
|
6448
|
-
const report = await runDoctor2({ runtime, credentialsDir });
|
|
6724
|
+
const report = await runDoctor2({ runtime, credentialsDir, serverName });
|
|
6449
6725
|
if (parsed.json) {
|
|
6450
6726
|
io.stdout(`${redactSecrets(JSON.stringify(report))}
|
|
6451
6727
|
`);
|
|
@@ -6457,13 +6733,16 @@ async function runCli(argv, io = {
|
|
|
6457
6733
|
`));
|
|
6458
6734
|
}
|
|
6459
6735
|
const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
|
|
6736
|
+
const wiringUnverifiable = report.checks.some(
|
|
6737
|
+
(check) => check.id === "superseded_agents" && check.level === "advisory"
|
|
6738
|
+
);
|
|
6460
6739
|
if (otherAgents.length > 0) {
|
|
6461
6740
|
io.stdout("\nOther agents on this machine:\n");
|
|
6462
6741
|
for (const agent of otherAgents) {
|
|
6463
6742
|
const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
|
|
6464
6743
|
const failed = agent.checks.filter((check) => check.level === "failed");
|
|
6465
6744
|
const advised = agent.checks.filter((check) => check.level === "advisory");
|
|
6466
|
-
const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
|
|
6745
|
+
const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification === "superseded" && wiringUnverifiable ? "not verified as wired \u2014 no config the connector can read names it" : agent.classification;
|
|
6467
6746
|
io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
|
|
6468
6747
|
`));
|
|
6469
6748
|
for (const check of [...failed, ...advised]) {
|
|
@@ -6554,6 +6833,6 @@ init_installed_clients();
|
|
|
6554
6833
|
init_storage();
|
|
6555
6834
|
init_runtime_manifest();
|
|
6556
6835
|
|
|
6557
|
-
export { CONNECTOR_VERSION, CONNECT_OUTCOME_FILENAME, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, failureOutcomeFor, generateDelegateKey, helpText, installRuntime, installedClientHint, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeConnectOutcomeRecord, writeCredentialFiles };
|
|
6836
|
+
export { CONNECTOR_VERSION, CONNECT_OUTCOME_FILENAME, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, assertDirectoryRecordsSlug, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, failureOutcomeFor, generateDelegateKey, helpText, installRuntime, installedClientHint, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveAgentDirectoryBySlug, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeConnectOutcomeRecord, writeCredentialFiles };
|
|
6558
6837
|
//# sourceMappingURL=index.js.map
|
|
6559
6838
|
//# sourceMappingURL=index.js.map
|