@haven_ai/connect 0.0.0-dev.202609230656.9b1d80a → 0.0.0-dev.202609232125.72f3d20

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 CHANGED
@@ -159,7 +159,9 @@ minutes. Two rules follow:
159
159
  This replaces the directory's signer wrapper with a diagnostic that logs the
160
160
  retirement (agent id, date, reason, restart guidance) to the host's MCP
161
161
  stderr log on every probe, and records it in `TOMBSTONE.json` for
162
- `--doctor`. It touches no key material and revokes nothing — revoke the
162
+ `--doctor` — with a copy in `~/.haven/tombstones/` (or `.tombstones/` inside
163
+ a custom credentials root), so the retirement stays visible after the
164
+ directory is deleted. It touches no key material and revokes nothing — revoke the
163
165
  agent on the Haven agent page yourself. Delete the tombstone only once every
164
166
  long-lived host has been restarted.
165
167
 
@@ -173,7 +175,12 @@ minutes. Two rules follow:
173
175
  and a refusal is `{"tombstoned": false, "error": {"code", "next_action"}}`
174
176
  with exit 1 — so check the result rather than assuming silence means success
175
177
  (#2175). The `message` field is present only for connector-authored refusals;
176
- an unexpected filesystem error keeps its raw text on stderr alone.
178
+ an unexpected filesystem error keeps its raw text on stderr alone. If only the
179
+ surviving copy fails — the directory IS tombstoned — the run still succeeds
180
+ with `"recordPath": null` and `"mirrorError"` set to the errno code (or
181
+ `mirror_write_failed` when there is none), and `--unwire --json` carries the
182
+ same as `mirror_error`; `--replace` still removes the old directory's key
183
+ files and names the failure in `retirement_mirror_errors` (#3259).
177
184
 
178
185
  ### Unwiring an agent (`--unwire`, #2169)
179
186
 
@@ -350,7 +357,8 @@ read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
350
357
  `existing_agents_before_write` (#3122 — the other live-keyed directories named
351
358
  before the first write, each with the account it spends from), and — on a run
352
359
  that replaced existing wiring — `superseded_agents_retired_locally` with
353
- `retired_agent_ids`; `server_name_rebound_from` (#3122) appears only when the
360
+ `retired_agent_ids`, plus `retirement_mirror_errors` (#3259) only when a retired
361
+ directory's surviving ledger record could not be written; `server_name_rebound_from` (#3122) appears only when the
354
362
  run took a server name over from another directory's local binding record. It
355
363
  contains no API key, private key, credential
356
364
  contents, full credential paths, or full delegate address. The same redacted
package/dist/cli.cjs CHANGED
@@ -619,9 +619,9 @@ var init_runtime_manifest = __esm({
619
619
  mcpPackage: "@haven_ai/mcp",
620
620
  mcpVersion: mcp.MCP_VERSION,
621
621
  sdkPackage: "@haven_ai/sdk",
622
- sdkVersion: "0.0.0-dev.202609230656.9b1d80a",
622
+ sdkVersion: "0.0.0-dev.202609232125.72f3d20",
623
623
  signerPackage: "@haven_ai/signer",
624
- signerVersion: "0.0.0-dev.202609230656.9b1d80a",
624
+ signerVersion: "0.0.0-dev.202609232125.72f3d20",
625
625
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
626
626
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
627
627
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1590,7 +1590,7 @@ async function probeLocalSignerCredential(signerPath) {
1590
1590
  }
1591
1591
  }
1592
1592
  async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
1593
- return new Promise((resolve10) => {
1593
+ return new Promise((resolve11) => {
1594
1594
  const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
1595
1595
  let stdout = "";
1596
1596
  let settled = false;
@@ -1602,7 +1602,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
1602
1602
  settled = true;
1603
1603
  clearTimeout(timeout);
1604
1604
  child.kill();
1605
- resolve10(result);
1605
+ resolve11(result);
1606
1606
  };
1607
1607
  const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
1608
1608
  child.on("error", () => finish({ status: "process_error" }));
@@ -3000,16 +3000,28 @@ var init_runtime_install = __esm({
3000
3000
  // src/tombstone.ts
3001
3001
  var tombstone_exports = {};
3002
3002
  __export(tombstone_exports, {
3003
+ ROOT_TOMBSTONES_DIRNAME: () => ROOT_TOMBSTONES_DIRNAME,
3003
3004
  TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
3004
3005
  TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
3005
3006
  defaultTombstonesDir: () => defaultTombstonesDir,
3006
3007
  readAgentTombstone: () => readAgentTombstone,
3007
3008
  readTombstoneRecords: () => readTombstoneRecords,
3009
+ tombstonesDirForAgentDirectory: () => tombstonesDirForAgentDirectory,
3010
+ tombstonesDirForCredentialRoot: () => tombstonesDirForCredentialRoot,
3008
3011
  writeAgentTombstone: () => writeAgentTombstone
3009
3012
  });
3010
3013
  function defaultTombstonesDir(baseDir) {
3011
3014
  return path.join(baseDir ?? path.join(os.homedir(), ".haven"), "tombstones");
3012
3015
  }
3016
+ function tombstonesDirForCredentialRoot(credentialRoot, homeDir = os.homedir()) {
3017
+ if (!credentialRoot) return defaultTombstonesDir(path.join(homeDir, ".haven"));
3018
+ const root = path.resolve(credentialRoot);
3019
+ if (root === path.resolve(homeDir, ".haven", "agents")) return defaultTombstonesDir(path.join(homeDir, ".haven"));
3020
+ return path.join(root, ROOT_TOMBSTONES_DIRNAME);
3021
+ }
3022
+ function tombstonesDirForAgentDirectory(directory, homeDir = os.homedir()) {
3023
+ return tombstonesDirForCredentialRoot(path.dirname(path.resolve(directory)), homeDir);
3024
+ }
3013
3025
  async function readTombstoneRecords(tombstonesDir) {
3014
3026
  const root = tombstonesDir ?? defaultTombstonesDir();
3015
3027
  let entries = [];
@@ -3086,9 +3098,14 @@ async function writeAgentTombstone(input) {
3086
3098
  const record = JSON.stringify(info, null, 2) + "\n";
3087
3099
  await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
3088
3100
  const root = input.tombstonesDir ?? defaultTombstonesDir();
3089
- await promises.mkdir(root, { recursive: true, mode: 448 });
3090
3101
  const recordPath = path.join(root, `${info.agent_id}.json`);
3091
- await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
3102
+ try {
3103
+ await promises.mkdir(root, { recursive: true, mode: 448 });
3104
+ await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
3105
+ } catch (err) {
3106
+ const code = err?.code;
3107
+ return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
3108
+ }
3092
3109
  return { ...info, recordPath };
3093
3110
  }
3094
3111
  async function readAgentTombstone(directory) {
@@ -3100,12 +3117,13 @@ async function readAgentTombstone(directory) {
3100
3117
  return null;
3101
3118
  }
3102
3119
  }
3103
- var TOMBSTONE_FILENAME, MIRROR_MODE, TOMBSTONE_MARKER;
3120
+ var TOMBSTONE_FILENAME, ROOT_TOMBSTONES_DIRNAME, MIRROR_MODE, TOMBSTONE_MARKER;
3104
3121
  var init_tombstone = __esm({
3105
3122
  "src/tombstone.ts"() {
3106
3123
  init_redact();
3107
3124
  init_connect_error();
3108
3125
  TOMBSTONE_FILENAME = "TOMBSTONE.json";
3126
+ ROOT_TOMBSTONES_DIRNAME = ".tombstones";
3109
3127
  MIRROR_MODE = 384;
3110
3128
  TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
3111
3129
  }
@@ -3156,7 +3174,7 @@ async function unwireAgent(input) {
3156
3174
  const slug = input.slug ?? sidecar?.server_name;
3157
3175
  const names = serverNamesFor(slug);
3158
3176
  const runtimes = [];
3159
- const tombstoned = await tombstoneDirectoryIfAbsent({
3177
+ const { written: tombstoned, mirrorError } = await tombstoneDirectoryIfAbsent({
3160
3178
  directory: input.directory,
3161
3179
  agentId,
3162
3180
  reason: input.reason ?? "unwired via --unwire",
@@ -3250,7 +3268,16 @@ async function unwireAgent(input) {
3250
3268
  const bindingReleased = await clearMcpServerBinding(input.directory);
3251
3269
  const teardown = await decideTeardown(identity, input);
3252
3270
  if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
3253
- return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
3271
+ return {
3272
+ directory: input.directory,
3273
+ agentId,
3274
+ slug,
3275
+ tombstoned,
3276
+ runtimes,
3277
+ teardown,
3278
+ bindingReleased,
3279
+ ...mirrorError === void 0 ? {} : { mirrorError }
3280
+ };
3254
3281
  }
3255
3282
  async function decideTeardown(identity, input) {
3256
3283
  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.";
@@ -3295,9 +3322,9 @@ async function decideTeardown(identity, input) {
3295
3322
  }
3296
3323
  }
3297
3324
  async function tombstoneDirectoryIfAbsent(input) {
3298
- if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
3299
- await writeAgentTombstone(input);
3300
- return true;
3325
+ if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return { written: false };
3326
+ const { mirrorError } = await writeAgentTombstone(input);
3327
+ return mirrorError === void 0 ? { written: true } : { written: true, mirrorError };
3301
3328
  }
3302
3329
  async function teardownLocalKeyMaterial(directory, identity) {
3303
3330
  await Promise.all([
@@ -4330,7 +4357,8 @@ async function runDoctor(input, deps = {}) {
4330
4357
  if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
4331
4358
  if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
4332
4359
  const knownIds = new Set([...inventory].map((e) => e.agentId ?? path.basename(e.directory)));
4333
- const ghostRecords = (await readTombstoneRecords(path.join(homeDir, ".haven", "tombstones"))).filter(
4360
+ const ledgerDir = input.credentialsDir ? tombstonesDirForAgentDirectory(input.credentialsDir, homeDir) : path.join(homeDir, ".haven", "tombstones");
4361
+ const ghostRecords = (await readTombstoneRecords(ledgerDir)).filter(
4334
4362
  (rec) => !knownIds.has(rec.agent_id)
4335
4363
  );
4336
4364
  if (ghostRecords.length > 0) {
@@ -4828,9 +4856,10 @@ async function pathExists2(path) {
4828
4856
 
4829
4857
  // src/runtime.ts
4830
4858
  init_unwire();
4859
+ init_tombstone();
4831
4860
  init_local_mcp_runtime();
4832
4861
  init_runtime_manifest();
4833
- var CONNECTOR_VERSION = "0.0.0-dev.202609230656.9b1d80a";
4862
+ var CONNECTOR_VERSION = "0.0.0-dev.202609232125.72f3d20";
4834
4863
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4835
4864
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4836
4865
  function failureOutcomeFor(runtimeHint, error) {
@@ -5118,6 +5147,7 @@ async function executeConnect(options, deps, trace) {
5118
5147
  }
5119
5148
  let supersededAgentsRetiredLocally;
5120
5149
  const retiredAgentIds = [];
5150
+ const retirementMirrorErrors = {};
5121
5151
  if (replacing) {
5122
5152
  if (runtimeInstall.errorCode) {
5123
5153
  supersededAgentsRetiredLocally = false;
@@ -5128,12 +5158,21 @@ async function executeConnect(options, deps, trace) {
5128
5158
  supersededAgentsRetiredLocally = true;
5129
5159
  for (const entry of replacing.superseded) {
5130
5160
  try {
5131
- await tombstoneDirectoryIfAbsent({
5161
+ const { mirrorError } = await tombstoneDirectoryIfAbsent({
5132
5162
  directory: entry.directory,
5133
5163
  agentId: entry.agentId,
5134
5164
  reason: "replaced by a new setup (--replace)",
5135
- replacedBy: registration.agent_id
5165
+ replacedBy: registration.agent_id,
5166
+ // #3251: the ledger follows this run's credential root (default
5167
+ // root → ~/.haven/tombstones, as before; any other → <root>/.tombstones).
5168
+ tombstonesDir: tombstonesDirForCredentialRoot(options.credentialsDir)
5136
5169
  });
5170
+ if (mirrorError !== void 0) {
5171
+ retirementMirrorErrors[entry.agentId] = mirrorError;
5172
+ log(
5173
+ `Warning: the surviving tombstone record for ${entry.agentId} could not be written to ${tombstonesDirForCredentialRoot(options.credentialsDir)} (${mirrorError}). The in-place TOMBSTONE.json stands; --doctor will not list this retirement once the directory is deleted.`
5174
+ );
5175
+ }
5137
5176
  await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
5138
5177
  await clearMcpServerBinding(entry.directory);
5139
5178
  retiredAgentIds.push(entry.agentId);
@@ -5205,6 +5244,7 @@ async function executeConnect(options, deps, trace) {
5205
5244
  supersededAgentIds,
5206
5245
  supersededAgentsRetiredLocally,
5207
5246
  ...replacing ? { retiredAgentIds } : {},
5247
+ ...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
5208
5248
  existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
5209
5249
  ...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
5210
5250
  setupChallengeExpiresAt: setup.challenge.expires_at,
@@ -5271,6 +5311,7 @@ function completionOutcome(input) {
5271
5311
  superseded_agent_ids: input.supersededAgentIds ?? [],
5272
5312
  ...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
5273
5313
  ...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
5314
+ ...input.retirementMirrorErrors ? { retirement_mirror_errors: input.retirementMirrorErrors } : {},
5274
5315
  // #3122: always emitted on a completed run (empty list included), for the
5275
5316
  // same reason as superseded_agent_ids above.
5276
5317
  existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
@@ -5459,7 +5500,7 @@ function describeWaitBound(timeoutMs) {
5459
5500
  async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
5460
5501
  const intervalMs = options.intervalMs ?? 5e3;
5461
5502
  const timeoutMs = options.timeoutMs ?? 18e4;
5462
- const sleep = options.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
5503
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
5463
5504
  const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
5464
5505
  const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
5465
5506
  let waitingAnnounced = false;
@@ -5932,7 +5973,7 @@ async function runCli(argv, io = {
5932
5973
  return 0;
5933
5974
  }
5934
5975
  if (parsed.tombstone) {
5935
- const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
5976
+ const { writeAgentTombstone: writeAgentTombstone2, tombstonesDirForAgentDirectory: tombstonesDirForAgentDirectory2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
5936
5977
  const { readFile: readFile15 } = await import('fs/promises');
5937
5978
  const { join: join13 } = await import('path');
5938
5979
  try {
@@ -5944,11 +5985,16 @@ async function runCli(argv, io = {
5944
5985
  agentId = identity.agent_id ?? "unknown";
5945
5986
  } catch {
5946
5987
  }
5988
+ const ledgerDir = tombstonesDirForAgentDirectory2(parsed.tombstone.directory);
5947
5989
  const info = await writeAgentTombstone2({
5948
5990
  directory: parsed.tombstone.directory,
5949
5991
  agentId,
5950
5992
  reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
5951
- replacedBy: parsed.tombstone.replacedBy
5993
+ replacedBy: parsed.tombstone.replacedBy,
5994
+ // #3251: the ledger of the root that holds the retired directory —
5995
+ // ~/.haven/agents/<x> → ~/.haven/tombstones, exactly as before; a
5996
+ // directory under any other root → <root>/.tombstones.
5997
+ tombstonesDir: ledgerDir
5952
5998
  });
5953
5999
  if (parsed.json) {
5954
6000
  io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
@@ -5960,11 +6006,18 @@ async function runCli(argv, io = {
5960
6006
  "Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
5961
6007
  );
5962
6008
  io.stdout(
5963
- `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
6009
+ info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
6010
+ ` : (
6011
+ // #3259: the directory IS tombstoned; only the surviving copy failed.
6012
+ redactSecrets(
6013
+ `! The surviving tombstone record could NOT be written to ${ledgerDir} (${info.mirrorError}). The in-place TOMBSTONE.json stands, but --doctor will not list this retirement once the directory is deleted.
5964
6014
  `
6015
+ )
6016
+ )
5965
6017
  );
5966
6018
  io.stdout(
5967
- "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path. The mirrored record keeps this retirement observable even after the agent directory itself is deleted.\n"
6019
+ "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path." + // #3259 review: never claim a copy that the line above says was not written.
6020
+ (info.recordPath !== null ? " The mirrored record keeps this retirement observable even after the agent directory itself is deleted." : "") + "\n"
5968
6021
  );
5969
6022
  }
5970
6023
  return 0;
@@ -5977,11 +6030,13 @@ async function runCli(argv, io = {
5977
6030
  }
5978
6031
  if (parsed.unwire) {
5979
6032
  const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
6033
+ const { tombstonesDirForAgentDirectory: tombstonesDirForAgentDirectory2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
5980
6034
  const { homedir: homedir11 } = await import('os');
5981
6035
  const { join: join13 } = await import('path');
5982
6036
  const homeDir = homedir11();
5983
6037
  const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
5984
6038
  const directory = parsed.unwireDir ?? (parsed.options.serverName ? join13(root, parsed.options.serverName) : root);
6039
+ const ledgerDir = tombstonesDirForAgentDirectory2(directory, homeDir);
5985
6040
  try {
5986
6041
  const result = await unwireAgent2({
5987
6042
  directory,
@@ -5989,7 +6044,14 @@ async function runCli(argv, io = {
5989
6044
  reason: parsed.unwire.reason,
5990
6045
  replacedBy: parsed.unwire.replacedBy,
5991
6046
  destroyKeyMaterial: parsed.unwire.destroyKeyMaterial,
5992
- homeDir
6047
+ homeDir,
6048
+ // #3251: the ledger of the root that holds the directory being
6049
+ // retired. Derived from the RESOLVED directory, not from
6050
+ // --credentials-dir: `--unwire --credentials-dir <path>` names the agent
6051
+ // directory itself, not a root. ~/.haven/agents/<slug> →
6052
+ // ~/.haven/tombstones, exactly as before; any other root →
6053
+ // <root>/.tombstones.
6054
+ tombstonesDir: ledgerDir
5993
6055
  });
5994
6056
  const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
5995
6057
  const retained = result.teardown.status === "retained";
@@ -6002,6 +6064,8 @@ async function runCli(argv, io = {
6002
6064
  slug: result.slug ?? null,
6003
6065
  directory: result.directory,
6004
6066
  tombstoned: result.tombstoned,
6067
+ // #3259: additive — present only when the surviving ledger record failed.
6068
+ ...result.mirrorError !== void 0 ? { mirror_error: result.mirrorError } : {},
6005
6069
  runtimes: result.runtimes.map((r) => ({
6006
6070
  runtime: r.runtime,
6007
6071
  label: r.label,
@@ -6027,6 +6091,12 @@ async function runCli(argv, io = {
6027
6091
  io.stdout(
6028
6092
  result.tombstoned ? " \xB7 Tombstoned first: any long-lived host still resolving the old wrapper gets the HAVEN-TOMBSTONE diagnosis.\n" : " \xB7 Directory was already tombstoned.\n"
6029
6093
  );
6094
+ if (result.mirrorError !== void 0) {
6095
+ io.stdout(redactSecrets(
6096
+ ` ! Surviving tombstone record: NOT written to ${ledgerDir} (${result.mirrorError}) \u2014 the in-place tombstone stands; --doctor will not list this retirement once the directory is deleted.
6097
+ `
6098
+ ));
6099
+ }
6030
6100
  for (const r of result.runtimes) {
6031
6101
  const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
6032
6102
  io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}