@haven_ai/connect 0.6.0-alpha.0 → 0.8.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 CHANGED
@@ -392,7 +392,8 @@ also yields one rather than failing a completed setup.
392
392
 
393
393
  **The dashboard offers the revoke, never the connector (#2561).** The same ids
394
394
  now ride the install-status report, so the Haven dashboard can put a
395
- one-click revoke next to the setup that displaced them. The connector does not
395
+ revoke next to the setup that displaced them; it also ends the agent's budget
396
+ with one owner signature (#3542). The connector does not
396
397
  and must not do it: `POST /agents/:id/revoke` is owner-authenticated, and an
397
398
  agent credential retiring a sibling agent is the "agent editing its own
398
399
  authority" the re-key routes refuse. Nothing is revoked automatically — the
@@ -703,6 +704,18 @@ which.
703
704
  runtime, rewrite the wrapper + sidecar, and re-write the runtime config from
704
705
  the STORED credentials. It never touches keys and never needs a new token.
705
706
 
707
+ **Several agents on one machine (#3412).** A repair rewrites the runtime config
708
+ from ONE agent's stored credentials. With more than one agent credential
709
+ directory under `~/.haven/agents` — or when `--doctor` was itself given
710
+ `--credentials-dir` — every repair line the doctor prints names the agent it is
711
+ about with `--credentials-dir <dir>`, and a `--repair` without
712
+ `--credentials-dir` is refused — nothing is changed — with one exact command
713
+ per live agent (its agent id beside it); a retired directory is listed as
714
+ "retired — not a repair target" and never offered. Without that, "the newest directory" was a
715
+ guess, and it re-wired a different agent than the one the doctor described.
716
+ Every "update your signer" hint Haven emits names the flagless
717
+ `npx -y @haven_ai/connect@<channel> --doctor`, which then prints that line.
718
+
706
719
  ## Installing an unpublished signer / SDK / MCP build (`HAVEN_SIGNER_SPEC`, #2424)
707
720
 
708
721
  Setup installs the connector's **pinned** siblings — `@haven_ai/signer@<pin>`
package/dist/cli.cjs CHANGED
@@ -620,9 +620,9 @@ var init_runtime_manifest = __esm({
620
620
  mcpPackage: "@haven_ai/mcp",
621
621
  mcpVersion: mcp.MCP_VERSION,
622
622
  sdkPackage: "@haven_ai/sdk",
623
- sdkVersion: "0.6.0-alpha.0",
623
+ sdkVersion: "0.8.0-alpha.0",
624
624
  signerPackage: "@haven_ai/signer",
625
- signerVersion: "0.6.0-alpha.0",
625
+ signerVersion: "0.8.0-alpha.0",
626
626
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
627
627
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
628
628
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -4451,7 +4451,7 @@ var init_runtime = __esm({
4451
4451
  init_tombstone();
4452
4452
  init_local_mcp_runtime();
4453
4453
  init_runtime_manifest();
4454
- CONNECTOR_VERSION = "0.6.0-alpha.0";
4454
+ CONNECTOR_VERSION = "0.8.0-alpha.0";
4455
4455
  CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
4456
4456
  CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4457
4457
  failureOutcomesByError = /* @__PURE__ */ new WeakMap();
@@ -4934,9 +4934,23 @@ function rollUpLevel(checks) {
4934
4934
  function runtimeFlagFor(runtime) {
4935
4935
  return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
4936
4936
  }
4937
- function repairCommandFor(runtime, suffix = "") {
4938
- if (normalizeRuntimeName(runtime)) return `Run: ${RERUN} --doctor --repair --runtime ${runtime}${suffix}`;
4939
- return `Run: ${RERUN} --doctor --repair --runtime <runtime>${suffix.replace(/\.$/, "")} \u2014 --repair needs the runtime named; one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`;
4937
+ async function pathExists3(path) {
4938
+ try {
4939
+ await promises.stat(path);
4940
+ return true;
4941
+ } catch {
4942
+ return false;
4943
+ }
4944
+ }
4945
+ function credentialsDirFlagFor(credentialsDir) {
4946
+ if (!credentialsDir) return "";
4947
+ const quoted = /^[\w@%+=:,./~-]+$/.test(credentialsDir) ? credentialsDir : `'${credentialsDir.replace(/'/g, `'\\''`)}'`;
4948
+ return ` --credentials-dir ${quoted}`;
4949
+ }
4950
+ function repairCommandFor(runtime, suffix = "", credentialsDir) {
4951
+ const dirFlag = credentialsDirFlagFor(credentialsDir);
4952
+ if (normalizeRuntimeName(runtime)) return `Run: ${RERUN} --doctor --repair --runtime ${runtime}${dirFlag}${suffix}`;
4953
+ return `Run: ${RERUN} --doctor --repair --runtime <runtime>${dirFlag}${suffix.replace(/\.$/, "")} \u2014 --repair needs the runtime named; one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`;
4940
4954
  }
4941
4955
  async function resolveDoctorRuntime(input, directory) {
4942
4956
  const explicit = input.runtime.trim();
@@ -4955,45 +4969,47 @@ async function discoverCredentialDirectory(homeDir, explicit) {
4955
4969
  try {
4956
4970
  entries = await promises.readdir(root);
4957
4971
  } catch {
4958
- return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
4972
+ return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set(), identityDirectories: [explicit] } : { others: [], parkedOnly: /* @__PURE__ */ new Set(), identityDirectories: [] };
4959
4973
  }
4960
4974
  const candidates = [];
4961
4975
  const tombstonedOnly = [];
4962
4976
  const parkedOnly = [];
4963
4977
  for (const entry of entries) {
4964
4978
  const directory = path.join(root, entry);
4979
+ if (await pathExists3(path.join(directory, TOMBSTONE_FILENAME))) {
4980
+ tombstonedOnly.push(directory);
4981
+ continue;
4982
+ }
4965
4983
  try {
4966
4984
  const s = await promises.stat(path.join(directory, "identity.json"));
4967
4985
  candidates.push({ directory, mtimeMs: s.mtimeMs });
4968
4986
  } catch {
4969
4987
  try {
4970
- await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
4971
- tombstonedOnly.push(directory);
4988
+ await promises.stat(path.join(directory, REKEY_PENDING_FILENAME));
4989
+ parkedOnly.push(directory);
4972
4990
  } catch {
4973
- try {
4974
- await promises.stat(path.join(directory, REKEY_PENDING_FILENAME));
4975
- parkedOnly.push(directory);
4976
- } catch {
4977
- }
4978
4991
  }
4979
4992
  }
4980
4993
  }
4981
4994
  const parkedOnlySet = new Set(parkedOnly);
4982
4995
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
4996
+ const identityDirectories = candidates.map((c) => c.directory);
4983
4997
  if (explicit) {
4984
4998
  return {
4985
4999
  directory: explicit,
4986
5000
  others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
4987
- parkedOnly: parkedOnlySet
5001
+ parkedOnly: parkedOnlySet,
5002
+ identityDirectories: [explicit, ...identityDirectories.filter((d) => d !== explicit)]
4988
5003
  };
4989
5004
  }
4990
5005
  if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
4991
- return { others: [], parkedOnly: parkedOnlySet };
5006
+ return { others: [], parkedOnly: parkedOnlySet, identityDirectories };
4992
5007
  }
4993
5008
  return {
4994
5009
  directory: candidates[0]?.directory,
4995
5010
  others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
4996
- parkedOnly: parkedOnlySet
5011
+ parkedOnly: parkedOnlySet,
5012
+ identityDirectories
4997
5013
  };
4998
5014
  }
4999
5015
  function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
@@ -5052,7 +5068,7 @@ function rekeyPendingVerdict(status, hostedDelegateAddress, runtime, slug) {
5052
5068
  detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
5053
5069
  };
5054
5070
  }
5055
- async function runtimeSpecOverrideCheck(directory, sidecar, env) {
5071
+ async function runtimeSpecOverrideCheck(directory, sidecar, env, repairDir) {
5056
5072
  const facts = [];
5057
5073
  const installed = sidecar?.runtime_spec_override;
5058
5074
  if (installed) {
@@ -5081,7 +5097,7 @@ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
5081
5097
  label: "Runtime spec override",
5082
5098
  level: "failed",
5083
5099
  detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
5084
- repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>. If the override is intentional, this finding is the record of it.`
5100
+ repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>${credentialsDirFlagFor(repairDir)}. If the override is intentional, this finding is the record of it.`
5085
5101
  });
5086
5102
  }
5087
5103
  async function readMcpSidecarOverride(directory) {
@@ -5140,7 +5156,7 @@ async function verdictsForAgent(entry, input, deps) {
5140
5156
  label: "Signer runtime (preinstalled wrapper)",
5141
5157
  level: "failed",
5142
5158
  detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
5143
- repair: repairCommandFor(input.runtime)
5159
+ repair: repairCommandFor(input.runtime, "", input.repairDir)
5144
5160
  });
5145
5161
  } else if (sidecar.runtime_spec_override) {
5146
5162
  const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
@@ -5152,7 +5168,7 @@ async function verdictsForAgent(entry, input, deps) {
5152
5168
  label: "Signer runtime (preinstalled wrapper)",
5153
5169
  level: matches ? "ok" : "failed",
5154
5170
  detail: matches ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory} (override install \u2014 see runtime_spec_override)` : `Override runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
5155
- ...matches ? {} : { repair: repairCommandFor(input.runtime, " with the same HAVEN_*_SPEC variables set.") }
5171
+ ...matches ? {} : { repair: repairCommandFor(input.runtime, " with the same HAVEN_*_SPEC variables set.", input.repairDir) }
5156
5172
  });
5157
5173
  } else {
5158
5174
  const intact = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
@@ -5166,10 +5182,10 @@ async function verdictsForAgent(entry, input, deps) {
5166
5182
  label: "Signer runtime (preinstalled wrapper)",
5167
5183
  level: ok ? "ok" : intact ? "advisory" : "failed",
5168
5184
  detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : intact ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion} \u2014 intact, but outdated.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
5169
- ...ok ? {} : { repair: repairCommandFor(input.runtime) }
5185
+ ...ok ? {} : { repair: repairCommandFor(input.runtime, "", input.repairDir) }
5170
5186
  });
5171
5187
  }
5172
- const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
5188
+ const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env, input.repairDir);
5173
5189
  if (overrideCheck) checks.push(overrideCheck);
5174
5190
  const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
5175
5191
  if (identity?.api_key && hostedUrl) {
@@ -5259,7 +5275,7 @@ async function verdictsForAgent(entry, input, deps) {
5259
5275
  label: "Signer stdio handshake",
5260
5276
  level: probe.status === "ok" ? "ok" : "failed",
5261
5277
  detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
5262
- ...probe.status === "ok" ? {} : { repair: repairCommandFor(input.runtime) }
5278
+ ...probe.status === "ok" ? {} : { repair: repairCommandFor(input.runtime, "", input.repairDir) }
5263
5279
  });
5264
5280
  }
5265
5281
  } else {
@@ -5268,7 +5284,7 @@ async function verdictsForAgent(entry, input, deps) {
5268
5284
  label: "Signer stdio handshake",
5269
5285
  level: "failed",
5270
5286
  detail: "Skipped \u2014 no prepared signer runtime to probe.",
5271
- repair: repairCommandFor(input.runtime)
5287
+ repair: repairCommandFor(input.runtime, "", input.repairDir)
5272
5288
  });
5273
5289
  }
5274
5290
  return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
@@ -5277,7 +5293,8 @@ async function runDoctor(input, deps = {}) {
5277
5293
  const homeDir = deps.homeDir ?? os.homedir();
5278
5294
  const checks = [];
5279
5295
  let signerCapabilities;
5280
- const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5296
+ const { directory, others, parkedOnly, identityDirectories } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5297
+ const nameRepairDir = identityDirectories.length > 1 || Boolean(input.credentialsDir);
5281
5298
  const resolution = await resolveDoctorRuntime(input, directory);
5282
5299
  const runtime = resolution.runtime;
5283
5300
  const input2 = { ...input, runtime };
@@ -5334,7 +5351,11 @@ async function runDoctor(input, deps = {}) {
5334
5351
  ...rekeyPending ? { rekeyPending } : {}
5335
5352
  };
5336
5353
  if (wired) {
5337
- const result = await checksForAgent({ directory: dir, identity, sidecar }, input2, deps);
5354
+ const result = await checksForAgent(
5355
+ { directory: dir, identity, sidecar },
5356
+ { ...input2, ...nameRepairDir ? { repairDir: dir } : {} },
5357
+ deps
5358
+ );
5338
5359
  entry.checks = result.checks;
5339
5360
  capabilitiesByDirectory.set(dir, result.signerCapabilities);
5340
5361
  } else if (rekeyPending) {
@@ -5363,7 +5384,7 @@ async function runDoctor(input, deps = {}) {
5363
5384
  if (!primaryChecksById.has("credentials")) {
5364
5385
  const result = await checksForAgent(
5365
5386
  { directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
5366
- input2,
5387
+ { ...input2, ...nameRepairDir ? { repairDir: primaryDirectory } : {} },
5367
5388
  deps
5368
5389
  );
5369
5390
  signerCapabilities = result.signerCapabilities;
@@ -5404,7 +5425,7 @@ async function runDoctor(input, deps = {}) {
5404
5425
  label: "Runtime MCP config",
5405
5426
  level: "failed",
5406
5427
  detail: `No runtime config at ${configPath}.`,
5407
- repair: repairCommandFor(input2.runtime)
5428
+ repair: repairCommandFor(input2.runtime, "", nameRepairDir ? primaryDirectory : void 0)
5408
5429
  });
5409
5430
  } else {
5410
5431
  const primaryIdentity = await readIdentity(primaryDirectory ?? "");
@@ -5418,7 +5439,7 @@ async function runDoctor(input, deps = {}) {
5418
5439
  label: "Runtime MCP config",
5419
5440
  level: ok ? "ok" : "failed",
5420
5441
  detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
5421
- ...ok ? {} : { repair: repairCommandFor(input2.runtime) }
5442
+ ...ok ? {} : { repair: repairCommandFor(input2.runtime, "", nameRepairDir ? primaryDirectory : void 0) }
5422
5443
  });
5423
5444
  }
5424
5445
  for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
@@ -5561,7 +5582,25 @@ async function runDoctor(input, deps = {}) {
5561
5582
  async function runRepair(input, deps = {}) {
5562
5583
  const homeDir = deps.homeDir ?? os.homedir();
5563
5584
  const messages = [];
5564
- const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5585
+ const { directory, others, identityDirectories } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5586
+ if (!input.credentialsDir && identityDirectories.length > 1) {
5587
+ const lines = await Promise.all(
5588
+ identityDirectories.map(async (dir) => {
5589
+ const identity2 = await readIdentity(dir);
5590
+ const who = identity2?.agent_id ? `agent ${identity2.agent_id}` : "agent (no agent_id recorded)";
5591
+ const retired = !identity2?.api_key || await pathExists3(path.join(dir, TOMBSTONE_FILENAME));
5592
+ return retired ? ` \u2022 ${who} (${dir}): retired \u2014 not a repair target` : ` \u2022 ${who}: ${repairCommandFor(input.runtime, "", dir).replace(/^Run: /, "")}`;
5593
+ })
5594
+ );
5595
+ return {
5596
+ ok: false,
5597
+ messages: [
5598
+ `${identityDirectories.length} agent credential directories exist on this machine, and a repair rewrites the runtime config from ONE of them. Name the agent to repair with --credentials-dir \u2014 nothing was changed:`,
5599
+ ...lines,
5600
+ "Not sure which agent is which? Compare the agent ids against the Haven agent page, or run --doctor first."
5601
+ ]
5602
+ };
5603
+ }
5565
5604
  if (others.length > 0) {
5566
5605
  messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
5567
5606
  }