@agentvault/claude-bridge 0.5.4 → 0.5.6

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 +178 -131
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -65024,145 +65024,140 @@ var init_channel = __esm2({
65024
65024
  }
65025
65025
  const raw = await loadState(this.config.dataDir);
65026
65026
  if (raw) {
65027
- await backupState(this.config.dataDir);
65028
- this._persisted = migratePersistedState(raw);
65029
- if (!this._persisted.messageHistory) {
65030
- this._persisted.messageHistory = [];
65031
- }
65032
- this._deviceId = this._persisted.deviceId;
65033
- this._deviceJwt = this._persisted.deviceJwt;
65034
- this._primaryConversationId = this._persisted.primaryConversationId;
65035
- this._fingerprint = this._persisted.fingerprint;
65036
- this._lastInboundRoomId = this._persisted.lastInboundRoomId;
65037
- if (this._persisted.seenMessageIds) {
65038
- this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
65039
- }
65040
- if (this._persisted.seenA2AMessageIds) {
65041
- this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
65042
- }
65043
- if (this._persisted?.rooms) {
65044
- const roomConvIds = /* @__PURE__ */ new Set();
65045
- for (const room of Object.values(this._persisted.rooms)) {
65046
- for (const cid of room.conversationIds || []) {
65047
- roomConvIds.add(cid);
65048
- }
65049
- }
65050
- let cleanedCount = 0;
65051
- for (const cid of roomConvIds) {
65052
- if (this._persisted.sessions[cid]) {
65053
- delete this._persisted.sessions[cid];
65054
- cleanedCount++;
65055
- }
65056
- }
65057
- if (cleanedCount > 0) {
65058
- console.log(`[SecureChannel] Cleaned ${cleanedCount} stale DR room sessions`);
65027
+ await this._hydrateFromPersisted(raw);
65028
+ this._connect();
65029
+ return;
65030
+ }
65031
+ const restored = await restoreState(this.config.dataDir);
65032
+ if (restored) {
65033
+ console.log("[SecureChannel] Restored state from backup");
65034
+ const restoredRaw = await loadState(this.config.dataDir);
65035
+ if (restoredRaw) {
65036
+ await this._hydrateFromPersisted(restoredRaw);
65037
+ this._connect();
65038
+ return;
65039
+ }
65040
+ }
65041
+ await this._enroll();
65042
+ }
65043
+ /**
65044
+ * Hydrate all in-memory state from persisted creds. The single source of
65045
+ * truth for both the primary boot and the .bak-restore boot, so the two can
65046
+ * never drift (the drift is what caused #416: the backup branch used to skip
65047
+ * MLS group loading and dropped owner MLS DMs). Callers invoke _connect()
65048
+ * after this resolves.
65049
+ */
65050
+ async _hydrateFromPersisted(raw) {
65051
+ await backupState(this.config.dataDir);
65052
+ this._persisted = migratePersistedState(raw);
65053
+ if (!this._persisted.messageHistory) {
65054
+ this._persisted.messageHistory = [];
65055
+ }
65056
+ this._deviceId = this._persisted.deviceId;
65057
+ this._deviceJwt = this._persisted.deviceJwt;
65058
+ this._primaryConversationId = this._persisted.primaryConversationId;
65059
+ this._fingerprint = this._persisted.fingerprint;
65060
+ this._lastInboundRoomId = this._persisted.lastInboundRoomId;
65061
+ if (this._persisted.seenMessageIds) {
65062
+ this._seenMessageIds = new Set(this._persisted.seenMessageIds.slice(-_SecureChannel.SEEN_MSG_MAX));
65063
+ }
65064
+ if (this._persisted.seenA2AMessageIds) {
65065
+ this._a2aSeenMessageIds = new Set(this._persisted.seenA2AMessageIds.slice(-_SecureChannel.A2A_SEEN_MAX));
65066
+ }
65067
+ if (this._persisted?.rooms) {
65068
+ const roomConvIds = /* @__PURE__ */ new Set();
65069
+ for (const room of Object.values(this._persisted.rooms)) {
65070
+ for (const cid of room.conversationIds || []) {
65071
+ roomConvIds.add(cid);
65059
65072
  }
65060
65073
  }
65061
- for (const [convId, sessionData] of Object.entries(
65062
- this._persisted.sessions
65063
- )) {
65064
- if (sessionData.ratchetState) {
65065
- const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
65066
- this._sessions.set(convId, {
65067
- ownerDeviceId: sessionData.ownerDeviceId,
65068
- ratchet,
65069
- activated: sessionData.activated ?? false,
65070
- epoch: sessionData.epoch
65071
- });
65074
+ let cleanedCount = 0;
65075
+ for (const cid of roomConvIds) {
65076
+ if (this._persisted.sessions[cid]) {
65077
+ delete this._persisted.sessions[cid];
65078
+ cleanedCount++;
65072
65079
  }
65073
65080
  }
65074
- if (this._persisted.conversationGroupIds) {
65075
- this._sessionGroupIds = new Map(Object.entries(this._persisted.conversationGroupIds));
65076
- console.log(`[SecureChannel] Restored ${this._sessionGroupIds.size} conversation\u2192group mappings`);
65081
+ if (cleanedCount > 0) {
65082
+ console.log(`[SecureChannel] Cleaned ${cleanedCount} stale DR room sessions`);
65077
65083
  }
65078
- if (this._persisted.rooms) {
65079
- for (const [roomId, room] of Object.entries(this._persisted.rooms)) {
65080
- const mlsGroupId = room.mlsGroupId;
65081
- if (mlsGroupId) {
65082
- try {
65083
- const mlsState = await loadMlsState(this.config.dataDir, mlsGroupId);
65084
- if (mlsState) {
65085
- const mgr = new MLSGroupManager();
65086
- mgr.importState(JSON.parse(mlsState));
65087
- this._mlsGroups.set(roomId, mgr);
65088
- }
65089
- } catch (mlsErr) {
65090
- console.warn(`[SecureChannel] Failed to restore MLS state for room ${roomId.slice(0, 8)}:`, mlsErr);
65091
- }
65092
- }
65093
- }
65084
+ }
65085
+ for (const [convId, sessionData] of Object.entries(
65086
+ this._persisted.sessions
65087
+ )) {
65088
+ if (sessionData.ratchetState) {
65089
+ const ratchet = DoubleRatchet.deserialize(sessionData.ratchetState);
65090
+ this._sessions.set(convId, {
65091
+ ownerDeviceId: sessionData.ownerDeviceId,
65092
+ ratchet,
65093
+ activated: sessionData.activated ?? false,
65094
+ epoch: sessionData.epoch
65095
+ });
65094
65096
  }
65095
- for (const [convId, entry] of Object.entries(this._persisted.mlsConversations ?? {})) {
65096
- if (entry.mlsGroupId) {
65097
+ }
65098
+ if (this._persisted.conversationGroupIds) {
65099
+ this._sessionGroupIds = new Map(Object.entries(this._persisted.conversationGroupIds));
65100
+ console.log(`[SecureChannel] Restored ${this._sessionGroupIds.size} conversation\u2192group mappings`);
65101
+ }
65102
+ if (this._persisted.rooms) {
65103
+ for (const [roomId, room] of Object.entries(this._persisted.rooms)) {
65104
+ const mlsGroupId = room.mlsGroupId;
65105
+ if (mlsGroupId) {
65097
65106
  try {
65098
- const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65107
+ const mlsState = await loadMlsState(this.config.dataDir, mlsGroupId);
65099
65108
  if (mlsState) {
65100
65109
  const mgr = new MLSGroupManager();
65101
65110
  mgr.importState(JSON.parse(mlsState));
65102
- this._mlsGroups.set(`conv:${convId}`, mgr);
65111
+ this._mlsGroups.set(roomId, mgr);
65103
65112
  }
65104
65113
  } catch (mlsErr) {
65105
- console.warn(`[SecureChannel] Failed to restore MLS state for conv ${convId.slice(0, 8)}:`, mlsErr);
65114
+ console.warn(`[SecureChannel] Failed to restore MLS state for room ${roomId.slice(0, 8)}:`, mlsErr);
65106
65115
  }
65107
65116
  }
65108
65117
  }
65109
- for (const [gid, entry] of Object.entries(this._persisted.mlsGroups ?? {})) {
65110
- if (!entry.mlsGroupId) continue;
65118
+ }
65119
+ for (const [convId, entry] of Object.entries(this._persisted.mlsConversations ?? {})) {
65120
+ if (entry.mlsGroupId) {
65111
65121
  try {
65112
- const stateJson = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65113
- if (stateJson) {
65122
+ const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65123
+ if (mlsState) {
65114
65124
  const mgr = new MLSGroupManager();
65115
- mgr.importState(JSON.parse(stateJson));
65116
- this._mlsGroups.set(`1to1-group:${gid}`, mgr);
65117
- console.log(`[SecureChannel] Loaded shared MLS group for ${gid.slice(0, 8)} (epoch=${mgr.epoch})`);
65125
+ mgr.importState(JSON.parse(mlsState));
65126
+ this._mlsGroups.set(`conv:${convId}`, mgr);
65118
65127
  }
65119
- } catch (loadErr) {
65120
- console.warn(`[SecureChannel] Failed to load shared MLS group ${gid.slice(0, 8)}:`, loadErr);
65128
+ } catch (mlsErr) {
65129
+ console.warn(`[SecureChannel] Failed to restore MLS state for conv ${convId.slice(0, 8)}:`, mlsErr);
65121
65130
  }
65122
65131
  }
65123
- for (const [channelId, entry] of Object.entries(this._persisted.a2aChannels ?? {})) {
65124
- if (entry.mlsGroupId) {
65125
- try {
65126
- const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65127
- if (mlsState) {
65128
- const mgr = new MLSGroupManager();
65129
- mgr.importState(JSON.parse(mlsState));
65130
- this._mlsGroups.set(`a2a:${channelId}`, mgr);
65131
- }
65132
- } catch (mlsErr) {
65133
- console.warn(`[SecureChannel] Failed to restore MLS state for A2A ${channelId.slice(0, 8)}:`, mlsErr);
65134
- }
65132
+ }
65133
+ for (const [gid, entry] of Object.entries(this._persisted.mlsGroups ?? {})) {
65134
+ if (!entry.mlsGroupId) continue;
65135
+ try {
65136
+ const stateJson = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65137
+ if (stateJson) {
65138
+ const mgr = new MLSGroupManager();
65139
+ mgr.importState(JSON.parse(stateJson));
65140
+ this._mlsGroups.set(`1to1-group:${gid}`, mgr);
65141
+ console.log(`[SecureChannel] Loaded shared MLS group for ${gid.slice(0, 8)} (epoch=${mgr.epoch})`);
65135
65142
  }
65143
+ } catch (loadErr) {
65144
+ console.warn(`[SecureChannel] Failed to load shared MLS group ${gid.slice(0, 8)}:`, loadErr);
65136
65145
  }
65137
- this._connect();
65138
- return;
65139
65146
  }
65140
- const restored = await restoreState(this.config.dataDir);
65141
- if (restored) {
65142
- console.log("[SecureChannel] Restored state from backup");
65143
- const restoredRaw = await loadState(this.config.dataDir);
65144
- if (restoredRaw) {
65145
- this._persisted = migratePersistedState(restoredRaw);
65146
- if (!this._persisted.messageHistory) this._persisted.messageHistory = [];
65147
- this._deviceId = this._persisted.deviceId;
65148
- this._deviceJwt = this._persisted.deviceJwt;
65149
- this._primaryConversationId = this._persisted.primaryConversationId;
65150
- this._fingerprint = this._persisted.fingerprint;
65151
- for (const [convId, sd] of Object.entries(this._persisted.sessions)) {
65152
- if (sd.ratchetState) {
65153
- this._sessions.set(convId, {
65154
- ownerDeviceId: sd.ownerDeviceId,
65155
- ratchet: DoubleRatchet.deserialize(sd.ratchetState),
65156
- activated: sd.activated ?? false,
65157
- epoch: sd.epoch
65158
- });
65147
+ for (const [channelId, entry] of Object.entries(this._persisted.a2aChannels ?? {})) {
65148
+ if (entry.mlsGroupId) {
65149
+ try {
65150
+ const mlsState = await loadMlsState(this.config.dataDir, entry.mlsGroupId);
65151
+ if (mlsState) {
65152
+ const mgr = new MLSGroupManager();
65153
+ mgr.importState(JSON.parse(mlsState));
65154
+ this._mlsGroups.set(`a2a:${channelId}`, mgr);
65159
65155
  }
65156
+ } catch (mlsErr) {
65157
+ console.warn(`[SecureChannel] Failed to restore MLS state for A2A ${channelId.slice(0, 8)}:`, mlsErr);
65160
65158
  }
65161
- this._connect();
65162
- return;
65163
65159
  }
65164
65160
  }
65165
- await this._enroll();
65166
65161
  }
65167
65162
  /**
65168
65163
  * Fetch scan rules from the server and load them into the ScanEngine.
@@ -65502,7 +65497,7 @@ var init_channel = __esm2({
65502
65497
  */
65503
65498
  sendActivitySpan(spanData) {
65504
65499
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65505
- const pluginVersion = true ? "0.23.0" : "0.0.0-dev";
65500
+ const pluginVersion = true ? "0.23.3" : "0.0.0-dev";
65506
65501
  const agentName = this.config.agentName ?? "Agent";
65507
65502
  const resource = {
65508
65503
  "service.name": "agentvault-agent",
@@ -67119,7 +67114,7 @@ var init_channel = __esm2({
67119
67114
  agentVersion: this.config.agentVersion ?? "0.0.0",
67120
67115
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67121
67116
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67122
- pluginVersion: true ? "0.23.0" : "0.0.0-dev"
67117
+ pluginVersion: true ? "0.23.3" : "0.0.0-dev"
67123
67118
  });
67124
67119
  this._telemetryReporter.startAutoFlush(3e4);
67125
67120
  }
@@ -67195,7 +67190,7 @@ var init_channel = __esm2({
67195
67190
  return;
67196
67191
  }
67197
67192
  if (data.event === "device_revoked") {
67198
- this._handleDeviceRevoked();
67193
+ await this._handleDeviceRevoked();
67199
67194
  return;
67200
67195
  }
67201
67196
  if (data.event === "device_linked") {
@@ -67429,7 +67424,7 @@ var init_channel = __esm2({
67429
67424
  agentVersion: this.config.agentVersion ?? "0.0.0",
67430
67425
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67431
67426
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67432
- pluginVersion: true ? "0.23.0" : "0.0.0-dev"
67427
+ pluginVersion: true ? "0.23.3" : "0.0.0-dev"
67433
67428
  });
67434
67429
  this._telemetryReporter.startAutoFlush(3e4);
67435
67430
  }
@@ -70594,22 +70589,74 @@ ${messageText}`;
70594
70589
  this._setState("error");
70595
70590
  this.emit("error", err);
70596
70591
  }
70592
+ // Phase 2 — rolling-window timestamps of device_revoked self-heal reconnects,
70593
+ // used as a war cap so a takeover war or a server status/WS inconsistency can't
70594
+ // thrash forever.
70595
+ _revokeRecoveries = [];
70596
+ static REVOKE_RECOVERY_MAX = 3;
70597
+ static REVOKE_RECOVERY_WINDOW_MS = 12e4;
70597
70598
  /**
70598
- * Handle a WS `device_revoked` event. Surfaces a terminal error so
70599
- * attachLifecycle stops the channel — but does NOT delete credentials.
70599
+ * Handle a WS `device_revoked` event.
70600
70600
  *
70601
- * This signal fires both for a genuine owner revoke AND for a transient
70602
- * session-takeover of a device that is still ACTIVE on the server (the error
70603
- * text even reads "or another session has taken over"). Deleting
70604
- * agentvault.json here as this path used to via clearState() strands a
70605
- * still-valid identity: the primary creds vanish, only the .bak survives, and
70606
- * the bridge's token-gate then demands a fresh invite token to restart.
70607
- * Keeping the creds means a restart reconnects via restoreState() when the
70608
- * device is still valid, and fails cleanly (device_revoked again, no data
70609
- * lost) when it isn't. Re-enrollment, which SHOULD wipe creds, goes through
70610
- * the explicit setup --force / _forceReEnroll path in start().
70601
+ * The signal is ambiguous: the server sends it both for a genuine owner revoke
70602
+ * AND for a transient/racy session-takeover of a device that is still ACTIVE
70603
+ * (the error text even reads "or another session has taken over" — loopita's
70604
+ * case, where the device stayed ACTIVE on the server). So we do NOT terminate
70605
+ * blindly. We ask the PUBLIC /devices/{id}/status endpoint (no JWT needed, so
70606
+ * it answers even when our device JWT is being rejected):
70607
+ * - ACTIVE -> reconnect in place (transient); stay online.
70608
+ * - non-ACTIVE -> terminal (genuinely revoked; operator must re-enroll).
70609
+ * - unreachable/429 -> reconnect (inconclusive; a network blip shouldn't
70610
+ * permanently kill a healthy agent the WS is down too,
70611
+ * so normal backoff handles it, and the war cap bounds
70612
+ * thrash if the device really is dead).
70613
+ *
70614
+ * A rolling-window cap (REVOKE_RECOVERY_MAX self-heals per WINDOW_MS) gives up
70615
+ * (terminal) on a repeating loop. Never clears credentials in any branch —
70616
+ * re-enrollment, which SHOULD wipe creds, goes through setup --force /
70617
+ * _forceReEnroll in start().
70611
70618
  */
70612
- _handleDeviceRevoked() {
70619
+ async _handleDeviceRevoked() {
70620
+ const now = Date.now();
70621
+ this._revokeRecoveries = this._revokeRecoveries.filter(
70622
+ (t22) => now - t22 < _SecureChannel.REVOKE_RECOVERY_WINDOW_MS
70623
+ );
70624
+ if (this._revokeRecoveries.length >= _SecureChannel.REVOKE_RECOVERY_MAX) {
70625
+ console.warn(
70626
+ "[SecureChannel] device_revoked self-healed too many times in a short window \u2014 giving up (terminal)"
70627
+ );
70628
+ this._handleError(new Error("Device was revoked"));
70629
+ return;
70630
+ }
70631
+ const deviceId = this._deviceId ?? this._persisted?.deviceId;
70632
+ if (!deviceId) {
70633
+ this._handleError(new Error("Device was revoked"));
70634
+ return;
70635
+ }
70636
+ let reconnect;
70637
+ try {
70638
+ const status = await pollDeviceStatus(this.config.apiUrl, deviceId);
70639
+ reconnect = status.rateLimited === true || status.status === "ACTIVE";
70640
+ if (!reconnect) {
70641
+ console.warn(
70642
+ `[SecureChannel] device_revoked confirmed by server (status=${status.status}) \u2014 terminal`
70643
+ );
70644
+ }
70645
+ } catch (err) {
70646
+ console.warn(
70647
+ "[SecureChannel] device_revoked status check failed; reconnecting (inconclusive):",
70648
+ err
70649
+ );
70650
+ reconnect = true;
70651
+ }
70652
+ if (reconnect) {
70653
+ this._revokeRecoveries.push(now);
70654
+ console.log(
70655
+ "[SecureChannel] device_revoked but device not confirmed dead \u2014 reconnecting (self-heal)"
70656
+ );
70657
+ this._scheduleReconnect();
70658
+ return;
70659
+ }
70613
70660
  this._handleError(new Error("Device was revoked"));
70614
70661
  }
70615
70662
  /**
@@ -97124,7 +97171,7 @@ var init_index = __esm2({
97124
97171
  init_skill_invoker();
97125
97172
  await init_skill_telemetry();
97126
97173
  await init_policy_enforcer();
97127
- VERSION = true ? "0.23.0" : "0.0.0-dev";
97174
+ VERSION = true ? "0.23.3" : "0.0.0-dev";
97128
97175
  }
97129
97176
  });
97130
97177
  await init_index();
@@ -132866,7 +132913,7 @@ async function main() {
132866
132913
  "[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"
132867
132914
  );
132868
132915
  }
132869
- console.error(`[bridge] version: ${true ? "0.5.4" : "dev"}`);
132916
+ console.error(`[bridge] version: ${true ? "0.5.6" : "dev"}`);
132870
132917
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
132871
132918
  if (cfg.worker) {
132872
132919
  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.4",
3
+ "version": "0.5.6",
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",