@haven_ai/connect 0.0.0-dev.202609190647.18526db → 0.0.0-dev.202609190936.6937e31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -6
- package/dist/cli.cjs +154 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +154 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +154 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +154 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -109,7 +109,7 @@ report stays `version: 1`; `level` is additive.
|
|
|
109
109
|
| Level | Meaning | Example |
|
|
110
110
|
| --- | --- | --- |
|
|
111
111
|
| `ok` | Nothing to say. | The installed signer matches the connector's pin. |
|
|
112
|
-
| `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered); `signer_runtime_unused` (#3123): runtime directories nothing references. `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
|
|
112
|
+
| `advisory` | Worth reading; nothing is broken. Exit 0. | `signer_runtime`: the install is intact but behind the pinned version ("intact, but outdated" — both versions named, `--repair` offered); `signer_runtime_unused` (#3123): runtime directories nothing references; `mcp_server_name_rebound` (#3122): two local binding records claim one server name. `superseded_agents` on a recognised runtime with no config file the connector can read (Claude Code, `other`): a second live key is reported, and the check says why "wired" cannot be verified from this machine. |
|
|
113
113
|
| `failed` | A real failure with one repair action. Exit 1. | A stale or empty runtime directory; a live key in a directory the runtime's config demonstrably does not use. |
|
|
114
114
|
|
|
115
115
|
What stays blocking is live spend authority: a directory whose stored key
|
|
@@ -191,7 +191,9 @@ npx @haven_ai/connect@<channel> --unwire --name research [--reason "..."]
|
|
|
191
191
|
touches local files only, so any published connector does the same job.
|
|
192
192
|
|
|
193
193
|
It tombstone-first (so a stale long-lived host still hears `HAVEN-TOMBSTONE`,
|
|
194
|
-
never a masked `ENOENT`),
|
|
194
|
+
never a masked `ENOENT`), releases the directory's local server-name binding
|
|
195
|
+
record (#3122, whatever the key-material decision below), then removes THAT
|
|
196
|
+
agent's hosted + signer pair from
|
|
195
197
|
every runtime config it appears in (Hermes YAML, Codex TOML, the Cursor / VS
|
|
196
198
|
Code / Insiders / Claude Desktop JSON configs), plus the Hermes dotenv API-key
|
|
197
199
|
line — bare `MCP_HAVEN_API_KEY` or named `MCP_HAVEN_<SLUG>_API_KEY`. Then it
|
|
@@ -224,7 +226,8 @@ revoked or destroyed — that is the honest state. **`--destroy-key-material`**
|
|
|
224
226
|
proceeds on every answer, states what it destroyed and that local recovery
|
|
225
227
|
of a stranded balance ends with it. In `--json` the record carries an
|
|
226
228
|
additive `teardown: { status: destroyed | retained | forced, probe, detail,
|
|
227
|
-
remedy? }
|
|
229
|
+
remedy? }` and, since #3122, `binding_released` (whether a local
|
|
230
|
+
`mcp-server-binding.json` was removed).
|
|
228
231
|
|
|
229
232
|
The `claude-code` copy of the key (written by `claude mcp add`, into a config
|
|
230
233
|
the connector does not own) is out of scope for `--unwire`, as it always was:
|
|
@@ -271,6 +274,67 @@ revoked on the backend; `connect reports, the user decides` (#1688) survives,
|
|
|
271
274
|
and revocation stays an owner action on the Haven agent page. Restart every
|
|
272
275
|
long-lived host afterwards, as with any retirement.
|
|
273
276
|
|
|
277
|
+
### The wallet warning arrives before the write; names are bound locally (#3122)
|
|
278
|
+
|
|
279
|
+
Setup guards the **name slot** (a taken `--name`, the bare pair already wired
|
|
280
|
+
— #2551 refuses or asks) but never the **wallet**: a machine can carry several
|
|
281
|
+
agents with live keys, and until #3122 the "your previous agent(s) still exist
|
|
282
|
+
with their own keys" heads-up was printed only after the credentials were
|
|
283
|
+
written (that completion heads-up, #1688, is unchanged — this slice ADDS an
|
|
284
|
+
earlier notice). Two things changed, both from local files only (no network call is
|
|
285
|
+
added; the backend is not asked whether any key still authenticates):
|
|
286
|
+
|
|
287
|
+
- **Before the key is minted or anything is written**, setup names every
|
|
288
|
+
other credential directory that still holds a stored key, with the account
|
|
289
|
+
it can spend from — `Heads-up (before anything is written): this machine
|
|
290
|
+
already carries N agent director(y/ies) with stored keys — <id> (spends
|
|
291
|
+
from 0x…)`. It **warns, it does not refuse** (owner decision on #3119): a
|
|
292
|
+
refusal would change behaviour for every non-interactive caller, including
|
|
293
|
+
the dashboard's connect flow. `--json` carries the same list as
|
|
294
|
+
`existing_agents_before_write: [{ agent_id, account_address }]`, always
|
|
295
|
+
present on a completed run (`[]` on a clean machine) — a subset of
|
|
296
|
+
`superseded_agent_ids`, which names every other directory that has an
|
|
297
|
+
`identity.json` at all, key-less and tombstoned ones included.
|
|
298
|
+
"Holds a stored key" is the whole test: a tombstoned directory whose key
|
|
299
|
+
files were never deleted is counted (it can still spend), which is the
|
|
300
|
+
opposite side of the doctor's rule that ignores a retired directory's
|
|
301
|
+
binding record (it launches nothing). The scan covers exactly the credential
|
|
302
|
+
root this run writes into — `~/.haven/agents`, or the root given by
|
|
303
|
+
`--credentials-dir` — and never unions the two (#2551's collision check uses
|
|
304
|
+
the same root; `--doctor` shares the no-union rule but reads its
|
|
305
|
+
`--credentials-dir` as the agent directory itself, so its root is that
|
|
306
|
+
directory's parent; #3123's union was for the one shared `signer-runtime`
|
|
307
|
+
root).
|
|
308
|
+
- **Each setup records what it bound** in a non-secret
|
|
309
|
+
`mcp-server-binding.json` beside `last-connect-outcome.json`: `{ version:
|
|
310
|
+
1, server_name, signer_name, agent_id, api_url, hosted_mcp_url?, bound_at }`
|
|
311
|
+
— no key material, by construction (pinned by test). Per credential
|
|
312
|
+
directory, not machine-wide: two concurrent setups never write the same
|
|
313
|
+
file, and "who else claims this name" is answered by scanning the root,
|
|
314
|
+
which setup and the doctor already do. When a setup is about to bind a
|
|
315
|
+
server name another directory's record holds, it says so before the write —
|
|
316
|
+
previous agent (the NEWEST holder when several records claim the name),
|
|
317
|
+
previous backend URL, when — and flags a **DIFFERENT backend** explicitly, because that is the case the backend cannot see and
|
|
318
|
+
the one that silently repoints a saved session; `--json` carries it as
|
|
319
|
+
`server_name_rebound_from: { server_name, agent_id, api_url, bound_at,
|
|
320
|
+
backend_changed }`. `--unwire` releases the record (its `--json` gains
|
|
321
|
+
`binding_released`), and so does a `--replace` retirement, so a legitimately
|
|
322
|
+
free name does not warn forever; `--tombstone` leaves it, and `--doctor`
|
|
323
|
+
ignores a retired directory's record. `--doctor` reports two records
|
|
324
|
+
claiming one name — excluding tombstoned (retired) directories — as the
|
|
325
|
+
`mcp_server_name_rebound` advisory (oldest → newest, backend change
|
|
326
|
+
flagged). The record is written
|
|
327
|
+
after the credentials, best-effort: a directory with credentials and no
|
|
328
|
+
record (a crash in between, or one retired by hand) is not proof the name
|
|
329
|
+
is free — the absence of a warning is not a guarantee, as with
|
|
330
|
+
`superseded_agent_ids`.
|
|
331
|
+
|
|
332
|
+
**What wins when the local record disagrees with the backend.** The backend's
|
|
333
|
+
`agents.mcp_server_name` is the authority for the same backend; the local
|
|
334
|
+
record is a reporting aid that covers the two cases the backend cannot —
|
|
335
|
+
another backend, or offline — and it never asserts itself as the backend's
|
|
336
|
+
current truth. Every message that reads it says "locally recorded".
|
|
337
|
+
|
|
274
338
|
### Structured output for automation
|
|
275
339
|
|
|
276
340
|
Pass `--json` when a launcher needs a machine-readable completion record. Connect
|
|
@@ -282,9 +346,12 @@ whenever ready and verify later with the read-only `haven_get_agent`
|
|
|
282
346
|
tool. The object includes runtime/topology status,
|
|
283
347
|
probe result, activation and next-action guidance, approval state/expiry (null
|
|
284
348
|
when the backend does not provide an approval expiry) and `approval.url`, the two
|
|
285
|
-
read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
|
|
286
|
-
|
|
287
|
-
with
|
|
349
|
+
read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
|
|
350
|
+
`existing_agents_before_write` (#3122 — the other live-keyed directories named
|
|
351
|
+
before the first write, each with the account it spends from), and — on a run
|
|
352
|
+
that replaced existing wiring — `superseded_agents_retired_locally` with
|
|
353
|
+
`retired_agent_ids`; `server_name_rebound_from` (#3122) appears only when the
|
|
354
|
+
run took a server name over from another directory's local binding record. It
|
|
288
355
|
contains no API key, private key, credential
|
|
289
356
|
contents, full credential paths, or full delegate address. The same redacted
|
|
290
357
|
object is available to library callers as `runConnect(...).outcome`; the older
|
package/dist/cli.cjs
CHANGED
|
@@ -172,6 +172,16 @@ function redactSecrets(value) {
|
|
|
172
172
|
function redactForAutomation(value) {
|
|
173
173
|
return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
|
|
174
174
|
}
|
|
175
|
+
function withoutUserinfo(url) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = new URL(url);
|
|
178
|
+
parsed.username = "";
|
|
179
|
+
parsed.password = "";
|
|
180
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
181
|
+
} catch {
|
|
182
|
+
return url;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
175
185
|
function shortAddress(address) {
|
|
176
186
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
|
|
177
187
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
@@ -543,12 +553,54 @@ async function readConnectOutcomeRuntime(directory) {
|
|
|
543
553
|
return null;
|
|
544
554
|
}
|
|
545
555
|
}
|
|
546
|
-
|
|
556
|
+
async function writeMcpServerBinding(directory, binding) {
|
|
557
|
+
const path$1 = path.join(directory, MCP_SERVER_BINDING_FILENAME);
|
|
558
|
+
await promises.writeFile(path$1, `${JSON.stringify(binding, null, 2)}
|
|
559
|
+
`, { mode: 384 });
|
|
560
|
+
return path$1;
|
|
561
|
+
}
|
|
562
|
+
async function readMcpServerBinding(directory) {
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(await promises.readFile(path.join(directory, MCP_SERVER_BINDING_FILENAME), "utf8"));
|
|
565
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
566
|
+
const record = parsed;
|
|
567
|
+
if (record.version !== 1 || typeof record.server_name !== "string" || typeof record.agent_id !== "string" || typeof record.api_url !== "string" || typeof record.bound_at !== "string") return null;
|
|
568
|
+
return record;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function clearMcpServerBinding(directory) {
|
|
574
|
+
try {
|
|
575
|
+
await promises.rm(path.join(directory, MCP_SERVER_BINDING_FILENAME));
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function listMcpServerBindings(baseDir, excludeDirectory) {
|
|
582
|
+
const root = defaultCredentialRoot(baseDir);
|
|
583
|
+
let entries = [];
|
|
584
|
+
try {
|
|
585
|
+
entries = await promises.readdir(root);
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
const out = [];
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const directory = path.join(root, entry);
|
|
592
|
+
const binding = await readMcpServerBinding(directory);
|
|
593
|
+
if (binding) out.push({ directory, binding });
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
|
|
547
598
|
var init_storage = __esm({
|
|
548
599
|
"src/storage.ts"() {
|
|
549
600
|
REKEY_PENDING_FILENAME = "rekey-pending.json";
|
|
550
601
|
REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
551
602
|
CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
|
|
603
|
+
MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
|
|
552
604
|
}
|
|
553
605
|
});
|
|
554
606
|
function mcpPackageSpec() {
|
|
@@ -567,9 +619,9 @@ var init_runtime_manifest = __esm({
|
|
|
567
619
|
mcpPackage: "@haven_ai/mcp",
|
|
568
620
|
mcpVersion: mcp.MCP_VERSION,
|
|
569
621
|
sdkPackage: "@haven_ai/sdk",
|
|
570
|
-
sdkVersion: "0.0.0-dev.
|
|
622
|
+
sdkVersion: "0.0.0-dev.202609190936.6937e31",
|
|
571
623
|
signerPackage: "@haven_ai/signer",
|
|
572
|
-
signerVersion: "0.0.0-dev.
|
|
624
|
+
signerVersion: "0.0.0-dev.202609190936.6937e31",
|
|
573
625
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
574
626
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
575
627
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -3195,9 +3247,10 @@ async function unwireAgent(input) {
|
|
|
3195
3247
|
}
|
|
3196
3248
|
}
|
|
3197
3249
|
}
|
|
3250
|
+
const bindingReleased = await clearMcpServerBinding(input.directory);
|
|
3198
3251
|
const teardown = await decideTeardown(identity, input);
|
|
3199
3252
|
if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
|
|
3200
|
-
return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown };
|
|
3253
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
|
|
3201
3254
|
}
|
|
3202
3255
|
async function decideTeardown(identity, input) {
|
|
3203
3256
|
const ended = "Local recovery of a stranded delegate balance ends with it: the sweep-recovery routes accept only this agent's API key and delegate signature, and neither exists on this machine any more.";
|
|
@@ -4296,6 +4349,30 @@ async function runDoctor(input, deps = {}) {
|
|
|
4296
4349
|
} : {}
|
|
4297
4350
|
});
|
|
4298
4351
|
}
|
|
4352
|
+
const bindings = (await Promise.all(
|
|
4353
|
+
inventory.filter((entry) => entry.classification !== "retired").map(async (entry) => ({ entry, binding: await readMcpServerBinding(entry.directory) }))
|
|
4354
|
+
)).filter((item) => item.binding !== null);
|
|
4355
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4356
|
+
for (const item of bindings) {
|
|
4357
|
+
const list = byName.get(item.binding.server_name) ?? [];
|
|
4358
|
+
list.push(item);
|
|
4359
|
+
byName.set(item.binding.server_name, list);
|
|
4360
|
+
}
|
|
4361
|
+
const rebound = [...byName.entries()].filter(([, items]) => items.length > 1);
|
|
4362
|
+
if (rebound.length > 0) {
|
|
4363
|
+
const parts = rebound.map(([name, items]) => {
|
|
4364
|
+
const ordered = [...items].sort((a, b) => a.binding.bound_at < b.binding.bound_at ? -1 : a.binding.bound_at > b.binding.bound_at ? 1 : 0);
|
|
4365
|
+
const backends = new Set(ordered.map((i) => withoutUserinfo(i.binding.api_url)));
|
|
4366
|
+
return `'${name}': ` + ordered.map((i) => `${i.binding.agent_id} on ${withoutUserinfo(i.binding.api_url)} at ${i.binding.bound_at} [${i.entry.classification}]`).join(" \u2192 ") + (backends.size > 1 ? " (BACKEND CHANGED)" : "");
|
|
4367
|
+
});
|
|
4368
|
+
checks.push({
|
|
4369
|
+
id: "mcp_server_name_rebound",
|
|
4370
|
+
label: "MCP server names bound more than once",
|
|
4371
|
+
level: "advisory",
|
|
4372
|
+
detail: `${rebound.length} MCP server name${rebound.length === 1 ? "" : "s"} changed hands on this machine (locally recorded bindings; the backend's own record is the authority for the same backend): ${parts.join("; ")}. A saved session, script or document naming the server may still mean the earlier agent.`,
|
|
4373
|
+
repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4299
4376
|
const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
|
|
4300
4377
|
if (parkedElsewhere.length > 0) {
|
|
4301
4378
|
const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
|
|
@@ -4743,7 +4820,7 @@ async function pathExists2(path) {
|
|
|
4743
4820
|
init_unwire();
|
|
4744
4821
|
init_local_mcp_runtime();
|
|
4745
4822
|
init_runtime_manifest();
|
|
4746
|
-
var CONNECTOR_VERSION = "0.0.0-dev.
|
|
4823
|
+
var CONNECTOR_VERSION = "0.0.0-dev.202609190936.6937e31";
|
|
4747
4824
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4748
4825
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4749
4826
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -4870,6 +4947,27 @@ async function executeConnect(options, deps, trace) {
|
|
|
4870
4947
|
}
|
|
4871
4948
|
}
|
|
4872
4949
|
}
|
|
4950
|
+
const existingAgents = await listExistingKeyedAgents(options.credentialsDir);
|
|
4951
|
+
if (existingAgents.length > 0) {
|
|
4952
|
+
log("");
|
|
4953
|
+
log(
|
|
4954
|
+
`Heads-up (before anything is written to this machine): it already carries ${existingAgents.length} agent director${existingAgents.length === 1 ? "y" : "ies"} with stored keys \u2014 ` + existingAgents.map((a) => `${a.agentId}${a.accountAddress ? ` (spends from ${shortAddress(a.accountAddress)})` : ""}`).join(", ") + ". This setup creates a NEW agent alongside them and revokes nothing."
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
const hostedNameForRun = serverNamesFor(serverName).hosted;
|
|
4958
|
+
let reboundFrom;
|
|
4959
|
+
const holders = (await listMcpServerBindings(options.credentialsDir)).filter(({ binding }) => binding.server_name === hostedNameForRun).sort((a, b) => a.binding.bound_at < b.binding.bound_at ? 1 : a.binding.bound_at > b.binding.bound_at ? -1 : 0);
|
|
4960
|
+
const newest = holders[0];
|
|
4961
|
+
if (newest) {
|
|
4962
|
+
const { binding, directory } = newest;
|
|
4963
|
+
const backendChanged = withoutUserinfo(binding.api_url) !== withoutUserinfo(options.apiBaseUrl);
|
|
4964
|
+
const previousApiUrl = withoutUserinfo(binding.api_url);
|
|
4965
|
+
reboundFrom = { server_name: binding.server_name, agent_id: binding.agent_id, api_url: previousApiUrl, bound_at: binding.bound_at, backend_changed: backendChanged };
|
|
4966
|
+
const beingReplaced = replacing?.superseded.some((entry) => entry.directory === directory) ?? false;
|
|
4967
|
+
log(
|
|
4968
|
+
`Heads-up: the MCP server name '${binding.server_name}' was bound to agent ${binding.agent_id} on ${previousApiUrl} at ${binding.bound_at} (locally recorded${holders.length > 1 ? `; ${holders.length - 1} older record${holders.length > 2 ? "s" : ""} also claim${holders.length > 2 ? "" : "s"} it` : ""}). ` + (beingReplaced ? `This run replaces that wiring, as you chose, with a new agent on ${withoutUserinfo(options.apiBaseUrl)}` : `This run rebinds it to a new agent on ${withoutUserinfo(options.apiBaseUrl)}`) + (backendChanged ? " \u2014 a DIFFERENT backend: any saved session, script or document naming this MCP server now resolves to a different backend and agent." : ".")
|
|
4969
|
+
);
|
|
4970
|
+
}
|
|
4873
4971
|
const localKey = generateKey();
|
|
4874
4972
|
const localApiKey = generateLocalApiKey();
|
|
4875
4973
|
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
@@ -4933,6 +5031,21 @@ async function executeConnect(options, deps, trace) {
|
|
|
4933
5031
|
warn: log
|
|
4934
5032
|
});
|
|
4935
5033
|
trace.directory = credentialPaths.directory;
|
|
5034
|
+
try {
|
|
5035
|
+
const bindingRecord = {
|
|
5036
|
+
version: 1,
|
|
5037
|
+
server_name: hostedNameForRun,
|
|
5038
|
+
signer_name: serverNamesFor(serverName).signer,
|
|
5039
|
+
agent_id: registration.agent_id,
|
|
5040
|
+
api_url: withoutUserinfo(options.apiBaseUrl),
|
|
5041
|
+
// never persist `user:pass@` (#3154 doc review r2)
|
|
5042
|
+
...registration.hosted_mcp_url ? { hosted_mcp_url: registration.hosted_mcp_url } : {},
|
|
5043
|
+
bound_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5044
|
+
};
|
|
5045
|
+
await writeMcpServerBinding(credentialPaths.directory, bindingRecord);
|
|
5046
|
+
} catch {
|
|
5047
|
+
log("Could not record the MCP server-name binding locally (non-fatal; the next setup cannot see this binding).");
|
|
5048
|
+
}
|
|
4936
5049
|
log(`Stored Haven identity credential locally: ${credentialPaths.identityPath}`);
|
|
4937
5050
|
log(`Stored local signer credential locally: ${credentialPaths.signerPath}`);
|
|
4938
5051
|
log(`Stored non-secret agent orientation locally: ${credentialPaths.agentPath}`);
|
|
@@ -5012,6 +5125,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
5012
5125
|
replacedBy: registration.agent_id
|
|
5013
5126
|
});
|
|
5014
5127
|
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
5128
|
+
await clearMcpServerBinding(entry.directory);
|
|
5015
5129
|
retiredAgentIds.push(entry.agentId);
|
|
5016
5130
|
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
5017
5131
|
} catch (err) {
|
|
@@ -5081,6 +5195,8 @@ async function executeConnect(options, deps, trace) {
|
|
|
5081
5195
|
supersededAgentIds,
|
|
5082
5196
|
supersededAgentsRetiredLocally,
|
|
5083
5197
|
...replacing ? { retiredAgentIds } : {},
|
|
5198
|
+
existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
|
|
5199
|
+
...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
|
|
5084
5200
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
5085
5201
|
approvalRequired: registration.agent_status === "pending_approval",
|
|
5086
5202
|
approvalUrl: registration.approval_url
|
|
@@ -5145,6 +5261,10 @@ function completionOutcome(input) {
|
|
|
5145
5261
|
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
5146
5262
|
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
5147
5263
|
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
5264
|
+
// #3122: always emitted on a completed run (empty list included), for the
|
|
5265
|
+
// same reason as superseded_agent_ids above.
|
|
5266
|
+
existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
|
|
5267
|
+
...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
|
|
5148
5268
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
5149
5269
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
5150
5270
|
};
|
|
@@ -5420,6 +5540,32 @@ function activationInstructionWithWhy(profile) {
|
|
|
5420
5540
|
return profile.activationInstruction;
|
|
5421
5541
|
}
|
|
5422
5542
|
var RERUN_HINT = sdk.connectorRerunCommand();
|
|
5543
|
+
async function listExistingKeyedAgents(baseDir) {
|
|
5544
|
+
const root = defaultCredentialRoot(baseDir);
|
|
5545
|
+
let entries = [];
|
|
5546
|
+
try {
|
|
5547
|
+
entries = await promises.readdir(root);
|
|
5548
|
+
} catch {
|
|
5549
|
+
return [];
|
|
5550
|
+
}
|
|
5551
|
+
const out = [];
|
|
5552
|
+
for (const entry of entries) {
|
|
5553
|
+
const directory = path.join(root, entry);
|
|
5554
|
+
try {
|
|
5555
|
+
const identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
|
|
5556
|
+
if (typeof identity.api_key !== "string" || identity.api_key.length === 0) continue;
|
|
5557
|
+
let agent = {};
|
|
5558
|
+
try {
|
|
5559
|
+
agent = JSON.parse(await promises.readFile(path.join(directory, "agent.json"), "utf8"));
|
|
5560
|
+
} catch {
|
|
5561
|
+
}
|
|
5562
|
+
const { accountAddress } = readStoredAccountAddress(identity, agent);
|
|
5563
|
+
out.push({ agentId: typeof identity.agent_id === "string" ? identity.agent_id : entry, directory, accountAddress: accountAddress ?? null });
|
|
5564
|
+
} catch {
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
return out;
|
|
5568
|
+
}
|
|
5423
5569
|
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
5424
5570
|
const root = defaultCredentialRoot(baseDir);
|
|
5425
5571
|
let entries = [];
|
|
@@ -5847,6 +5993,8 @@ async function runCli(argv, io = {
|
|
|
5847
5993
|
status: r.status,
|
|
5848
5994
|
...r.detail ? { detail: r.detail } : {}
|
|
5849
5995
|
})),
|
|
5996
|
+
// #3122: additive — whether the local server-name binding record was released.
|
|
5997
|
+
binding_released: result.bindingReleased,
|
|
5850
5998
|
// #3123: additive — what happened to the key material and why.
|
|
5851
5999
|
teardown: {
|
|
5852
6000
|
status: result.teardown.status,
|
|
@@ -5869,6 +6017,7 @@ async function runCli(argv, io = {
|
|
|
5869
6017
|
io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}
|
|
5870
6018
|
`));
|
|
5871
6019
|
}
|
|
6020
|
+
io.stdout(result.bindingReleased ? " \u2713 MCP server-name binding: released (the name is free for the next setup).\n" : " \u2013 MCP server-name binding: none recorded for this directory.\n");
|
|
5872
6021
|
const t = result.teardown;
|
|
5873
6022
|
io.stdout(redactSecrets(` ${t.status === "retained" ? "\u2717" : t.status === "forced" ? "!" : "\u2713"} Key material: ${t.status} (probe: ${t.probe}) \u2014 ${t.detail}
|
|
5874
6023
|
`));
|