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