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