@haven_ai/connect 0.8.0-alpha.0 → 0.9.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 +49 -24
- package/dist/cli.cjs +189 -37
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +189 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +189 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -4
- package/dist/index.d.ts +52 -4
- package/dist/index.js +189 -37
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -318,6 +318,19 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
|
318
318
|
`No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
|
|
319
319
|
);
|
|
320
320
|
}
|
|
321
|
+
let discoveredSlug;
|
|
322
|
+
if (!key) {
|
|
323
|
+
const binding = await readJsonFile(join(directory, "mcp-server-binding.json"));
|
|
324
|
+
const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
|
|
325
|
+
if (hostedName && hostedName.startsWith("haven-")) {
|
|
326
|
+
const slug = hostedName.slice("haven-".length);
|
|
327
|
+
try {
|
|
328
|
+
assertValidServerSlug(slug);
|
|
329
|
+
discoveredSlug = slug;
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
321
334
|
const agent = await readJsonFile(join(directory, "agent.json")) ?? {};
|
|
322
335
|
const signer = await readJsonFile(join(directory, "signer.json")) ?? {};
|
|
323
336
|
const agentId = asString(identity.agent_id);
|
|
@@ -333,6 +346,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
|
333
346
|
directory,
|
|
334
347
|
agentId,
|
|
335
348
|
apiKey,
|
|
349
|
+
...discoveredSlug ? { discoveredSlug } : {},
|
|
336
350
|
apiUrl,
|
|
337
351
|
hostedMcpUrl,
|
|
338
352
|
delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
|
|
@@ -361,7 +375,9 @@ async function discoverSoleAgentDirectory(baseDir) {
|
|
|
361
375
|
const candidates = [];
|
|
362
376
|
for (const entry of entries) {
|
|
363
377
|
const directory = join(root, entry);
|
|
364
|
-
|
|
378
|
+
const identity = await readJsonFile(join(directory, "identity.json"));
|
|
379
|
+
if (!identity) continue;
|
|
380
|
+
if (!asString(identity.api_key)) continue;
|
|
365
381
|
if (await readJsonFile(join(directory, "TOMBSTONE.json"))) continue;
|
|
366
382
|
candidates.push(directory);
|
|
367
383
|
}
|
|
@@ -591,6 +607,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
|
591
607
|
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
|
|
592
608
|
var init_storage = __esm({
|
|
593
609
|
"src/storage.ts"() {
|
|
610
|
+
init_server_names();
|
|
594
611
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
595
612
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
596
613
|
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
@@ -613,9 +630,9 @@ var init_runtime_manifest = __esm({
|
|
|
613
630
|
mcpPackage: "@haven_ai/mcp",
|
|
614
631
|
mcpVersion: MCP_VERSION,
|
|
615
632
|
sdkPackage: "@haven_ai/sdk",
|
|
616
|
-
sdkVersion: "0.
|
|
633
|
+
sdkVersion: "0.9.0-alpha.0",
|
|
617
634
|
signerPackage: "@haven_ai/signer",
|
|
618
|
-
signerVersion: "0.
|
|
635
|
+
signerVersion: "0.9.0-alpha.0",
|
|
619
636
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
620
637
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
621
638
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -1028,7 +1045,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
1028
1045
|
restartRequired: true,
|
|
1029
1046
|
messages: [
|
|
1030
1047
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
1031
|
-
|
|
1048
|
+
// #3737: name the pair actually written — a bare run prints exactly the
|
|
1049
|
+
// historical sentence; a named run names its own pair.
|
|
1050
|
+
`Restart Hermes (start a new session; gateway users: /restart), then verify with \`hermes mcp list\`, \`hermes mcp test ${names.hosted}\`, and \`hermes mcp test ${names.signer}\`.`,
|
|
1032
1051
|
"If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
|
|
1033
1052
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
1034
1053
|
]
|
|
@@ -2996,9 +3015,23 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
|
|
|
2996
3015
|
runtime: "claude-code",
|
|
2997
3016
|
label: "Claude Code",
|
|
2998
3017
|
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
2999
|
-
// not by writing a file this module owns — so
|
|
3000
|
-
//
|
|
3018
|
+
// not by writing a file this module owns — so `configPath` stays null
|
|
3019
|
+
// and the write still goes through the CLI. But a user-scope
|
|
3020
|
+
// `~/.claude.json` carrying an `mcpServers` key IS config-file
|
|
3021
|
+
// evidence: the user has already pointed Claude Code at some MCP
|
|
3022
|
+
// server. It is not the file the connector would write, hence the
|
|
3023
|
+
// separate evidence path (#3732): without it, a Codex config won by
|
|
3024
|
+
// construction on any machine where the user runs Claude Code.
|
|
3001
3025
|
configPath: null,
|
|
3026
|
+
evidencePath: join(homeDir, ".claude.json"),
|
|
3027
|
+
evidenceMatches: (contents) => {
|
|
3028
|
+
try {
|
|
3029
|
+
const parsed = JSON.parse(contents);
|
|
3030
|
+
return typeof parsed === "object" && parsed !== null && "mcpServers" in parsed;
|
|
3031
|
+
} catch {
|
|
3032
|
+
return false;
|
|
3033
|
+
}
|
|
3034
|
+
},
|
|
3002
3035
|
markers: [join(homeDir, ".claude"), join(homeDir, ".claude.json")]
|
|
3003
3036
|
},
|
|
3004
3037
|
{
|
|
@@ -3030,7 +3063,10 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
|
|
|
3030
3063
|
},
|
|
3031
3064
|
{
|
|
3032
3065
|
runtime: "claude-desktop",
|
|
3033
|
-
|
|
3066
|
+
// The chat app, a separate runtime from Claude Code. Named so a user in
|
|
3067
|
+
// the desktop app's Code tab cannot read this row as "where I am" —
|
|
3068
|
+
// it sorted above Claude Code whenever both had config (#3732).
|
|
3069
|
+
label: "Claude Desktop (chat app)",
|
|
3034
3070
|
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3035
3071
|
markers: []
|
|
3036
3072
|
},
|
|
@@ -3045,6 +3081,7 @@ function installedClientTargets(homeDir = homedir(), cwd = process.cwd(), env =
|
|
|
3045
3081
|
}
|
|
3046
3082
|
async function scanInstalledClients(options = {}) {
|
|
3047
3083
|
const exists = options.exists ?? pathExists;
|
|
3084
|
+
const readFile15 = options.readFile ?? ((path) => readFile(path, "utf8"));
|
|
3048
3085
|
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3049
3086
|
const found = [];
|
|
3050
3087
|
for (const target of targets) {
|
|
@@ -3054,6 +3091,18 @@ async function scanInstalledClients(options = {}) {
|
|
|
3054
3091
|
label: target.label,
|
|
3055
3092
|
detail: `MCP config found at ${target.configPath}`,
|
|
3056
3093
|
configPath: target.configPath,
|
|
3094
|
+
evidencePath: target.configPath,
|
|
3095
|
+
evidence: "config-file"
|
|
3096
|
+
});
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
if (target.evidencePath && await hasConfigEvidence(target, exists, readFile15)) {
|
|
3100
|
+
found.push({
|
|
3101
|
+
runtime: target.runtime,
|
|
3102
|
+
label: target.label,
|
|
3103
|
+
detail: `MCP config found at ${target.evidencePath}`,
|
|
3104
|
+
configPath: target.configPath,
|
|
3105
|
+
evidencePath: target.evidencePath,
|
|
3057
3106
|
evidence: "config-file"
|
|
3058
3107
|
});
|
|
3059
3108
|
continue;
|
|
@@ -3065,6 +3114,7 @@ async function scanInstalledClients(options = {}) {
|
|
|
3065
3114
|
label: target.label,
|
|
3066
3115
|
detail: `installed (${marker})`,
|
|
3067
3116
|
configPath: target.configPath,
|
|
3117
|
+
evidencePath: null,
|
|
3068
3118
|
evidence: "client-directory"
|
|
3069
3119
|
});
|
|
3070
3120
|
break;
|
|
@@ -3075,6 +3125,16 @@ async function scanInstalledClients(options = {}) {
|
|
|
3075
3125
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3076
3126
|
});
|
|
3077
3127
|
}
|
|
3128
|
+
async function hasConfigEvidence(target, exists, readFile15) {
|
|
3129
|
+
const path = target.evidencePath;
|
|
3130
|
+
if (!await exists(path)) return false;
|
|
3131
|
+
if (!target.evidenceMatches) return true;
|
|
3132
|
+
try {
|
|
3133
|
+
return target.evidenceMatches(await readFile15(path));
|
|
3134
|
+
} catch {
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3078
3138
|
function installedClientHint(candidates) {
|
|
3079
3139
|
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3080
3140
|
if (candidates.length === 1) {
|
|
@@ -3085,18 +3145,24 @@ function installedClientHint(candidates) {
|
|
|
3085
3145
|
}
|
|
3086
3146
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3087
3147
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3148
|
+
const hint = installedClientHint(candidates);
|
|
3088
3149
|
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3089
3150
|
io.write("These agent clients are installed on this machine:\n");
|
|
3090
3151
|
candidates.forEach((candidate, index) => {
|
|
3091
|
-
|
|
3152
|
+
const suggested = hint.suggestedRuntime === candidate.runtime ? " (suggested)" : "";
|
|
3153
|
+
io.write(` ${index + 1}) ${candidate.label}${suggested} \u2014 ${candidate.detail}
|
|
3092
3154
|
`);
|
|
3093
3155
|
});
|
|
3094
3156
|
io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
|
|
3095
3157
|
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3096
|
-
const answer = await io.question(`Which one? [1-${candidates.length}]
|
|
3158
|
+
const answer = await io.question(`Which one? [1-${candidates.length}]: `);
|
|
3097
3159
|
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3098
3160
|
const trimmed = answer.trim();
|
|
3099
|
-
if (trimmed === "")
|
|
3161
|
+
if (trimmed === "") {
|
|
3162
|
+
io.write(`Type the number of the client to configure (1-${candidates.length}).
|
|
3163
|
+
`);
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3100
3166
|
const picked = Number.parseInt(trimmed, 10);
|
|
3101
3167
|
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3102
3168
|
return candidates[picked - 1].runtime;
|
|
@@ -3338,17 +3404,45 @@ async function detectWiringCollision(input) {
|
|
|
3338
3404
|
suggestedServerName: proposeServerSlug(input.agentName, taken)
|
|
3339
3405
|
};
|
|
3340
3406
|
}
|
|
3341
|
-
function proposeServerSlug(agentName, taken) {
|
|
3342
|
-
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0,
|
|
3407
|
+
function proposeServerSlug(agentName, taken, maxBaseLength = 32) {
|
|
3408
|
+
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxBaseLength).replace(/-+$/g, "");
|
|
3343
3409
|
if (!slugIsValid(base)) base = "agent";
|
|
3344
3410
|
if (!taken.has(base)) return base;
|
|
3345
3411
|
for (let n = 2; n < 1e3; n += 1) {
|
|
3346
3412
|
const suffix = `-${n}`;
|
|
3347
|
-
const candidate = `${base.slice(0,
|
|
3413
|
+
const candidate = `${base.slice(0, maxBaseLength - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
3348
3414
|
if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
|
|
3349
3415
|
}
|
|
3350
3416
|
throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
|
|
3351
3417
|
}
|
|
3418
|
+
function deriveServerSlug(agentName, taken, markNonProduction) {
|
|
3419
|
+
const mark = "-dev";
|
|
3420
|
+
if (!markNonProduction) return proposeServerSlug(agentName, taken);
|
|
3421
|
+
const base = proposeServerSlug(agentName, taken, 32 - mark.length);
|
|
3422
|
+
if (base.endsWith(mark)) return base;
|
|
3423
|
+
const marked = `${base}${mark}`;
|
|
3424
|
+
if (!taken.has(marked)) return marked;
|
|
3425
|
+
return proposeServerSlug(marked, taken);
|
|
3426
|
+
}
|
|
3427
|
+
async function listTakenDirectoryNames(credentialsDir) {
|
|
3428
|
+
const root = defaultCredentialRoot(credentialsDir);
|
|
3429
|
+
let entries = [];
|
|
3430
|
+
try {
|
|
3431
|
+
entries = await readdir(root);
|
|
3432
|
+
} catch {
|
|
3433
|
+
return /* @__PURE__ */ new Set();
|
|
3434
|
+
}
|
|
3435
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3436
|
+
for (const entry of entries) {
|
|
3437
|
+
try {
|
|
3438
|
+
if (!(await stat(join(root, entry))).isDirectory()) continue;
|
|
3439
|
+
} catch {
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
taken.add(entry);
|
|
3443
|
+
}
|
|
3444
|
+
return taken;
|
|
3445
|
+
}
|
|
3352
3446
|
function slugIsValid(slug) {
|
|
3353
3447
|
try {
|
|
3354
3448
|
assertValidServerSlug(slug);
|
|
@@ -3364,7 +3458,7 @@ async function promptWiringCollisionResolution(collision, agentName, io) {
|
|
|
3364
3458
|
io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
|
|
3365
3459
|
`);
|
|
3366
3460
|
io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
|
|
3367
|
-
io.write(" (tombstoned, local key files removed; you still
|
|
3461
|
+
io.write(" (tombstoned, local key files removed; you still use Remove agent\u2026 on the Haven agent page)\n");
|
|
3368
3462
|
io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
|
|
3369
3463
|
`);
|
|
3370
3464
|
io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
|
|
@@ -3587,7 +3681,7 @@ async function decideTeardown(identity, input) {
|
|
|
3587
3681
|
status: "retained",
|
|
3588
3682
|
probe: "ok",
|
|
3589
3683
|
detail: "This agent is still ACTIVE on the backend: its API key and delegate key still carry spend authority, so destroying them would be a live spend-authority change. Key material kept in this directory (0o600); its MCP wiring above is gone.",
|
|
3590
|
-
remedy: `
|
|
3684
|
+
remedy: `Use Remove agent\u2026 on the Haven agent page (connect never revokes), then re-run --unwire; or, to delete the key anyway, re-run with ${DESTROY_FLAG}.`
|
|
3591
3685
|
};
|
|
3592
3686
|
case "unauthorized":
|
|
3593
3687
|
return {
|
|
@@ -3748,7 +3842,16 @@ async function executeConnect(options, deps, trace) {
|
|
|
3748
3842
|
log("Checked local credential storage \u2014 all clear.");
|
|
3749
3843
|
let serverName = options.serverName;
|
|
3750
3844
|
let replacing;
|
|
3751
|
-
if (!serverName) {
|
|
3845
|
+
if (!serverName && options.deriveServerName) {
|
|
3846
|
+
const taken = await listTakenDirectoryNames(options.credentialsDir);
|
|
3847
|
+
const nonProduction = setup.haven_wallet.chain_id !== PRODUCTION_CHAIN_ID;
|
|
3848
|
+
serverName = deriveServerSlug(setup.agent.name, taken, nonProduction);
|
|
3849
|
+
await assertServerSlugAvailable(serverName, options.credentialsDir);
|
|
3850
|
+
const derivedNames = serverNamesFor(serverName);
|
|
3851
|
+
log(
|
|
3852
|
+
`Naming this agent's MCP pair from its display name: ${derivedNames.hosted} / ${derivedNames.signer}. Nothing was displaced; to wire the bare haven / haven-signer pair instead, re-run with --bare, or choose the name yourself with --name <slug>.`
|
|
3853
|
+
);
|
|
3854
|
+
} else if (!serverName) {
|
|
3752
3855
|
const collision = await detectWiringCollision({
|
|
3753
3856
|
credentialsDir: options.credentialsDir,
|
|
3754
3857
|
agentName: setup.agent.name
|
|
@@ -3974,7 +4077,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
3974
4077
|
if (supersededAgentsRetiredLocally === true) {
|
|
3975
4078
|
log("");
|
|
3976
4079
|
log(
|
|
3977
|
-
`Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014
|
|
4080
|
+
`Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014 use Remove agent\u2026 on the Haven agent page for each (it ends their live budgets), then restart EVERY long-lived host (gateways, TUI workers, editors): each holds the MCP wiring snapshot from its own start time, and the tombstone speaks only when a stale host next probes the old path.`
|
|
3978
4081
|
);
|
|
3979
4082
|
} else if (supersededAgentIds.length > 0) {
|
|
3980
4083
|
log("");
|
|
@@ -3982,7 +4085,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
3982
4085
|
`Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededAgentIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
|
|
3983
4086
|
);
|
|
3984
4087
|
log(
|
|
3985
|
-
`If you meant to replace them:
|
|
4088
|
+
`If you meant to replace them: use Remove agent\u2026 on the Haven agent page for each (it ends their live budgets), then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
|
|
3986
4089
|
);
|
|
3987
4090
|
}
|
|
3988
4091
|
} catch {
|
|
@@ -4023,12 +4126,29 @@ async function executeConnect(options, deps, trace) {
|
|
|
4023
4126
|
runtimeInstall,
|
|
4024
4127
|
delegateAddress: registration.delegate_address,
|
|
4025
4128
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4129
|
+
// #3737: the resolved name — derived, explicit, or the bare pair — rides
|
|
4130
|
+
// the machine-readable outcome alongside the prose completion text.
|
|
4131
|
+
serverName,
|
|
4026
4132
|
supersededAgentIds,
|
|
4027
4133
|
supersededAgentsRetiredLocally,
|
|
4028
4134
|
...replacing ? { retiredAgentIds } : {},
|
|
4029
4135
|
...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
|
|
4030
4136
|
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
4031
4137
|
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
4138
|
+
// #3772: only when the install completed. An install that ended with an
|
|
4139
|
+
// error code may not have written the wiring, and on --replace it leaves the
|
|
4140
|
+
// old agent's wiring as possibly the only working one (retirement is skipped
|
|
4141
|
+
// on the same condition) — so it gets no such sentence. That includes
|
|
4142
|
+
// manual_runtime_setup_required, deliberately: the connector wrote nothing
|
|
4143
|
+
// and the manual instruction already says to start a fresh session. A
|
|
4144
|
+
// --replace names every displaced agent even when no binding record exists
|
|
4145
|
+
// (pre-0.4.0 wiring, or a failed best-effort write).
|
|
4146
|
+
...!runtimeInstall.errorCode && (replacing || reboundFrom) ? {
|
|
4147
|
+
staleSession: {
|
|
4148
|
+
agentIds: [.../* @__PURE__ */ new Set([...replacing?.superseded.map((entry) => entry.agentId) ?? [], ...reboundFrom ? [reboundFrom.agent_id] : []])],
|
|
4149
|
+
...reboundFrom?.backend_changed ? { otherBackendAgentId: reboundFrom.agent_id } : {}
|
|
4150
|
+
}
|
|
4151
|
+
} : {},
|
|
4032
4152
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4033
4153
|
approvalRequired: registration.agent_status === "pending_approval",
|
|
4034
4154
|
approvalUrl: registration.approval_url
|
|
@@ -4044,6 +4164,10 @@ async function executeConnect(options, deps, trace) {
|
|
|
4044
4164
|
outcome
|
|
4045
4165
|
};
|
|
4046
4166
|
}
|
|
4167
|
+
function staleSessionNotice(previousAgentIds, otherBackendAgentId) {
|
|
4168
|
+
const one = previousAgentIds.length === 1;
|
|
4169
|
+
return `Any session that was already running keeps acting as the previous agent${one ? "" : "s"} (${previousAgentIds.join(", ")}) until it is restarted, and ${one ? "that agent" : "they"} may still be active in Haven` + (otherBackendAgentId ? ` (${otherBackendAgentId} is on the backend it was created on, not this one)` : "") + `: if ${one ? "it" : "they"} should no longer spend, use Remove agent\u2026 on the Haven agent page (it ends ${one ? "its" : "their"} live budgets).`;
|
|
4170
|
+
}
|
|
4047
4171
|
function completionOutcome(input) {
|
|
4048
4172
|
const { runtimeInstall } = input;
|
|
4049
4173
|
const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
|
|
@@ -4061,7 +4185,10 @@ function completionOutcome(input) {
|
|
|
4061
4185
|
probe: { result: runtimeInstall.probeResult },
|
|
4062
4186
|
activation: {
|
|
4063
4187
|
restart_required: runtimeInstall.restartRequired,
|
|
4064
|
-
instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
|
|
4188
|
+
instruction: (manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction) + // #3772: a rebind changes which agent the server name means, but a
|
|
4189
|
+
// session that is already running keeps the config it loaded — it
|
|
4190
|
+
// goes on acting as the previous agent, which Connect never revokes.
|
|
4191
|
+
(input.staleSession && input.staleSession.agentIds.length > 0 ? ` ${staleSessionNotice(input.staleSession.agentIds, input.staleSession.otherBackendAgentId)}` : "")
|
|
4065
4192
|
},
|
|
4066
4193
|
next_action: nextAction2,
|
|
4067
4194
|
approval: {
|
|
@@ -4097,6 +4224,9 @@ function completionOutcome(input) {
|
|
|
4097
4224
|
// #3122: always emitted on a completed run (empty list included), for the
|
|
4098
4225
|
// same reason as superseded_agent_ids above.
|
|
4099
4226
|
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
4227
|
+
// #3737: the resolved hosted server name — always present on a completed
|
|
4228
|
+
// run, bare pair included, so a caller never has to parse the prose.
|
|
4229
|
+
server_name: serverNamesFor(input.serverName).hosted,
|
|
4100
4230
|
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
4101
4231
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4102
4232
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
@@ -4120,7 +4250,8 @@ function installedClientProse(hint) {
|
|
|
4120
4250
|
}
|
|
4121
4251
|
function runtimeSelectionPrompt(options, deps) {
|
|
4122
4252
|
if (!interactivePromptAllowed(options, deps)) return void 0;
|
|
4123
|
-
|
|
4253
|
+
if (deps.promptRuntime) return deps.promptRuntime;
|
|
4254
|
+
return () => (deps.promptRuntimeByInstalledClient ?? resolveRuntimeByInstalledClientPrompt)({ env: deps.env });
|
|
4124
4255
|
}
|
|
4125
4256
|
function interactivePromptAllowed(options, deps) {
|
|
4126
4257
|
if (options.interactive !== true) return false;
|
|
@@ -4146,7 +4277,7 @@ async function resolveWiringCollision(collision, agentName, options, deps) {
|
|
|
4146
4277
|
}
|
|
4147
4278
|
throw new ConnectError(
|
|
4148
4279
|
"wiring_collision",
|
|
4149
|
-
`This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still
|
|
4280
|
+
`This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still use Remove agent\u2026 on the Haven agent page to end the old agent\u2019s budgets), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
|
|
4150
4281
|
"relay_wiring_collision_to_user",
|
|
4151
4282
|
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4152
4283
|
);
|
|
@@ -4427,7 +4558,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
|
4427
4558
|
function printNextSteps(result, log, approval, approvalUrl) {
|
|
4428
4559
|
for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
|
|
4429
4560
|
}
|
|
4430
|
-
var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4561
|
+
var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4431
4562
|
var init_runtime = __esm({
|
|
4432
4563
|
"src/runtime.ts"() {
|
|
4433
4564
|
init_api();
|
|
@@ -4444,7 +4575,8 @@ var init_runtime = __esm({
|
|
|
4444
4575
|
init_tombstone();
|
|
4445
4576
|
init_local_mcp_runtime();
|
|
4446
4577
|
init_runtime_manifest();
|
|
4447
|
-
CONNECTOR_VERSION = "0.
|
|
4578
|
+
CONNECTOR_VERSION = "0.9.0-alpha.0";
|
|
4579
|
+
PRODUCTION_CHAIN_ID = 8453;
|
|
4448
4580
|
CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
|
|
4449
4581
|
CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4450
4582
|
failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
@@ -4654,9 +4786,10 @@ async function startRekey(options, deps = {}) {
|
|
|
4654
4786
|
started_at: startedAt,
|
|
4655
4787
|
expires_at: expiresAt
|
|
4656
4788
|
});
|
|
4789
|
+
const finishName = options.serverName ?? stored.discoveredSlug;
|
|
4657
4790
|
const finishCommand = [
|
|
4658
4791
|
connectorRerunCommand("--rekey-finish"),
|
|
4659
|
-
|
|
4792
|
+
finishName ? `--name ${finishName}` : void 0,
|
|
4660
4793
|
"--api-key <the key the dashboard showed you>",
|
|
4661
4794
|
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
4662
4795
|
].filter(Boolean).join(" ");
|
|
@@ -5488,9 +5621,9 @@ async function runDoctor(input, deps = {}) {
|
|
|
5488
5621
|
level: supersededLevel,
|
|
5489
5622
|
detail: supersededLevel === "failed" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
|
|
5490
5623
|
...supersededLevel === "failed" ? {
|
|
5491
|
-
repair: `
|
|
5624
|
+
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.`
|
|
5492
5625
|
} : supersededLevel === "advisory" ? {
|
|
5493
|
-
repair: `Check ${supersededLive.join(", ")} on the Haven agent page:
|
|
5626
|
+
repair: `Check ${supersededLive.join(", ")} on the Haven agent page: use Remove agent\u2026 on the ones you no longer use, then remove their director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
|
|
5494
5627
|
} : {}
|
|
5495
5628
|
});
|
|
5496
5629
|
}
|
|
@@ -5715,6 +5848,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
5715
5848
|
let dryRun = false;
|
|
5716
5849
|
let unwireDir;
|
|
5717
5850
|
let replace = false;
|
|
5851
|
+
let bare = false;
|
|
5718
5852
|
for (let i = 0; i < argv.length; i += 1) {
|
|
5719
5853
|
const arg = argv[i];
|
|
5720
5854
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -5762,6 +5896,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
5762
5896
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
5763
5897
|
} else if (arg === "--replace") {
|
|
5764
5898
|
replace = true;
|
|
5899
|
+
} else if (arg === "--bare") {
|
|
5900
|
+
bare = true;
|
|
5765
5901
|
} else if (arg === "--name") {
|
|
5766
5902
|
options.serverName = requireValue(argv, ++i, arg);
|
|
5767
5903
|
assertValidServerSlug(options.serverName);
|
|
@@ -5787,16 +5923,23 @@ function parseArgs(argv, env = process.env) {
|
|
|
5787
5923
|
if (help) {
|
|
5788
5924
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5789
5925
|
}
|
|
5790
|
-
if (replace) {
|
|
5926
|
+
if (replace || bare) {
|
|
5791
5927
|
if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
|
|
5792
|
-
throw new Error("--replace belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent
|
|
5928
|
+
throw new Error(`${replace ? "--replace" : "--bare"} belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.`);
|
|
5793
5929
|
}
|
|
5794
|
-
if (options.serverName) {
|
|
5930
|
+
if (bare && options.serverName) {
|
|
5795
5931
|
throw new Error(
|
|
5796
|
-
"--
|
|
5932
|
+
"--bare and --name contradict each other: --bare wires the bare haven / haven-signer pair, --name installs a named haven-<slug> pair. Pass one of them."
|
|
5797
5933
|
);
|
|
5798
5934
|
}
|
|
5799
|
-
|
|
5935
|
+
if (replace) {
|
|
5936
|
+
if (options.serverName) {
|
|
5937
|
+
throw new Error(
|
|
5938
|
+
"--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
|
|
5939
|
+
);
|
|
5940
|
+
}
|
|
5941
|
+
options.replaceExistingWiring = true;
|
|
5942
|
+
}
|
|
5800
5943
|
}
|
|
5801
5944
|
if (destroyKeyMaterial && !unwire) {
|
|
5802
5945
|
throw new Error("--destroy-key-material only applies to --unwire.");
|
|
@@ -5870,6 +6013,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
5870
6013
|
if (!options.apiBaseUrl) {
|
|
5871
6014
|
throw new Error("Missing --api <Haven API URL>.");
|
|
5872
6015
|
}
|
|
6016
|
+
if (!options.serverName && !bare && !replace) {
|
|
6017
|
+
options.deriveServerName = true;
|
|
6018
|
+
}
|
|
5873
6019
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
5874
6020
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5875
6021
|
}
|
|
@@ -5897,13 +6043,19 @@ function helpText() {
|
|
|
5897
6043
|
" --replace When this machine is already wired to a different Haven agent on the bare",
|
|
5898
6044
|
" haven / haven-signer pair, re-point that pair at the new agent and retire the",
|
|
5899
6045
|
" previous agent directory locally (tombstoned, local key files removed).",
|
|
5900
|
-
" Nothing is revoked \u2014
|
|
5901
|
-
"
|
|
5902
|
-
" and an interactive terminal is asked
|
|
6046
|
+
" Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Implies",
|
|
6047
|
+
" the bare pair. Without it (or --bare) a non-interactive run REFUSES such a",
|
|
6048
|
+
" collision (wiring_collision) and an interactive terminal is asked.",
|
|
6049
|
+
" --bare Wire the bare haven / haven-signer pair. Without --name a setup now names",
|
|
6050
|
+
" its pair from the agent's display name (haven-<slug> /",
|
|
6051
|
+
" haven-signer-<slug>, de-collided against this machine, ending -dev on a",
|
|
6052
|
+
" non-production backend), so adding an agent never has to displace anyone;",
|
|
6053
|
+
" --bare is the explicit opt-in to the unnamed pair. --name overrides it.",
|
|
5903
6054
|
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
5904
6055
|
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
5905
6056
|
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
5906
|
-
" single hyphens; immutable once wired.
|
|
6057
|
+
" single hyphens; immutable once wired. Without it the pair is named from the",
|
|
6058
|
+
" agent's display name; --bare wires the bare haven / haven-signer pair.",
|
|
5907
6059
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
5908
6060
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
5909
6061
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -6049,7 +6201,7 @@ async function runCli(argv, io = {
|
|
|
6049
6201
|
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
6050
6202
|
`));
|
|
6051
6203
|
io.stdout(
|
|
6052
|
-
"Key files were NOT touched and nothing was revoked \u2014
|
|
6204
|
+
"Key files were NOT touched and nothing was revoked \u2014 on the Haven agent page, use Remove agent\u2026 (or Stop budget) if you have not already.\n"
|
|
6053
6205
|
);
|
|
6054
6206
|
io.stdout(
|
|
6055
6207
|
info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
@@ -6158,7 +6310,7 @@ async function runCli(argv, io = {
|
|
|
6158
6310
|
failures.length > 0 ? " Some entries were NOT removed (\u2717 above). Re-run `--unwire` after resolving each refusal \u2014\n it is idempotent.\n" : retained ? " The wiring is gone; the key material is not (\u2717 above). `--doctor` will keep reporting this directory\n as `superseded` until the key is revoked or destroyed \u2014 that is the honest state.\n" : " Verify: `--doctor --runtime <runtime>` per host should report this agent as `retired` with a\n clean runtime-config check.\n"
|
|
6159
6311
|
);
|
|
6160
6312
|
io.stdout(
|
|
6161
|
-
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. " + (retained ? "This directory\u2019s local key material was KEPT (see above); the tombstone\n" : "This directory\u2019s local key material was removed and the tombstone\n") + "record + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready,
|
|
6313
|
+
"Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot\nfrom its own start time. " + (retained ? "This directory\u2019s local key material was KEPT (see above); the tombstone\n" : "This directory\u2019s local key material was removed and the tombstone\n") + "record + #2155 mirror survive \u2014 but nothing was REVOKED on the backend. If you have not\nalready, use Remove agent\u2026 on the Haven agent page to end its budgets on-chain and stop it spending.\n"
|
|
6162
6314
|
);
|
|
6163
6315
|
}
|
|
6164
6316
|
return failures.length > 0 || retained ? 1 : 0;
|