@agentvault/claude-bridge 0.5.3 → 0.5.5

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.
Files changed (2) hide show
  1. package/dist/index.js +161 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,10 +10,19 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/config.ts
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
14
14
  import { join as join5 } from "node:path";
15
+ function hasRecoverableBackup(dataDir) {
16
+ try {
17
+ const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
18
+ return !!(parsed && parsed.deviceId && parsed.deviceJwt && parsed.sessions && Object.keys(parsed.sessions).length > 0);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
15
23
  function hasPersistedCreds(dataDir) {
16
- return CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)));
24
+ if (CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)))) return true;
25
+ return hasRecoverableBackup(dataDir);
17
26
  }
18
27
  function slugify2(name) {
19
28
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
@@ -78,11 +87,12 @@ function loadConfig(env, argv = []) {
78
87
  armRoom
79
88
  };
80
89
  }
81
- var CRED_FILES;
90
+ var CRED_FILES, BACKUP_FILE;
82
91
  var init_config = __esm({
83
92
  "src/config.ts"() {
84
93
  "use strict";
85
94
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
95
+ BACKUP_FILE = "agentvault.json.bak";
86
96
  }
87
97
  });
88
98
 
@@ -64550,6 +64560,11 @@ function migratePersistedState(raw) {
64550
64560
  messageHistory: []
64551
64561
  };
64552
64562
  }
64563
+ function _isSelfHubAddress(ownHubAddress, target) {
64564
+ const own = (ownHubAddress ?? "").trim().toLowerCase();
64565
+ const tgt = (target ?? "").trim().toLowerCase();
64566
+ return own.length > 0 && own === tgt;
64567
+ }
64553
64568
  var ROOM_AGENT_TYPES;
64554
64569
  var CREDENTIAL_MESSAGE_TYPES;
64555
64570
  var CATCHUP_MESSAGE_TYPES;
@@ -64621,6 +64636,15 @@ var init_channel = __esm2({
64621
64636
  _ackTimer = null;
64622
64637
  _stopped = false;
64623
64638
  _persisted = null;
64639
+ // AgentVault display_name, resolved from GET /hub/identities (#499, A1).
64640
+ // hub_address alone isn't enough — its random suffix (wren-8789ee6d…) differs
64641
+ // from what the owner types (Wren). Empty until resolved; the DM preamble is
64642
+ // then omitted rather than asserting a wrong name. Re-resolved on every
64643
+ // connect (picks up renames; retries after a failed fetch).
64644
+ _ownDisplayName = "";
64645
+ // Dedupes concurrent resolves (connect-trigger + lazy DM-trigger) so a burst
64646
+ // of DMs can't stampede /hub/identities.
64647
+ _resolvingOwnDisplayName = null;
64624
64648
  _httpServer = null;
64625
64649
  _mcpServer = null;
64626
64650
  _pollFallbackTimer = null;
@@ -64652,7 +64676,7 @@ var init_channel = __esm2({
64652
64676
  _pendingMlsKpBundle;
64653
64677
  /**
64654
64678
  * Pool of pending KeyPackage bundles this device has published but not yet
64655
- * consumed by a Welcome. The backend now consumes published KeyPackages FIFO,
64679
+ * consumed by a Welcome. The backend consumes published KeyPackages newest-first (#508),
64656
64680
  * so a given room's creator may have been served ANY of these — `_handleMlsWelcome`
64657
64681
  * trial-decrypts the incoming Welcome against every bundle in the pool (plus the
64658
64682
  * connect KP and the persisted per-group bundle). Capped at `_KP_POOL_TARGET`;
@@ -64890,6 +64914,50 @@ var init_channel = __esm2({
64890
64914
  get deviceId() {
64891
64915
  return this._deviceId;
64892
64916
  }
64917
+ /** This agent's AgentVault display_name (#499, A1). "" until resolved. */
64918
+ get ownDisplayName() {
64919
+ return this._ownDisplayName;
64920
+ }
64921
+ /** This agent's AgentVault hub_address (#499, A1). "" until known. */
64922
+ get ownHubAddress() {
64923
+ return this._persisted?.hubAddress ?? "";
64924
+ }
64925
+ /** Public best-effort trigger to (re)resolve this agent's display_name.
64926
+ * Called lazily from the 1:1 DM path when the name is still unknown, so a
64927
+ * resolve that failed at connect (or a fresh agent that hadn't registered
64928
+ * yet) is retried without waiting for the next WS reconnect (#499, A1). */
64929
+ ensureOwnIdentityResolved() {
64930
+ return this._resolveOwnDisplayName();
64931
+ }
64932
+ /** Resolve this agent's own display_name from GET /api/v1/hub/identities
64933
+ * (matches its own device_id). Deduped — concurrent callers share one
64934
+ * in-flight fetch. Refreshes the cached value on success (picks up a rename);
64935
+ * on failure leaves the prior value intact. Best-effort — never throws.
64936
+ * Mirrors Hermes RestClient.get_own_identity (#499, Component 0). */
64937
+ _resolveOwnDisplayName() {
64938
+ if (this._resolvingOwnDisplayName) return this._resolvingOwnDisplayName;
64939
+ const p2 = (async () => {
64940
+ const jwt22 = this._persisted?.deviceJwt ?? this._deviceJwt;
64941
+ const deviceId = this._persisted?.deviceId ?? this._deviceId;
64942
+ if (!jwt22 || !deviceId) return;
64943
+ try {
64944
+ const res = await fetch(`${this.config.apiUrl}/api/v1/hub/identities`, {
64945
+ headers: { Authorization: `Bearer ${jwt22}` }
64946
+ });
64947
+ if (!res.ok) return;
64948
+ const list = await res.json();
64949
+ if (!Array.isArray(list)) return;
64950
+ const mine = list.find((x22) => x22?.device_id === deviceId);
64951
+ if (mine?.display_name) this._ownDisplayName = String(mine.display_name);
64952
+ } catch {
64953
+ }
64954
+ })();
64955
+ this._resolvingOwnDisplayName = p2;
64956
+ void p2.finally(() => {
64957
+ if (this._resolvingOwnDisplayName === p2) this._resolvingOwnDisplayName = null;
64958
+ });
64959
+ return p2;
64960
+ }
64893
64961
  get fingerprint() {
64894
64962
  return this._fingerprint;
64895
64963
  }
@@ -65434,7 +65502,7 @@ var init_channel = __esm2({
65434
65502
  */
65435
65503
  sendActivitySpan(spanData) {
65436
65504
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65437
- const pluginVersion = true ? "0.23.0" : "0.0.0-dev";
65505
+ const pluginVersion = true ? "0.23.2" : "0.0.0-dev";
65438
65506
  const agentName = this.config.agentName ?? "Agent";
65439
65507
  const resource = {
65440
65508
  "service.name": "agentvault-agent",
@@ -66413,6 +66481,11 @@ var init_channel = __esm2({
66413
66481
  if (!this._persisted?.hubAddress) {
66414
66482
  throw new Error("This agent does not have a hub address assigned");
66415
66483
  }
66484
+ if (_isSelfHubAddress(this._persisted.hubAddress, responderHubAddress)) {
66485
+ throw new Error(
66486
+ "Refusing to open an A2A channel to this agent's own hub address (self-reference). The owner is addressing you by your own name \u2014 reply to them directly instead of opening a channel to yourself."
66487
+ );
66488
+ }
66416
66489
  if (!this._deviceJwt) {
66417
66490
  throw new Error("Channel not authenticated");
66418
66491
  }
@@ -66493,6 +66566,11 @@ var init_channel = __esm2({
66493
66566
  * Falls back to plaintext for channels without a session (legacy/pre-encryption).
66494
66567
  */
66495
66568
  async sendToAgent(hubAddress, text, opts) {
66569
+ if (_isSelfHubAddress(this._persisted?.hubAddress ?? "", hubAddress)) {
66570
+ throw new Error(
66571
+ "Refusing to send an A2A message to this agent's own hub address (self-reference) \u2014 reply to the owner directly."
66572
+ );
66573
+ }
66496
66574
  if (!this._persisted?.a2aChannels) {
66497
66575
  try {
66498
66576
  await this.listA2AChannels();
@@ -67041,7 +67119,7 @@ var init_channel = __esm2({
67041
67119
  agentVersion: this.config.agentVersion ?? "0.0.0",
67042
67120
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67043
67121
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67044
- pluginVersion: true ? "0.23.0" : "0.0.0-dev"
67122
+ pluginVersion: true ? "0.23.2" : "0.0.0-dev"
67045
67123
  });
67046
67124
  this._telemetryReporter.startAutoFlush(3e4);
67047
67125
  }
@@ -67117,8 +67195,7 @@ var init_channel = __esm2({
67117
67195
  return;
67118
67196
  }
67119
67197
  if (data.event === "device_revoked") {
67120
- await clearState(this.config.dataDir);
67121
- this._handleError(new Error("Device was revoked"));
67198
+ await this._handleDeviceRevoked();
67122
67199
  return;
67123
67200
  }
67124
67201
  if (data.event === "device_linked") {
@@ -67342,6 +67419,7 @@ var init_channel = __esm2({
67342
67419
  this._persisted.agentHubId = data.data.hub_id;
67343
67420
  this._persisted.agentRole = data.data.agent_role ?? "peer";
67344
67421
  if (changed) this._persistState();
67422
+ void this._resolveOwnDisplayName();
67345
67423
  if (!this._telemetryReporter && this._persisted.deviceJwt && this._persisted.hubId) {
67346
67424
  this._telemetryReporter = new TelemetryReporter({
67347
67425
  apiBase: this.config.apiUrl,
@@ -67351,7 +67429,7 @@ var init_channel = __esm2({
67351
67429
  agentVersion: this.config.agentVersion ?? "0.0.0",
67352
67430
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67353
67431
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67354
- pluginVersion: true ? "0.23.0" : "0.0.0-dev"
67432
+ pluginVersion: true ? "0.23.2" : "0.0.0-dev"
67355
67433
  });
67356
67434
  this._telemetryReporter.startAutoFlush(3e4);
67357
67435
  }
@@ -70516,6 +70594,76 @@ ${messageText}`;
70516
70594
  this._setState("error");
70517
70595
  this.emit("error", err);
70518
70596
  }
70597
+ // Phase 2 — rolling-window timestamps of device_revoked self-heal reconnects,
70598
+ // used as a war cap so a takeover war or a server status/WS inconsistency can't
70599
+ // thrash forever.
70600
+ _revokeRecoveries = [];
70601
+ static REVOKE_RECOVERY_MAX = 3;
70602
+ static REVOKE_RECOVERY_WINDOW_MS = 12e4;
70603
+ /**
70604
+ * Handle a WS `device_revoked` event.
70605
+ *
70606
+ * The signal is ambiguous: the server sends it both for a genuine owner revoke
70607
+ * AND for a transient/racy session-takeover of a device that is still ACTIVE
70608
+ * (the error text even reads "or another session has taken over" — loopita's
70609
+ * case, where the device stayed ACTIVE on the server). So we do NOT terminate
70610
+ * blindly. We ask the PUBLIC /devices/{id}/status endpoint (no JWT needed, so
70611
+ * it answers even when our device JWT is being rejected):
70612
+ * - ACTIVE -> reconnect in place (transient); stay online.
70613
+ * - non-ACTIVE -> terminal (genuinely revoked; operator must re-enroll).
70614
+ * - unreachable/429 -> reconnect (inconclusive; a network blip shouldn't
70615
+ * permanently kill a healthy agent — the WS is down too,
70616
+ * so normal backoff handles it, and the war cap bounds
70617
+ * thrash if the device really is dead).
70618
+ *
70619
+ * A rolling-window cap (REVOKE_RECOVERY_MAX self-heals per WINDOW_MS) gives up
70620
+ * (terminal) on a repeating loop. Never clears credentials in any branch —
70621
+ * re-enrollment, which SHOULD wipe creds, goes through setup --force /
70622
+ * _forceReEnroll in start().
70623
+ */
70624
+ async _handleDeviceRevoked() {
70625
+ const now = Date.now();
70626
+ this._revokeRecoveries = this._revokeRecoveries.filter(
70627
+ (t22) => now - t22 < _SecureChannel.REVOKE_RECOVERY_WINDOW_MS
70628
+ );
70629
+ if (this._revokeRecoveries.length >= _SecureChannel.REVOKE_RECOVERY_MAX) {
70630
+ console.warn(
70631
+ "[SecureChannel] device_revoked self-healed too many times in a short window \u2014 giving up (terminal)"
70632
+ );
70633
+ this._handleError(new Error("Device was revoked"));
70634
+ return;
70635
+ }
70636
+ const deviceId = this._deviceId ?? this._persisted?.deviceId;
70637
+ if (!deviceId) {
70638
+ this._handleError(new Error("Device was revoked"));
70639
+ return;
70640
+ }
70641
+ let reconnect;
70642
+ try {
70643
+ const status = await pollDeviceStatus(this.config.apiUrl, deviceId);
70644
+ reconnect = status.rateLimited === true || status.status === "ACTIVE";
70645
+ if (!reconnect) {
70646
+ console.warn(
70647
+ `[SecureChannel] device_revoked confirmed by server (status=${status.status}) \u2014 terminal`
70648
+ );
70649
+ }
70650
+ } catch (err) {
70651
+ console.warn(
70652
+ "[SecureChannel] device_revoked status check failed; reconnecting (inconclusive):",
70653
+ err
70654
+ );
70655
+ reconnect = true;
70656
+ }
70657
+ if (reconnect) {
70658
+ this._revokeRecoveries.push(now);
70659
+ console.log(
70660
+ "[SecureChannel] device_revoked but device not confirmed dead \u2014 reconnecting (self-heal)"
70661
+ );
70662
+ this._scheduleReconnect();
70663
+ return;
70664
+ }
70665
+ this._handleError(new Error("Device was revoked"));
70666
+ }
70519
70667
  /**
70520
70668
  * Persist all ratchet session states to disk.
70521
70669
  * Syncs live ratchet states back into the persisted sessions map.
@@ -97028,7 +97176,7 @@ var init_index = __esm2({
97028
97176
  init_skill_invoker();
97029
97177
  await init_skill_telemetry();
97030
97178
  await init_policy_enforcer();
97031
- VERSION = true ? "0.23.0" : "0.0.0-dev";
97179
+ VERSION = true ? "0.23.2" : "0.0.0-dev";
97032
97180
  }
97033
97181
  });
97034
97182
  await init_index();
@@ -132488,7 +132636,7 @@ var ArmingState = class {
132488
132636
  };
132489
132637
 
132490
132638
  // src/approve-cli.ts
132491
- import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132639
+ import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132492
132640
  import { join as join6 } from "node:path";
132493
132641
  var APPROVALS_SUBDIR = "arm-approvals";
132494
132642
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
@@ -132521,7 +132669,7 @@ function drainApprovals(dataDir) {
132521
132669
  if (!ID_RE.test(name)) continue;
132522
132670
  let roomId = "";
132523
132671
  try {
132524
- roomId = readFileSync3(join6(dir, name), "utf8").trim();
132672
+ roomId = readFileSync4(join6(dir, name), "utf8").trim();
132525
132673
  } catch {
132526
132674
  }
132527
132675
  out.push({ requestId: name, roomId: ID_RE.test(roomId) ? roomId : "" });
@@ -132770,7 +132918,7 @@ async function main() {
132770
132918
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
132771
132919
  );
132772
132920
  }
132773
- console.error(`[bridge] version: ${true ? "0.5.3" : "dev"}`);
132921
+ console.error(`[bridge] version: ${true ? "0.5.5" : "dev"}`);
132774
132922
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
132775
132923
  if (cfg.worker) {
132776
132924
  console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",