@haven_ai/connect 0.8.1-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 +46 -21
- 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.cjs
CHANGED
|
@@ -325,6 +325,19 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
|
325
325
|
`No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
|
|
326
326
|
);
|
|
327
327
|
}
|
|
328
|
+
let discoveredSlug;
|
|
329
|
+
if (!key) {
|
|
330
|
+
const binding = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
|
|
331
|
+
const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
|
|
332
|
+
if (hostedName && hostedName.startsWith("haven-")) {
|
|
333
|
+
const slug = hostedName.slice("haven-".length);
|
|
334
|
+
try {
|
|
335
|
+
assertValidServerSlug(slug);
|
|
336
|
+
discoveredSlug = slug;
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
328
341
|
const agent = await readJsonFile(path.join(directory, "agent.json")) ?? {};
|
|
329
342
|
const signer = await readJsonFile(path.join(directory, "signer.json")) ?? {};
|
|
330
343
|
const agentId = asString(identity.agent_id);
|
|
@@ -340,6 +353,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
|
|
|
340
353
|
directory,
|
|
341
354
|
agentId,
|
|
342
355
|
apiKey,
|
|
356
|
+
...discoveredSlug ? { discoveredSlug } : {},
|
|
343
357
|
apiUrl,
|
|
344
358
|
hostedMcpUrl,
|
|
345
359
|
delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
|
|
@@ -368,7 +382,9 @@ async function discoverSoleAgentDirectory(baseDir) {
|
|
|
368
382
|
const candidates = [];
|
|
369
383
|
for (const entry of entries) {
|
|
370
384
|
const directory = path.join(root, entry);
|
|
371
|
-
|
|
385
|
+
const identity = await readJsonFile(path.join(directory, "identity.json"));
|
|
386
|
+
if (!identity) continue;
|
|
387
|
+
if (!asString(identity.api_key)) continue;
|
|
372
388
|
if (await readJsonFile(path.join(directory, "TOMBSTONE.json"))) continue;
|
|
373
389
|
candidates.push(directory);
|
|
374
390
|
}
|
|
@@ -598,6 +614,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
|
598
614
|
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
|
|
599
615
|
var init_storage = __esm({
|
|
600
616
|
"src/storage.ts"() {
|
|
617
|
+
init_server_names();
|
|
601
618
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
602
619
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
603
620
|
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
@@ -620,9 +637,9 @@ var init_runtime_manifest = __esm({
|
|
|
620
637
|
mcpPackage: "@haven_ai/mcp",
|
|
621
638
|
mcpVersion: mcp.MCP_VERSION,
|
|
622
639
|
sdkPackage: "@haven_ai/sdk",
|
|
623
|
-
sdkVersion: "0.
|
|
640
|
+
sdkVersion: "0.9.0-alpha.0",
|
|
624
641
|
signerPackage: "@haven_ai/signer",
|
|
625
|
-
signerVersion: "0.
|
|
642
|
+
signerVersion: "0.9.0-alpha.0",
|
|
626
643
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
627
644
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
628
645
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -1035,7 +1052,9 @@ async function writeHermesConfig(input, deps) {
|
|
|
1035
1052
|
restartRequired: true,
|
|
1036
1053
|
messages: [
|
|
1037
1054
|
`Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
|
|
1038
|
-
|
|
1055
|
+
// #3737: name the pair actually written — a bare run prints exactly the
|
|
1056
|
+
// historical sentence; a named run names its own pair.
|
|
1057
|
+
`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}\`.`,
|
|
1039
1058
|
"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.",
|
|
1040
1059
|
"If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
|
|
1041
1060
|
]
|
|
@@ -3003,9 +3022,23 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
|
|
|
3003
3022
|
runtime: "claude-code",
|
|
3004
3023
|
label: "Claude Code",
|
|
3005
3024
|
// Claude Code is configured through its own CLI (`claude mcp add-json`),
|
|
3006
|
-
// not by writing a file this module owns — so
|
|
3007
|
-
//
|
|
3025
|
+
// not by writing a file this module owns — so `configPath` stays null
|
|
3026
|
+
// and the write still goes through the CLI. But a user-scope
|
|
3027
|
+
// `~/.claude.json` carrying an `mcpServers` key IS config-file
|
|
3028
|
+
// evidence: the user has already pointed Claude Code at some MCP
|
|
3029
|
+
// server. It is not the file the connector would write, hence the
|
|
3030
|
+
// separate evidence path (#3732): without it, a Codex config won by
|
|
3031
|
+
// construction on any machine where the user runs Claude Code.
|
|
3008
3032
|
configPath: null,
|
|
3033
|
+
evidencePath: path.join(homeDir, ".claude.json"),
|
|
3034
|
+
evidenceMatches: (contents) => {
|
|
3035
|
+
try {
|
|
3036
|
+
const parsed = JSON.parse(contents);
|
|
3037
|
+
return typeof parsed === "object" && parsed !== null && "mcpServers" in parsed;
|
|
3038
|
+
} catch {
|
|
3039
|
+
return false;
|
|
3040
|
+
}
|
|
3041
|
+
},
|
|
3009
3042
|
markers: [path.join(homeDir, ".claude"), path.join(homeDir, ".claude.json")]
|
|
3010
3043
|
},
|
|
3011
3044
|
{
|
|
@@ -3037,7 +3070,10 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
|
|
|
3037
3070
|
},
|
|
3038
3071
|
{
|
|
3039
3072
|
runtime: "claude-desktop",
|
|
3040
|
-
|
|
3073
|
+
// The chat app, a separate runtime from Claude Code. Named so a user in
|
|
3074
|
+
// the desktop app's Code tab cannot read this row as "where I am" —
|
|
3075
|
+
// it sorted above Claude Code whenever both had config (#3732).
|
|
3076
|
+
label: "Claude Desktop (chat app)",
|
|
3041
3077
|
configPath: runtimeConfigPathFor("claude-desktop", homeDir),
|
|
3042
3078
|
markers: []
|
|
3043
3079
|
},
|
|
@@ -3052,6 +3088,7 @@ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env
|
|
|
3052
3088
|
}
|
|
3053
3089
|
async function scanInstalledClients(options = {}) {
|
|
3054
3090
|
const exists = options.exists ?? pathExists;
|
|
3091
|
+
const readFile15 = options.readFile ?? ((path) => promises.readFile(path, "utf8"));
|
|
3055
3092
|
const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
|
|
3056
3093
|
const found = [];
|
|
3057
3094
|
for (const target of targets) {
|
|
@@ -3061,6 +3098,18 @@ async function scanInstalledClients(options = {}) {
|
|
|
3061
3098
|
label: target.label,
|
|
3062
3099
|
detail: `MCP config found at ${target.configPath}`,
|
|
3063
3100
|
configPath: target.configPath,
|
|
3101
|
+
evidencePath: target.configPath,
|
|
3102
|
+
evidence: "config-file"
|
|
3103
|
+
});
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
if (target.evidencePath && await hasConfigEvidence(target, exists, readFile15)) {
|
|
3107
|
+
found.push({
|
|
3108
|
+
runtime: target.runtime,
|
|
3109
|
+
label: target.label,
|
|
3110
|
+
detail: `MCP config found at ${target.evidencePath}`,
|
|
3111
|
+
configPath: target.configPath,
|
|
3112
|
+
evidencePath: target.evidencePath,
|
|
3064
3113
|
evidence: "config-file"
|
|
3065
3114
|
});
|
|
3066
3115
|
continue;
|
|
@@ -3072,6 +3121,7 @@ async function scanInstalledClients(options = {}) {
|
|
|
3072
3121
|
label: target.label,
|
|
3073
3122
|
detail: `installed (${marker})`,
|
|
3074
3123
|
configPath: target.configPath,
|
|
3124
|
+
evidencePath: null,
|
|
3075
3125
|
evidence: "client-directory"
|
|
3076
3126
|
});
|
|
3077
3127
|
break;
|
|
@@ -3082,6 +3132,16 @@ async function scanInstalledClients(options = {}) {
|
|
|
3082
3132
|
return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
|
|
3083
3133
|
});
|
|
3084
3134
|
}
|
|
3135
|
+
async function hasConfigEvidence(target, exists, readFile15) {
|
|
3136
|
+
const path = target.evidencePath;
|
|
3137
|
+
if (!await exists(path)) return false;
|
|
3138
|
+
if (!target.evidenceMatches) return true;
|
|
3139
|
+
try {
|
|
3140
|
+
return target.evidenceMatches(await readFile15(path));
|
|
3141
|
+
} catch {
|
|
3142
|
+
return false;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3085
3145
|
function installedClientHint(candidates) {
|
|
3086
3146
|
const installedClients = candidates.map((candidate) => candidate.runtime);
|
|
3087
3147
|
if (candidates.length === 1) {
|
|
@@ -3092,18 +3152,24 @@ function installedClientHint(candidates) {
|
|
|
3092
3152
|
}
|
|
3093
3153
|
async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
|
|
3094
3154
|
if (candidates.length === 0) throw noInstalledClientsError();
|
|
3155
|
+
const hint = installedClientHint(candidates);
|
|
3095
3156
|
io.write("Haven could not detect which agent runtime this is.\n");
|
|
3096
3157
|
io.write("These agent clients are installed on this machine:\n");
|
|
3097
3158
|
candidates.forEach((candidate, index) => {
|
|
3098
|
-
|
|
3159
|
+
const suggested = hint.suggestedRuntime === candidate.runtime ? " (suggested)" : "";
|
|
3160
|
+
io.write(` ${index + 1}) ${candidate.label}${suggested} \u2014 ${candidate.detail}
|
|
3099
3161
|
`);
|
|
3100
3162
|
});
|
|
3101
3163
|
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");
|
|
3102
3164
|
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
3103
|
-
const answer = await io.question(`Which one? [1-${candidates.length}]
|
|
3165
|
+
const answer = await io.question(`Which one? [1-${candidates.length}]: `);
|
|
3104
3166
|
if (answer === null) throw promptAbortedError("the prompt was cancelled");
|
|
3105
3167
|
const trimmed = answer.trim();
|
|
3106
|
-
if (trimmed === "")
|
|
3168
|
+
if (trimmed === "") {
|
|
3169
|
+
io.write(`Type the number of the client to configure (1-${candidates.length}).
|
|
3170
|
+
`);
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3107
3173
|
const picked = Number.parseInt(trimmed, 10);
|
|
3108
3174
|
if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
|
|
3109
3175
|
return candidates[picked - 1].runtime;
|
|
@@ -3345,17 +3411,45 @@ async function detectWiringCollision(input) {
|
|
|
3345
3411
|
suggestedServerName: proposeServerSlug(input.agentName, taken)
|
|
3346
3412
|
};
|
|
3347
3413
|
}
|
|
3348
|
-
function proposeServerSlug(agentName, taken) {
|
|
3349
|
-
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0,
|
|
3414
|
+
function proposeServerSlug(agentName, taken, maxBaseLength = 32) {
|
|
3415
|
+
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxBaseLength).replace(/-+$/g, "");
|
|
3350
3416
|
if (!slugIsValid(base)) base = "agent";
|
|
3351
3417
|
if (!taken.has(base)) return base;
|
|
3352
3418
|
for (let n = 2; n < 1e3; n += 1) {
|
|
3353
3419
|
const suffix = `-${n}`;
|
|
3354
|
-
const candidate = `${base.slice(0,
|
|
3420
|
+
const candidate = `${base.slice(0, maxBaseLength - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
3355
3421
|
if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
|
|
3356
3422
|
}
|
|
3357
3423
|
throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
|
|
3358
3424
|
}
|
|
3425
|
+
function deriveServerSlug(agentName, taken, markNonProduction) {
|
|
3426
|
+
const mark = "-dev";
|
|
3427
|
+
if (!markNonProduction) return proposeServerSlug(agentName, taken);
|
|
3428
|
+
const base = proposeServerSlug(agentName, taken, 32 - mark.length);
|
|
3429
|
+
if (base.endsWith(mark)) return base;
|
|
3430
|
+
const marked = `${base}${mark}`;
|
|
3431
|
+
if (!taken.has(marked)) return marked;
|
|
3432
|
+
return proposeServerSlug(marked, taken);
|
|
3433
|
+
}
|
|
3434
|
+
async function listTakenDirectoryNames(credentialsDir) {
|
|
3435
|
+
const root = defaultCredentialRoot(credentialsDir);
|
|
3436
|
+
let entries = [];
|
|
3437
|
+
try {
|
|
3438
|
+
entries = await promises.readdir(root);
|
|
3439
|
+
} catch {
|
|
3440
|
+
return /* @__PURE__ */ new Set();
|
|
3441
|
+
}
|
|
3442
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3443
|
+
for (const entry of entries) {
|
|
3444
|
+
try {
|
|
3445
|
+
if (!(await promises.stat(path.join(root, entry))).isDirectory()) continue;
|
|
3446
|
+
} catch {
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
taken.add(entry);
|
|
3450
|
+
}
|
|
3451
|
+
return taken;
|
|
3452
|
+
}
|
|
3359
3453
|
function slugIsValid(slug) {
|
|
3360
3454
|
try {
|
|
3361
3455
|
assertValidServerSlug(slug);
|
|
@@ -3371,7 +3465,7 @@ async function promptWiringCollisionResolution(collision, agentName, io) {
|
|
|
3371
3465
|
io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
|
|
3372
3466
|
`);
|
|
3373
3467
|
io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
|
|
3374
|
-
io.write(" (tombstoned, local key files removed; you still
|
|
3468
|
+
io.write(" (tombstoned, local key files removed; you still use Remove agent\u2026 on the Haven agent page)\n");
|
|
3375
3469
|
io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
|
|
3376
3470
|
`);
|
|
3377
3471
|
io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
|
|
@@ -3594,7 +3688,7 @@ async function decideTeardown(identity, input) {
|
|
|
3594
3688
|
status: "retained",
|
|
3595
3689
|
probe: "ok",
|
|
3596
3690
|
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.",
|
|
3597
|
-
remedy: `
|
|
3691
|
+
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}.`
|
|
3598
3692
|
};
|
|
3599
3693
|
case "unauthorized":
|
|
3600
3694
|
return {
|
|
@@ -3755,7 +3849,16 @@ async function executeConnect(options, deps, trace) {
|
|
|
3755
3849
|
log("Checked local credential storage \u2014 all clear.");
|
|
3756
3850
|
let serverName = options.serverName;
|
|
3757
3851
|
let replacing;
|
|
3758
|
-
if (!serverName) {
|
|
3852
|
+
if (!serverName && options.deriveServerName) {
|
|
3853
|
+
const taken = await listTakenDirectoryNames(options.credentialsDir);
|
|
3854
|
+
const nonProduction = setup.haven_wallet.chain_id !== PRODUCTION_CHAIN_ID;
|
|
3855
|
+
serverName = deriveServerSlug(setup.agent.name, taken, nonProduction);
|
|
3856
|
+
await assertServerSlugAvailable(serverName, options.credentialsDir);
|
|
3857
|
+
const derivedNames = serverNamesFor(serverName);
|
|
3858
|
+
log(
|
|
3859
|
+
`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>.`
|
|
3860
|
+
);
|
|
3861
|
+
} else if (!serverName) {
|
|
3759
3862
|
const collision = await detectWiringCollision({
|
|
3760
3863
|
credentialsDir: options.credentialsDir,
|
|
3761
3864
|
agentName: setup.agent.name
|
|
@@ -3981,7 +4084,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
3981
4084
|
if (supersededAgentsRetiredLocally === true) {
|
|
3982
4085
|
log("");
|
|
3983
4086
|
log(
|
|
3984
|
-
`Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014
|
|
4087
|
+
`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.`
|
|
3985
4088
|
);
|
|
3986
4089
|
} else if (supersededAgentIds.length > 0) {
|
|
3987
4090
|
log("");
|
|
@@ -3989,7 +4092,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
3989
4092
|
`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.`
|
|
3990
4093
|
);
|
|
3991
4094
|
log(
|
|
3992
|
-
`If you meant to replace them:
|
|
4095
|
+
`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.`
|
|
3993
4096
|
);
|
|
3994
4097
|
}
|
|
3995
4098
|
} catch {
|
|
@@ -4030,12 +4133,29 @@ async function executeConnect(options, deps, trace) {
|
|
|
4030
4133
|
runtimeInstall,
|
|
4031
4134
|
delegateAddress: registration.delegate_address,
|
|
4032
4135
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4136
|
+
// #3737: the resolved name — derived, explicit, or the bare pair — rides
|
|
4137
|
+
// the machine-readable outcome alongside the prose completion text.
|
|
4138
|
+
serverName,
|
|
4033
4139
|
supersededAgentIds,
|
|
4034
4140
|
supersededAgentsRetiredLocally,
|
|
4035
4141
|
...replacing ? { retiredAgentIds } : {},
|
|
4036
4142
|
...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
|
|
4037
4143
|
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
4038
4144
|
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
4145
|
+
// #3772: only when the install completed. An install that ended with an
|
|
4146
|
+
// error code may not have written the wiring, and on --replace it leaves the
|
|
4147
|
+
// old agent's wiring as possibly the only working one (retirement is skipped
|
|
4148
|
+
// on the same condition) — so it gets no such sentence. That includes
|
|
4149
|
+
// manual_runtime_setup_required, deliberately: the connector wrote nothing
|
|
4150
|
+
// and the manual instruction already says to start a fresh session. A
|
|
4151
|
+
// --replace names every displaced agent even when no binding record exists
|
|
4152
|
+
// (pre-0.4.0 wiring, or a failed best-effort write).
|
|
4153
|
+
...!runtimeInstall.errorCode && (replacing || reboundFrom) ? {
|
|
4154
|
+
staleSession: {
|
|
4155
|
+
agentIds: [.../* @__PURE__ */ new Set([...replacing?.superseded.map((entry) => entry.agentId) ?? [], ...reboundFrom ? [reboundFrom.agent_id] : []])],
|
|
4156
|
+
...reboundFrom?.backend_changed ? { otherBackendAgentId: reboundFrom.agent_id } : {}
|
|
4157
|
+
}
|
|
4158
|
+
} : {},
|
|
4039
4159
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4040
4160
|
approvalRequired: registration.agent_status === "pending_approval",
|
|
4041
4161
|
approvalUrl: registration.approval_url
|
|
@@ -4051,6 +4171,10 @@ async function executeConnect(options, deps, trace) {
|
|
|
4051
4171
|
outcome
|
|
4052
4172
|
};
|
|
4053
4173
|
}
|
|
4174
|
+
function staleSessionNotice(previousAgentIds, otherBackendAgentId) {
|
|
4175
|
+
const one = previousAgentIds.length === 1;
|
|
4176
|
+
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).`;
|
|
4177
|
+
}
|
|
4054
4178
|
function completionOutcome(input) {
|
|
4055
4179
|
const { runtimeInstall } = input;
|
|
4056
4180
|
const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
|
|
@@ -4068,7 +4192,10 @@ function completionOutcome(input) {
|
|
|
4068
4192
|
probe: { result: runtimeInstall.probeResult },
|
|
4069
4193
|
activation: {
|
|
4070
4194
|
restart_required: runtimeInstall.restartRequired,
|
|
4071
|
-
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
|
|
4195
|
+
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
|
|
4196
|
+
// session that is already running keeps the config it loaded — it
|
|
4197
|
+
// goes on acting as the previous agent, which Connect never revokes.
|
|
4198
|
+
(input.staleSession && input.staleSession.agentIds.length > 0 ? ` ${staleSessionNotice(input.staleSession.agentIds, input.staleSession.otherBackendAgentId)}` : "")
|
|
4072
4199
|
},
|
|
4073
4200
|
next_action: nextAction2,
|
|
4074
4201
|
approval: {
|
|
@@ -4104,6 +4231,9 @@ function completionOutcome(input) {
|
|
|
4104
4231
|
// #3122: always emitted on a completed run (empty list included), for the
|
|
4105
4232
|
// same reason as superseded_agent_ids above.
|
|
4106
4233
|
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
4234
|
+
// #3737: the resolved hosted server name — always present on a completed
|
|
4235
|
+
// run, bare pair included, so a caller never has to parse the prose.
|
|
4236
|
+
server_name: serverNamesFor(input.serverName).hosted,
|
|
4107
4237
|
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
4108
4238
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4109
4239
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
@@ -4127,7 +4257,8 @@ function installedClientProse(hint) {
|
|
|
4127
4257
|
}
|
|
4128
4258
|
function runtimeSelectionPrompt(options, deps) {
|
|
4129
4259
|
if (!interactivePromptAllowed(options, deps)) return void 0;
|
|
4130
|
-
|
|
4260
|
+
if (deps.promptRuntime) return deps.promptRuntime;
|
|
4261
|
+
return () => (deps.promptRuntimeByInstalledClient ?? resolveRuntimeByInstalledClientPrompt)({ env: deps.env });
|
|
4131
4262
|
}
|
|
4132
4263
|
function interactivePromptAllowed(options, deps) {
|
|
4133
4264
|
if (options.interactive !== true) return false;
|
|
@@ -4153,7 +4284,7 @@ async function resolveWiringCollision(collision, agentName, options, deps) {
|
|
|
4153
4284
|
}
|
|
4154
4285
|
throw new ConnectError(
|
|
4155
4286
|
"wiring_collision",
|
|
4156
|
-
`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
|
|
4287
|
+
`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.`,
|
|
4157
4288
|
"relay_wiring_collision_to_user",
|
|
4158
4289
|
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4159
4290
|
);
|
|
@@ -4434,7 +4565,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
|
4434
4565
|
function printNextSteps(result, log, approval, approvalUrl) {
|
|
4435
4566
|
for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
|
|
4436
4567
|
}
|
|
4437
|
-
var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4568
|
+
var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
|
|
4438
4569
|
var init_runtime = __esm({
|
|
4439
4570
|
"src/runtime.ts"() {
|
|
4440
4571
|
init_api();
|
|
@@ -4451,7 +4582,8 @@ var init_runtime = __esm({
|
|
|
4451
4582
|
init_tombstone();
|
|
4452
4583
|
init_local_mcp_runtime();
|
|
4453
4584
|
init_runtime_manifest();
|
|
4454
|
-
CONNECTOR_VERSION = "0.
|
|
4585
|
+
CONNECTOR_VERSION = "0.9.0-alpha.0";
|
|
4586
|
+
PRODUCTION_CHAIN_ID = 8453;
|
|
4455
4587
|
CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
|
|
4456
4588
|
CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4457
4589
|
failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
@@ -4661,9 +4793,10 @@ async function startRekey(options, deps = {}) {
|
|
|
4661
4793
|
started_at: startedAt,
|
|
4662
4794
|
expires_at: expiresAt
|
|
4663
4795
|
});
|
|
4796
|
+
const finishName = options.serverName ?? stored.discoveredSlug;
|
|
4664
4797
|
const finishCommand = [
|
|
4665
4798
|
sdk.connectorRerunCommand("--rekey-finish"),
|
|
4666
|
-
|
|
4799
|
+
finishName ? `--name ${finishName}` : void 0,
|
|
4667
4800
|
"--api-key <the key the dashboard showed you>",
|
|
4668
4801
|
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
4669
4802
|
].filter(Boolean).join(" ");
|
|
@@ -5495,9 +5628,9 @@ async function runDoctor(input, deps = {}) {
|
|
|
5495
5628
|
level: supersededLevel,
|
|
5496
5629
|
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("; ")}.`,
|
|
5497
5630
|
...supersededLevel === "failed" ? {
|
|
5498
|
-
repair: `
|
|
5631
|
+
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.`
|
|
5499
5632
|
} : supersededLevel === "advisory" ? {
|
|
5500
|
-
repair: `Check ${supersededLive.join(", ")} on the Haven agent page:
|
|
5633
|
+
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.`
|
|
5501
5634
|
} : {}
|
|
5502
5635
|
});
|
|
5503
5636
|
}
|
|
@@ -5722,6 +5855,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
5722
5855
|
let dryRun = false;
|
|
5723
5856
|
let unwireDir;
|
|
5724
5857
|
let replace = false;
|
|
5858
|
+
let bare = false;
|
|
5725
5859
|
for (let i = 0; i < argv.length; i += 1) {
|
|
5726
5860
|
const arg = argv[i];
|
|
5727
5861
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -5769,6 +5903,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
5769
5903
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
5770
5904
|
} else if (arg === "--replace") {
|
|
5771
5905
|
replace = true;
|
|
5906
|
+
} else if (arg === "--bare") {
|
|
5907
|
+
bare = true;
|
|
5772
5908
|
} else if (arg === "--name") {
|
|
5773
5909
|
options.serverName = requireValue(argv, ++i, arg);
|
|
5774
5910
|
assertValidServerSlug(options.serverName);
|
|
@@ -5794,16 +5930,23 @@ function parseArgs(argv, env = process.env) {
|
|
|
5794
5930
|
if (help) {
|
|
5795
5931
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5796
5932
|
}
|
|
5797
|
-
if (replace) {
|
|
5933
|
+
if (replace || bare) {
|
|
5798
5934
|
if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
|
|
5799
|
-
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
|
|
5935
|
+
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.`);
|
|
5800
5936
|
}
|
|
5801
|
-
if (options.serverName) {
|
|
5937
|
+
if (bare && options.serverName) {
|
|
5802
5938
|
throw new Error(
|
|
5803
|
-
"--
|
|
5939
|
+
"--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."
|
|
5804
5940
|
);
|
|
5805
5941
|
}
|
|
5806
|
-
|
|
5942
|
+
if (replace) {
|
|
5943
|
+
if (options.serverName) {
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
"--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."
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
options.replaceExistingWiring = true;
|
|
5949
|
+
}
|
|
5807
5950
|
}
|
|
5808
5951
|
if (destroyKeyMaterial && !unwire) {
|
|
5809
5952
|
throw new Error("--destroy-key-material only applies to --unwire.");
|
|
@@ -5877,6 +6020,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
5877
6020
|
if (!options.apiBaseUrl) {
|
|
5878
6021
|
throw new Error("Missing --api <Haven API URL>.");
|
|
5879
6022
|
}
|
|
6023
|
+
if (!options.serverName && !bare && !replace) {
|
|
6024
|
+
options.deriveServerName = true;
|
|
6025
|
+
}
|
|
5880
6026
|
options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
|
5881
6027
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
5882
6028
|
}
|
|
@@ -5904,13 +6050,19 @@ function helpText() {
|
|
|
5904
6050
|
" --replace When this machine is already wired to a different Haven agent on the bare",
|
|
5905
6051
|
" haven / haven-signer pair, re-point that pair at the new agent and retire the",
|
|
5906
6052
|
" previous agent directory locally (tombstoned, local key files removed).",
|
|
5907
|
-
" Nothing is revoked \u2014
|
|
5908
|
-
"
|
|
5909
|
-
" and an interactive terminal is asked
|
|
6053
|
+
" Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Implies",
|
|
6054
|
+
" the bare pair. Without it (or --bare) a non-interactive run REFUSES such a",
|
|
6055
|
+
" collision (wiring_collision) and an interactive terminal is asked.",
|
|
6056
|
+
" --bare Wire the bare haven / haven-signer pair. Without --name a setup now names",
|
|
6057
|
+
" its pair from the agent's display name (haven-<slug> /",
|
|
6058
|
+
" haven-signer-<slug>, de-collided against this machine, ending -dev on a",
|
|
6059
|
+
" non-production backend), so adding an agent never has to displace anyone;",
|
|
6060
|
+
" --bare is the explicit opt-in to the unnamed pair. --name overrides it.",
|
|
5910
6061
|
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
5911
6062
|
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
5912
6063
|
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
5913
|
-
" single hyphens; immutable once wired.
|
|
6064
|
+
" single hyphens; immutable once wired. Without it the pair is named from the",
|
|
6065
|
+
" agent's display name; --bare wires the bare haven / haven-signer pair.",
|
|
5914
6066
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
5915
6067
|
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
5916
6068
|
" --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
|
|
@@ -6056,7 +6208,7 @@ async function runCli(argv, io = {
|
|
|
6056
6208
|
io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
|
|
6057
6209
|
`));
|
|
6058
6210
|
io.stdout(
|
|
6059
|
-
"Key files were NOT touched and nothing was revoked \u2014
|
|
6211
|
+
"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"
|
|
6060
6212
|
);
|
|
6061
6213
|
io.stdout(
|
|
6062
6214
|
info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
|
|
@@ -6165,7 +6317,7 @@ async function runCli(argv, io = {
|
|
|
6165
6317
|
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"
|
|
6166
6318
|
);
|
|
6167
6319
|
io.stdout(
|
|
6168
|
-
"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,
|
|
6320
|
+
"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"
|
|
6169
6321
|
);
|
|
6170
6322
|
}
|
|
6171
6323
|
return failures.length > 0 || retained ? 1 : 0;
|