@agentvault/claude-bridge 0.7.4 → 0.7.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.
package/dist/bridge.d.ts CHANGED
@@ -34,6 +34,9 @@ export interface ArmingSnapshot {
34
34
  * its ABSENCE is how we identify a true owner↔agent 1:1 DM. */
35
35
  export interface MessageMeta {
36
36
  roomId?: string;
37
+ /** #621 — set by SecureChannel when the message decrypted under the owner's
38
+ * 1:1 material. Gates the full-access DM lane; see the `message` handler. */
39
+ ownerDm1to1?: boolean;
37
40
  }
38
41
  /** #392 room hush (advisory) — emitted by SecureChannel from the backend's
39
42
  * `room_hushed` event. ``hushedUntil`` is an ISO string, or null when cleared. */
package/dist/index.js CHANGED
@@ -452,12 +452,13 @@ import { join as join11 } from "node:path";
452
452
 
453
453
  // ../plugin/dist/index.js
454
454
  import * as nc from "node:crypto";
455
- import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
455
+ import { chmod, mkdir, writeFile } from "node:fs/promises";
456
+ import { readFile, rm } from "node:fs/promises";
456
457
  import { join } from "node:path";
457
- import { mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
458
+ import { readFile as readFile2, rename, rm as rm2 } from "node:fs/promises";
458
459
  import { join as join2 } from "node:path";
459
460
  import { Readable } from "node:stream";
460
- import { readdir, readFile as readFile3, writeFile as writeFile3, rename as rename2, stat, unlink, mkdir as mkdir3 } from "node:fs/promises";
461
+ import { readdir, readFile as readFile3, writeFile as writeFile2, rename as rename2, stat, unlink, mkdir as mkdir2 } from "node:fs/promises";
461
462
  import { join as join3 } from "node:path";
462
463
  import { randomUUID } from "node:crypto";
463
464
  import { EventEmitter } from "node:events";
@@ -465,7 +466,7 @@ import { createServer } from "node:http";
465
466
  import { randomUUID as randomUUID2, createHash } from "node:crypto";
466
467
  import { readFileSync } from "node:fs";
467
468
  import { homedir as osHomedir } from "node:os";
468
- import { writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
469
+ import { writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
469
470
  import { join as join4 } from "node:path";
470
471
  import { readFile as readFile4 } from "node:fs/promises";
471
472
  import WebSocket2 from "ws";
@@ -63664,16 +63665,39 @@ var init_dist = __esm2({
63664
63665
  init_mls_delivery_order();
63665
63666
  }
63666
63667
  });
63668
+ async function ensureSecureDir(dir) {
63669
+ await mkdir(dir, { recursive: true, mode: DIR_MODE });
63670
+ try {
63671
+ await chmod(dir, DIR_MODE);
63672
+ } catch {
63673
+ }
63674
+ }
63675
+ async function writeSecureFile(filePath, data, flag) {
63676
+ await writeFile(filePath, data, { encoding: "utf-8", mode: FILE_MODE, ...flag ? { flag } : {} });
63677
+ try {
63678
+ await chmod(filePath, FILE_MODE);
63679
+ } catch {
63680
+ }
63681
+ }
63682
+ var DIR_MODE;
63683
+ var FILE_MODE;
63684
+ var init_secure_file = __esm2({
63685
+ "src/secure-file.ts"() {
63686
+ "use strict";
63687
+ DIR_MODE = 448;
63688
+ FILE_MODE = 384;
63689
+ }
63690
+ });
63667
63691
  function syncLockPath(dataDir, channelId) {
63668
63692
  const safe = channelId.replace(/[^a-zA-Z0-9_-]/g, "_");
63669
63693
  return join(dataDir, `mls-a2a-sync-${safe}.lock`);
63670
63694
  }
63671
63695
  async function acquireA2ASyncLock(dataDir, channelId) {
63672
- await mkdir(dataDir, { recursive: true, mode: DIR_MODE });
63696
+ await ensureSecureDir(dataDir);
63673
63697
  const lockFile = syncLockPath(dataDir, channelId);
63674
63698
  const content = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
63675
63699
  try {
63676
- await writeFile(lockFile, content, { encoding: "utf-8", mode: FILE_MODE, flag: "wx" });
63700
+ await writeSecureFile(lockFile, content, "wx");
63677
63701
  return true;
63678
63702
  } catch (err) {
63679
63703
  if (err.code !== "EEXIST") throw err;
@@ -63684,7 +63708,7 @@ async function acquireA2ASyncLock(dataDir, channelId) {
63684
63708
  await rm(lockFile).catch(() => {
63685
63709
  });
63686
63710
  try {
63687
- await writeFile(lockFile, content, { encoding: "utf-8", mode: FILE_MODE, flag: "wx" });
63711
+ await writeSecureFile(lockFile, content, "wx");
63688
63712
  return true;
63689
63713
  } catch {
63690
63714
  return false;
@@ -63714,7 +63738,7 @@ async function hasPendingWelcome(dataDir, groupId) {
63714
63738
  }
63715
63739
  }
63716
63740
  async function savePendingKpBundle(dataDir, groupId, kp, serializePublicFn) {
63717
- await mkdir(dataDir, { recursive: true, mode: DIR_MODE });
63741
+ await ensureSecureDir(dataDir);
63718
63742
  const serialized = {
63719
63743
  publicPackageBytes: Buffer.from(serializePublicFn(kp.publicPackage)).toString("base64"),
63720
63744
  privatePackage: {
@@ -63723,7 +63747,7 @@ async function savePendingKpBundle(dataDir, groupId, kp, serializePublicFn) {
63723
63747
  signaturePrivateKey: Buffer.from(kp.privatePackage.signaturePrivateKey).toString("base64")
63724
63748
  }
63725
63749
  };
63726
- await writeFile(pendingKpPath(dataDir, groupId), JSON.stringify(serialized), { encoding: "utf-8", mode: FILE_MODE });
63750
+ await writeSecureFile(pendingKpPath(dataDir, groupId), JSON.stringify(serialized));
63727
63751
  }
63728
63752
  async function loadPendingKpBundle(dataDir, groupId) {
63729
63753
  try {
@@ -63748,9 +63772,9 @@ async function clearPendingWelcome(dataDir, groupId) {
63748
63772
  }
63749
63773
  }
63750
63774
  async function saveMlsState(dataDir, groupId, state) {
63751
- await mkdir(dataDir, { recursive: true, mode: DIR_MODE });
63775
+ await ensureSecureDir(dataDir);
63752
63776
  const filePath = join(dataDir, mlsFileName(groupId));
63753
- await writeFile(filePath, state, { encoding: "utf-8", mode: FILE_MODE });
63777
+ await writeSecureFile(filePath, state);
63754
63778
  }
63755
63779
  async function loadMlsState(dataDir, groupId) {
63756
63780
  try {
@@ -63767,15 +63791,12 @@ async function deleteMlsState(dataDir, groupId) {
63767
63791
  } catch {
63768
63792
  }
63769
63793
  }
63770
- var FILE_MODE;
63771
- var DIR_MODE;
63772
63794
  var SYNC_LOCK_STALE_MS;
63773
63795
  var pendingKpPath;
63774
63796
  var init_mls_state = __esm2({
63775
63797
  "src/mls-state.ts"() {
63776
63798
  "use strict";
63777
- FILE_MODE = 384;
63778
- DIR_MODE = 448;
63799
+ init_secure_file();
63779
63800
  SYNC_LOCK_STALE_MS = 5 * 60 * 1e3;
63780
63801
  pendingKpPath = (dataDir, groupId) => join(dataDir, `mls-kp-pending-${groupId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
63781
63802
  }
@@ -63926,9 +63947,9 @@ var init_crypto_helpers = __esm2({
63926
63947
  }
63927
63948
  });
63928
63949
  async function saveState(dataDir, state) {
63929
- await mkdir2(dataDir, { recursive: true, mode: DIR_MODE2 });
63950
+ await ensureSecureDir(dataDir);
63930
63951
  const filePath = join2(dataDir, STATE_FILE);
63931
- await writeFile2(filePath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: FILE_MODE2 });
63952
+ await writeSecureFile(filePath, JSON.stringify(state, null, 2));
63932
63953
  try {
63933
63954
  await rm2(join2(dataDir, LEGACY_STATE_FILE));
63934
63955
  } catch {
@@ -63945,8 +63966,8 @@ async function loadState(dataDir) {
63945
63966
  try {
63946
63967
  const raw = await readFile2(legacyPath, "utf-8");
63947
63968
  const parsed = JSON.parse(raw);
63948
- await mkdir2(dataDir, { recursive: true, mode: DIR_MODE2 });
63949
- await writeFile2(filePath, JSON.stringify(parsed, null, 2), { encoding: "utf-8", mode: FILE_MODE2 });
63969
+ await ensureSecureDir(dataDir);
63970
+ await writeSecureFile(filePath, JSON.stringify(parsed, null, 2));
63950
63971
  await rm2(legacyPath);
63951
63972
  return parsed;
63952
63973
  } catch {
@@ -63969,7 +63990,7 @@ async function backupState(dataDir) {
63969
63990
  const tmp = join2(dataDir, `${STATE_FILE}.bak.tmp`);
63970
63991
  try {
63971
63992
  const data = await readFile2(src, "utf-8");
63972
- await writeFile2(tmp, data, { encoding: "utf-8", mode: FILE_MODE2 });
63993
+ await writeSecureFile(tmp, data);
63973
63994
  await rename(tmp, bak);
63974
63995
  } catch {
63975
63996
  }
@@ -63984,8 +64005,8 @@ async function restoreState(dataDir) {
63984
64005
  if (!parsed || !parsed.deviceId || !parsed.deviceJwt || !parsed.sessions || Object.keys(parsed.sessions).length === 0) {
63985
64006
  return false;
63986
64007
  }
63987
- await mkdir2(dataDir, { recursive: true, mode: DIR_MODE2 });
63988
- await writeFile2(join2(dataDir, STATE_FILE), data, { encoding: "utf-8", mode: FILE_MODE2 });
64008
+ await ensureSecureDir(dataDir);
64009
+ await writeSecureFile(join2(dataDir, STATE_FILE), data);
63989
64010
  return true;
63990
64011
  } catch {
63991
64012
  return false;
@@ -64048,16 +64069,13 @@ async function clearState(dataDir) {
64048
64069
  }
64049
64070
  var STATE_FILE;
64050
64071
  var LEGACY_STATE_FILE;
64051
- var DIR_MODE2;
64052
- var FILE_MODE2;
64053
64072
  var init_state = __esm2({
64054
64073
  async "src/state.ts"() {
64055
64074
  "use strict";
64056
64075
  await init_dist();
64076
+ init_secure_file();
64057
64077
  STATE_FILE = "agentvault.json";
64058
64078
  LEGACY_STATE_FILE = "secure-channel.json";
64059
- DIR_MODE2 = 448;
64060
- FILE_MODE2 = 384;
64061
64079
  }
64062
64080
  });
64063
64081
  async function enrollDevice(apiUrl, inviteToken, identityPkHex, ephemeralPkHex, proofHex, platform) {
@@ -64486,11 +64504,11 @@ async function handleWorkspaceUpload(data, workspaceDir) {
64486
64504
  if (!verified) {
64487
64505
  return { status: "error", error: "Invalid signature \u2014 file may have been tampered with" };
64488
64506
  }
64489
- await mkdir3(workspaceDir, { recursive: true });
64507
+ await mkdir2(workspaceDir, { recursive: true });
64490
64508
  const targetPath = join3(workspaceDir, data.filename);
64491
64509
  const tempPath = join3(workspaceDir, `.tmp_${randomUUID()}_${data.filename}`);
64492
64510
  try {
64493
- await writeFile3(tempPath, data.content, "utf-8");
64511
+ await writeFile2(tempPath, data.content, "utf-8");
64494
64512
  await rename2(tempPath, targetPath);
64495
64513
  } catch (err) {
64496
64514
  try {
@@ -64616,6 +64634,8 @@ var OUTBOUND_DEDUPE_WINDOW_MS;
64616
64634
  var OUTBOUND_DEDUPE_MAX_SUPPRESS_MS;
64617
64635
  var OUTBOUND_DEDUPE_MAX;
64618
64636
  var OUTBOUND_DEDUPE_TYPES;
64637
+ var RETRY_CANDIDATE_TTL_MS;
64638
+ var RETRY_CANDIDATE_MAX;
64619
64639
  var SecureChannel;
64620
64640
  var init_channel = __esm2({
64621
64641
  async "src/channel.ts"() {
@@ -64653,6 +64673,8 @@ var init_channel = __esm2({
64653
64673
  OUTBOUND_DEDUPE_MAX_SUPPRESS_MS = 15e3;
64654
64674
  OUTBOUND_DEDUPE_MAX = 256;
64655
64675
  OUTBOUND_DEDUPE_TYPES = /* @__PURE__ */ new Set(["text", "status_alert", "artifact", "attachment"]);
64676
+ RETRY_CANDIDATE_TTL_MS = 6e4;
64677
+ RETRY_CANDIDATE_MAX = 32;
64656
64678
  SecureChannel = class _SecureChannel extends EventEmitter {
64657
64679
  constructor(config22) {
64658
64680
  super();
@@ -64772,6 +64794,20 @@ var init_channel = __esm2({
64772
64794
  * single delivery. In-memory only (short window; no cross-restart persistence needed).
64773
64795
  */
64774
64796
  _recentDeliveries = /* @__PURE__ */ new Map();
64797
+ /**
64798
+ * Last outbound plaintext per MLS group id (#732), so a message refused with
64799
+ * `unknown group` can be resent once the map is reconciled.
64800
+ *
64801
+ * PLAINTEXT, not the frame: the refused frame's `payload` is ciphertext
64802
+ * encrypted to a group that no longer exists, so replaying those bytes into
64803
+ * the replacement group would store something nobody can decrypt. The retry
64804
+ * has to encrypt again, which means keeping what was encrypted.
64805
+ *
64806
+ * In-memory only, TTL-bounded, and consumed on first use — this exists for
64807
+ * the ~50ms between a frame reaching the socket and the server refusing it,
64808
+ * not as a durable outbox (that is #688's dead-letter).
64809
+ */
64810
+ _retryCandidates = /* @__PURE__ */ new Map();
64775
64811
  _scanEngine = null;
64776
64812
  _scanRuleSetVersion = 0;
64777
64813
  _telemetryReporter = null;
@@ -64871,13 +64907,21 @@ var init_channel = __esm2({
64871
64907
  * Non-fatal: a transient network error or 5xx keeps the current JWT and
64872
64908
  * relies on the next reconnect. Terminal failures (401/403) surface via
64873
64909
  * the `auth_failed` event so callers can prompt the user to recover.
64910
+ *
64911
+ * Returns TRUE when the credentials are terminally dead. #743: the caller
64912
+ * MUST NOT go on to open a WebSocket in that case — the server has already
64913
+ * told us this credential can never be served, and connecting anyway is what
64914
+ * produced `ws_guard action=denylist` on repeat in prod (device 0e87ff50, a
64915
+ * hard-deleted device, ~12/hour). The verdict is returned explicitly rather
64916
+ * than read back off `_authFailedThisSession`, whose lifetime is reset per
64917
+ * connect and is easy to get subtly wrong.
64874
64918
  */
64875
64919
  async _maybeReissueDeviceJwt() {
64876
64920
  const jwt22 = this._persisted?.deviceJwt ?? this._deviceJwt;
64877
- if (!jwt22) return;
64921
+ if (!jwt22) return false;
64878
64922
  const exp = this._decodeJwtExp(jwt22);
64879
64923
  const nowSec = Math.floor(Date.now() / 1e3);
64880
- if (exp - nowSec >= 30 * 86400) return;
64924
+ if (exp - nowSec >= 30 * 86400) return false;
64881
64925
  try {
64882
64926
  const resp = await fetch(`${this.config.apiUrl}/api/v1/auth/reissue`, {
64883
64927
  method: "POST",
@@ -64897,7 +64941,7 @@ var init_channel = __esm2({
64897
64941
  } else {
64898
64942
  console.warn("[SecureChannel] reissue returned 200 but no device_jwt in response");
64899
64943
  }
64900
- return;
64944
+ return false;
64901
64945
  }
64902
64946
  if (resp.status === 401 || resp.status === 403) {
64903
64947
  console.warn(`[SecureChannel] reissue rejected (${resp.status}); credentials need attention`);
@@ -64905,12 +64949,13 @@ var init_channel = __esm2({
64905
64949
  const reason = resp.status === 401 ? "device_jwt_expired" : "device_revoked";
64906
64950
  await this._postAuthFailedToBackend(reason);
64907
64951
  this.emit("auth_failed", { reason });
64908
- return;
64952
+ return true;
64909
64953
  }
64910
64954
  console.warn(`[SecureChannel] reissue ${resp.status}; keeping current jwt`);
64911
64955
  } catch (err) {
64912
64956
  console.warn("[SecureChannel] reissue network error; keeping current jwt:", err);
64913
64957
  }
64958
+ return false;
64914
64959
  }
64915
64960
  /**
64916
64961
  * Wraps `fetch` for authenticated AV REST endpoints with reactive device-JWT
@@ -65396,7 +65441,9 @@ var init_channel = __esm2({
65396
65441
  }
65397
65442
  scanStatus = scanResult.status;
65398
65443
  }
65399
- this._appendHistory("agent", plaintext, topicId);
65444
+ if (!options?.isResend) {
65445
+ this._appendHistory("agent", plaintext, topicId);
65446
+ }
65400
65447
  const roomConvIds = /* @__PURE__ */ new Set();
65401
65448
  if (this._persisted?.rooms) {
65402
65449
  for (const room of Object.values(this._persisted.rooms)) {
@@ -65410,6 +65457,7 @@ var init_channel = __esm2({
65410
65457
  let sentCount = 0;
65411
65458
  const pendingWsSends = [];
65412
65459
  const sentSharedGroupIds = /* @__PURE__ */ new Set();
65460
+ const addressedMlsGroupIds = [];
65413
65461
  if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
65414
65462
  const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
65415
65463
  for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
@@ -65435,6 +65483,7 @@ var init_channel = __esm2({
65435
65483
  if (this._persisted?.hubAddress) payload.hub_address = this._persisted.hubAddress;
65436
65484
  if (this._persisted?.hubId) payload.sender_hub_id = this._persisted.hubId;
65437
65485
  pendingWsSends.push(JSON.stringify({ event: "message_mls", data: payload }));
65486
+ addressedMlsGroupIds.push(entry.mlsGroupId);
65438
65487
  sentSharedGroupIds.add(gid);
65439
65488
  sentCount++;
65440
65489
  console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
@@ -65487,6 +65536,7 @@ var init_channel = __esm2({
65487
65536
  data: payload
65488
65537
  })
65489
65538
  );
65539
+ addressedMlsGroupIds.push(mlsGroupId);
65490
65540
  if (convGroupId) sentSharedGroupIds.add(convGroupId);
65491
65541
  } else {
65492
65542
  const encrypted = session.ratchet.encrypt(plaintext);
@@ -65572,6 +65622,7 @@ var init_channel = __esm2({
65572
65622
  if (this._persisted?.hubAddress) payload.hub_address = this._persisted.hubAddress;
65573
65623
  if (this._persisted?.hubId) payload.sender_hub_id = this._persisted.hubId;
65574
65624
  pendingWsSends.push(JSON.stringify({ event: "message_mls", data: payload }));
65625
+ addressedMlsGroupIds.push(resolvedMlsGroupId);
65575
65626
  sentCount++;
65576
65627
  if (mlsOnlyConvGroupId) sentSharedGroupIds.add(mlsOnlyConvGroupId);
65577
65628
  console.log(`[SecureChannel] MLS-only send for conv ${mlsConvId.slice(0, 8)} (no DR session)`);
@@ -65587,6 +65638,28 @@ var init_channel = __esm2({
65587
65638
  for (const frame of pendingWsSends) {
65588
65639
  this._ws.send(frame);
65589
65640
  }
65641
+ if (!options?.isResend) {
65642
+ for (const mlsGroupId of addressedMlsGroupIds) {
65643
+ this._rememberRetryCandidate(mlsGroupId, plaintext, options);
65644
+ }
65645
+ }
65646
+ }
65647
+ /**
65648
+ * Retain one outbound plaintext against the MLS group it was sent to (#732).
65649
+ * Evicts expired entries, then the oldest, so the map cannot grow unbounded
65650
+ * on an agent that sends steadily and is never refused.
65651
+ */
65652
+ _rememberRetryCandidate(mlsGroupId, plaintext, options) {
65653
+ const now = Date.now();
65654
+ for (const [k2, v22] of this._retryCandidates) {
65655
+ if (now - v22.ts > RETRY_CANDIDATE_TTL_MS) this._retryCandidates.delete(k2);
65656
+ }
65657
+ this._retryCandidates.set(mlsGroupId, { plaintext, options, ts: now });
65658
+ while (this._retryCandidates.size > RETRY_CANDIDATE_MAX) {
65659
+ const oldest = this._retryCandidates.keys().next().value;
65660
+ if (!oldest) break;
65661
+ this._retryCandidates.delete(oldest);
65662
+ }
65590
65663
  }
65591
65664
  /**
65592
65665
  * Send a typing indicator to all owner devices.
@@ -65614,7 +65687,7 @@ var init_channel = __esm2({
65614
65687
  */
65615
65688
  sendActivitySpan(spanData) {
65616
65689
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65617
- const pluginVersion = true ? "0.23.14" : "0.0.0-dev";
65690
+ const pluginVersion = true ? "0.23.18" : "0.0.0-dev";
65618
65691
  const agentName = this.config.agentName ?? "Agent";
65619
65692
  const resource = {
65620
65693
  "service.name": "agentvault-agent",
@@ -65959,13 +66032,20 @@ var init_channel = __esm2({
65959
66032
  * entry, then persist.
65960
66033
  */
65961
66034
  async _handleServerError(payload) {
65962
- if (payload?.detail !== "conversation deleted") return;
66035
+ const detail = payload?.detail;
66036
+ if (detail !== "conversation deleted" && detail !== "unknown group") return;
65963
66037
  const mlsGroupId = payload.group_id;
65964
66038
  if (!mlsGroupId) return;
65965
66039
  const groups = this._persisted?.mlsGroups;
65966
66040
  if (!groups) return;
65967
66041
  const gid = Object.keys(groups).find((k2) => groups[k2]?.mlsGroupId === mlsGroupId);
65968
66042
  if (!gid) return;
66043
+ if (detail === "unknown group") {
66044
+ const changed = await this._reconcileConversationGroups(gid, mlsGroupId);
66045
+ if (changed) await this._retryAfterReconcile(mlsGroupId);
66046
+ return;
66047
+ }
66048
+ this._retryCandidates.delete(mlsGroupId);
65969
66049
  try {
65970
66050
  await deleteMlsState(this.config.dataDir, mlsGroupId);
65971
66051
  } catch {
@@ -65978,6 +66058,120 @@ var init_channel = __esm2({
65978
66058
  );
65979
66059
  this.emit("dm_group_pruned", { conversationGroupId: gid, mlsGroupId });
65980
66060
  }
66061
+ /**
66062
+ * Re-resolve the conversation→group map from the server (#719).
66063
+ *
66064
+ * `_persisted.conversationGroupIds` and `_persisted.groupId` are built ONCE,
66065
+ * in `_activate()`, from the activation response. `_activate` runs at
66066
+ * enrollment and never again, so when the owner deletes a conversation and a
66067
+ * new one is created the agent keeps addressing the OLD group forever.
66068
+ * Nothing reconciled it — not restart (the stale map is reloaded from disk),
66069
+ * not reconnect, not the heartbeat.
66070
+ *
66071
+ * wren, 2026-07-31: `groupId` 53798dd2 had no `mls_groups` row while three
66072
+ * ACTIVE conversations sat on 99faf707. Inbound worked, every reply was
66073
+ * refused `unknown group` and dropped. Silent since 2026-07-02 — the #460
66074
+ * fan-out was spraying all 12 known groups, so a live one always got hit.
66075
+ *
66076
+ * Best-effort and conservative, mirroring `_refreshRoomRoster`: on any
66077
+ * failure the existing map is kept. The prune is NARROW — only the group the
66078
+ * server named, and only once the server has positively reported an active
66079
+ * conversation on some other group. Absence of data is never treated as
66080
+ * evidence of deletion.
66081
+ *
66082
+ * Returns TRUE only when the PRIMARY group pointer actually moved, which is
66083
+ * the signal #732's resend is gated on. Deliberately narrower than "anything
66084
+ * in the map changed": if some other group went stale we cannot tell which
66085
+ * conversation replaced it, and delivering a reply to the wrong counterparty
66086
+ * is strictly worse than losing it.
66087
+ */
66088
+ async _reconcileConversationGroups(staleGid, staleMlsGroupId) {
66089
+ const jwt22 = this._deviceJwt ?? this._persisted?.deviceJwt;
66090
+ if (!jwt22 || !this._persisted) return false;
66091
+ let rows;
66092
+ try {
66093
+ const res = await fetch(`${this.config.apiUrl}/api/v1/conversations`, {
66094
+ headers: { Authorization: `Bearer ${jwt22}` }
66095
+ });
66096
+ if (!res.ok) return false;
66097
+ rows = await res.json();
66098
+ } catch (err) {
66099
+ console.warn("[SecureChannel] Conversation re-resolve failed:", err);
66100
+ return false;
66101
+ }
66102
+ if (!Array.isArray(rows)) return false;
66103
+ const mine = rows.filter(
66104
+ (r22) => r22?.agent_device_id === this._deviceId && r22?.status === "active" && typeof r22?.group_id === "string" && typeof r22?.id === "string"
66105
+ );
66106
+ if (mine.length === 0) return false;
66107
+ const before = this._persisted.groupId;
66108
+ this._sessionGroupIds = new Map(mine.map((r22) => [r22.id, r22.group_id]));
66109
+ this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
66110
+ const primary = mine.find((r22) => r22.id === this._persisted.primaryConversationId) ?? mine[0];
66111
+ this._persisted.groupId = primary.group_id;
66112
+ this._persisted.primaryConversationId = primary.id;
66113
+ const liveGroupIds = new Set(mine.map((r22) => r22.group_id));
66114
+ if (!liveGroupIds.has(staleGid)) {
66115
+ try {
66116
+ await deleteMlsState(this.config.dataDir, staleMlsGroupId);
66117
+ } catch {
66118
+ }
66119
+ this._mlsGroups.delete(`1to1-group:${staleGid}`);
66120
+ delete this._persisted.mlsGroups?.[staleGid];
66121
+ this.emit("dm_group_pruned", {
66122
+ conversationGroupId: staleGid,
66123
+ mlsGroupId: staleMlsGroupId
66124
+ });
66125
+ }
66126
+ await this._persistState();
66127
+ console.log(
66128
+ `[SecureChannel] Re-resolved conversation groups after 'unknown group' ${staleMlsGroupId.slice(0, 8)}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
66129
+ );
66130
+ return this._persisted.groupId !== before && before === staleGid;
66131
+ }
66132
+ /**
66133
+ * Resend the message that a now-reconciled `unknown group` refusal killed (#732).
66134
+ *
66135
+ * Called ONLY after `_reconcileConversationGroups` reported that the primary
66136
+ * group pointer moved off the refused group, so there is exactly one sensible
66137
+ * destination: the conversation the reconcile settled on.
66138
+ *
66139
+ * The candidate is consumed BEFORE the resend, not after — one attempt is the
66140
+ * bound, and that must hold even if the resend itself throws. A retry that
66141
+ * re-armed on failure would turn a persistent refusal into an infinite loop,
66142
+ * which is a worse failure than the dropped message it set out to fix.
66143
+ *
66144
+ * The resend is always TARGETED, never a broadcast: the original send may
66145
+ * have fanned out to several groups of which only one was refused, and
66146
+ * re-broadcasting would duplicate the message in all the others.
66147
+ */
66148
+ async _retryAfterReconcile(staleMlsGroupId) {
66149
+ const candidate = this._retryCandidates.get(staleMlsGroupId);
66150
+ this._retryCandidates.delete(staleMlsGroupId);
66151
+ if (!candidate) return;
66152
+ if (Date.now() - candidate.ts > RETRY_CANDIDATE_TTL_MS) {
66153
+ console.warn(
66154
+ `[deliver] delivery_result=LOST group=${staleMlsGroupId.slice(0, 8)} reason=retry-window-expired`
66155
+ );
66156
+ return;
66157
+ }
66158
+ const conversationId = this._persisted?.primaryConversationId;
66159
+ if (!conversationId) return;
66160
+ try {
66161
+ await this.send(candidate.plaintext, {
66162
+ ...candidate.options,
66163
+ conversationId,
66164
+ isResend: true
66165
+ });
66166
+ console.log(
66167
+ `[deliver] delivery_result=RESENT group=${staleMlsGroupId.slice(0, 8)} \u2192 ${String(this._persisted?.groupId).slice(0, 8)} conv=${conversationId.slice(0, 8)}`
66168
+ );
66169
+ } catch (err) {
66170
+ console.error(
66171
+ `[deliver] delivery_result=LOST group=${staleMlsGroupId.slice(0, 8)} reason=resend-failed detail=${err instanceof Error ? err.message : String(err)}`
66172
+ );
66173
+ }
66174
+ }
65981
66175
  /**
65982
66176
  * Return info for all joined rooms.
65983
66177
  */
@@ -66350,7 +66544,7 @@ var init_channel = __esm2({
66350
66544
  });
66351
66545
  }
66352
66546
  const targetLabel = resolved.kind === "owner" ? "owner" : `${resolved.kind}:${resolved.id?.slice(0, 8)}...`;
66353
- console.log(`[deliver] target=${targetLabel} content=${content.type} result=ok`);
66547
+ console.log(`[deliver] target=${targetLabel} content=${content.type} result=sent`);
66354
66548
  if (dedupeKey) {
66355
66549
  try {
66356
66550
  const now = Date.now();
@@ -67244,13 +67438,12 @@ var init_channel = __esm2({
67244
67438
  sessions,
67245
67439
  messageHistory: this._persisted.messageHistory ?? []
67246
67440
  };
67247
- if (conversations.length > 0) {
67248
- const firstConv = conversations[0];
67249
- if (firstConv.group_id) {
67250
- this._persisted.groupId = firstConv.group_id;
67441
+ if (primary) {
67442
+ if (primary.group_id) {
67443
+ this._persisted.groupId = primary.group_id;
67251
67444
  }
67252
- if (firstConv.default_topic_id) {
67253
- this._persisted.defaultTopicId = firstConv.default_topic_id;
67445
+ if (primary.default_topic_id) {
67446
+ this._persisted.defaultTopicId = primary.default_topic_id;
67254
67447
  }
67255
67448
  }
67256
67449
  this._sessionGroupIds = /* @__PURE__ */ new Map();
@@ -67312,7 +67505,13 @@ var init_channel = __esm2({
67312
67505
  this._ws = null;
67313
67506
  }
67314
67507
  this._setState("connecting");
67315
- await this._maybeReissueDeviceJwt();
67508
+ if (await this._maybeReissueDeviceJwt()) {
67509
+ console.warn(
67510
+ "[SecureChannel] credentials are terminally dead \u2014 aborting connect (no WS attempt)"
67511
+ );
67512
+ this._setState("error");
67513
+ return;
67514
+ }
67316
67515
  const wsUrl = this.config.apiUrl.replace(/^http/, "ws");
67317
67516
  const url22 = `${wsUrl}/api/v1/ws?token=${encodeURIComponent(this._deviceJwt)}&device_id=${this._deviceId}`;
67318
67517
  const ws = new WebSocket2(url22);
@@ -67353,7 +67552,7 @@ var init_channel = __esm2({
67353
67552
  agentVersion: this.config.agentVersion ?? "0.0.0",
67354
67553
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67355
67554
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67356
- pluginVersion: true ? "0.23.14" : "0.0.0-dev"
67555
+ pluginVersion: true ? "0.23.18" : "0.0.0-dev"
67357
67556
  });
67358
67557
  this._telemetryReporter.startAutoFlush(3e4);
67359
67558
  }
@@ -67422,7 +67621,12 @@ var init_channel = __esm2({
67422
67621
  try {
67423
67622
  const data = JSON.parse(raw.toString());
67424
67623
  if (data.event && data.event !== "ping" && data.event !== "typing") {
67425
- console.log(`[SecureChannel] WS event: ${data.event} conv=${(data.conversation_id || data.data?.conversation_id || "").toString().slice(0, 8)}`);
67624
+ const d22 = data.data ?? {};
67625
+ const conv = (data.conversation_id || d22.conversation_id || "").toString();
67626
+ const parts = [`conv=${conv.slice(0, 8)}`];
67627
+ if (d22.group_id) parts.push(`group=${String(d22.group_id).slice(0, 8)}`);
67628
+ if (d22.detail) parts.push(`detail=${d22.detail}`);
67629
+ console.log(`[SecureChannel] WS event: ${data.event} ${parts.join(" ")}`);
67426
67630
  }
67427
67631
  if (data.event === "ping") {
67428
67632
  ws.send(JSON.stringify({ event: "pong" }));
@@ -67671,7 +67875,7 @@ var init_channel = __esm2({
67671
67875
  agentVersion: this.config.agentVersion ?? "0.0.0",
67672
67876
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67673
67877
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67674
- pluginVersion: true ? "0.23.14" : "0.0.0-dev"
67878
+ pluginVersion: true ? "0.23.18" : "0.0.0-dev"
67675
67879
  });
67676
67880
  this._telemetryReporter.startAutoFlush(3e4);
67677
67881
  }
@@ -67980,6 +68184,10 @@ var init_channel = __esm2({
67980
68184
  }
67981
68185
  if (data.event === "error") {
67982
68186
  const detail = data.data?.detail || data.detail || "Unknown server error";
68187
+ const rejectedGroup = String(data.data?.group_id ?? "").slice(0, 8);
68188
+ console.error(
68189
+ `[deliver] delivery_result=REJECTED group=${rejectedGroup || "unknown"} detail=${detail}`
68190
+ );
67983
68191
  console.error(`[SecureChannel] Server error: ${detail}`);
67984
68192
  await this._handleServerError(data.data || data);
67985
68193
  this.emit("error", new Error(`Server: ${detail}`));
@@ -68151,6 +68359,10 @@ var init_channel = __esm2({
68151
68359
  conversationId: convId ?? this._primaryConversationId,
68152
68360
  timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
68153
68361
  topicId,
68362
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
68363
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
68364
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
68365
+ ownerDm1to1: true,
68154
68366
  messageType
68155
68367
  };
68156
68368
  this.emit("message", text, metadata);
@@ -68393,7 +68605,7 @@ ${messageText}`;
68393
68605
  */
68394
68606
  async _downloadAndDecryptAttachment(info) {
68395
68607
  const attachDir = join4(this.config.dataDir, "attachments");
68396
- await mkdir4(attachDir, { recursive: true });
68608
+ await mkdir3(attachDir, { recursive: true });
68397
68609
  const url22 = `${this.config.apiUrl}${info.blobUrl}`;
68398
68610
  const res = await fetch(url22, {
68399
68611
  headers: { Authorization: `Bearer ${this._deviceJwt}` }
@@ -68411,7 +68623,7 @@ ${messageText}`;
68411
68623
  const fileNonce = base64ToBytes(info.fileNonce);
68412
68624
  const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
68413
68625
  const filePath = join4(attachDir, info.filename);
68414
- await writeFile4(filePath, decrypted);
68626
+ await writeFile3(filePath, decrypted);
68415
68627
  console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
68416
68628
  return { filePath, decrypted };
68417
68629
  }
@@ -69066,10 +69278,10 @@ ${messageText}`;
69066
69278
  await mlsGroup.processCommit(commitBytes);
69067
69279
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69068
69280
  console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69281
+ this._mlsCommitFailCounts.delete(roomId);
69069
69282
  } catch (err) {
69070
69283
  await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
69071
69284
  }
69072
- this._mlsCommitFailCounts.delete(roomId);
69073
69285
  } else {
69074
69286
  this._bufferMlsCommit(groupId, epoch, data);
69075
69287
  console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69086,10 +69298,10 @@ ${messageText}`;
69086
69298
  await mlsGroup.processCommit(commitBytes);
69087
69299
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69088
69300
  console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69301
+ this._mlsCommitFailCounts.delete(`a2a:${chId}`);
69089
69302
  } catch (err) {
69090
69303
  await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
69091
69304
  }
69092
- this._mlsCommitFailCounts.delete(`a2a:${chId}`);
69093
69305
  } else {
69094
69306
  this._bufferMlsCommit(groupId, epoch, data);
69095
69307
  console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69431,7 +69643,7 @@ ${messageText}`;
69431
69643
  console.warn("[SecureChannel] KeyPackage pool replenish after join failed:", err);
69432
69644
  });
69433
69645
  console.log(`[SecureChannel] Welcome joined: group=${groupId?.slice(0, 8)} kpSource=${kpSource} welcomeLen=${welcomeBytes.length} candidates=${candidateCount} poolRemaining=${this._pendingKpBundles.length}`);
69434
- if (conversationGroupId) {
69646
+ if (conversationGroupId && !data.room_id) {
69435
69647
  const key = `1to1-group:${conversationGroupId}`;
69436
69648
  this._mlsGroups.set(key, mgr);
69437
69649
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
@@ -70252,7 +70464,11 @@ ${messageText}`;
70252
70464
  messageId: msg.message_id,
70253
70465
  conversationId: msg.conversation_id,
70254
70466
  timestamp: msg.created_at,
70255
- topicId
70467
+ topicId,
70468
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
70469
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
70470
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
70471
+ ownerDm1to1: true
70256
70472
  };
70257
70473
  this.emit("message", messageText, metadata);
70258
70474
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -70584,7 +70800,11 @@ ${messageText}`;
70584
70800
  messageId: msg.id,
70585
70801
  conversationId: msg.conversation_id,
70586
70802
  timestamp: msg.created_at,
70587
- topicId
70803
+ topicId,
70804
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
70805
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
70806
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
70807
+ ownerDm1to1: true
70588
70808
  };
70589
70809
  this.emit("message", messageText, metadata);
70590
70810
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -70865,7 +71085,11 @@ ${messageText}`;
70865
71085
  messageId: msg.id,
70866
71086
  conversationId: msg.conversation_id,
70867
71087
  timestamp: msg.created_at,
70868
- topicId
71088
+ topicId,
71089
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
71090
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
71091
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
71092
+ ownerDm1to1: true
70869
71093
  };
70870
71094
  this.emit("message", messageText, metadata);
70871
71095
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -70959,7 +71183,7 @@ ${messageText}`;
70959
71183
  return;
70960
71184
  }
70961
71185
  this._authFailedThisSession = true;
70962
- const authReason = data?.reason === "device_jwt_expired" ? "device_jwt_expired" : "device_revoked";
71186
+ const authReason = data?.reason ?? "device_revoked";
70963
71187
  console.warn(
70964
71188
  `[SecureChannel] connection_rejected (reason=${data?.reason ?? "unknown"}, retryable=false) \u2014 terminal; surfacing auth_failed`
70965
71189
  );
@@ -71123,6 +71347,14 @@ function terminalReenrollMessage(agentName, reason) {
71123
71347
  const who = agentName ? `[${agentName}] ` : "";
71124
71348
  return `${who}device is no longer valid (${reason}) \u2014 it was revoked or replaced. Re-enroll with a fresh token from AgentVault; the old credentials are dead. This agent will stop reconnecting.`;
71125
71349
  }
71350
+ function formatChannelError(err) {
71351
+ const msg = String(err);
71352
+ const rejection = /Server: (.+)$/.exec(msg);
71353
+ if (rejection) {
71354
+ return `[AgentVault] DELIVERY FAILED \u2014 the server refused this frame: ${rejection[1]}. The connection survives; this message was dropped and nothing retries it.`;
71355
+ }
71356
+ return `[AgentVault] channel error (non-fatal to the connection): ${msg}`;
71357
+ }
71126
71358
  function attachLifecycle(channel, opts = {}) {
71127
71359
  const log = opts.log ?? (() => {
71128
71360
  });
@@ -71323,7 +71555,7 @@ var init_openclaw_plugin = __esm2({
71323
71555
  });
71324
71556
  _channels.set(account.accountId, channel);
71325
71557
  channel.on("error", (err) => {
71326
- _log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
71558
+ _log?.(formatChannelError(err));
71327
71559
  });
71328
71560
  attachLifecycle(channel, {
71329
71561
  agentName: account.agentName,
@@ -71460,6 +71692,11 @@ var init_fetch_interceptor = __esm2({
71460
71692
  traceStore = new AsyncLocalStorage();
71461
71693
  }
71462
71694
  });
71695
+ var init_tool_audit = __esm2({
71696
+ "src/tool-audit.ts"() {
71697
+ "use strict";
71698
+ }
71699
+ });
71463
71700
  var isUsingManagedRoutes;
71464
71701
  var init_openclaw_entry = __esm2({
71465
71702
  "src/openclaw-entry.ts"() {
@@ -71469,6 +71706,7 @@ var init_openclaw_entry = __esm2({
71469
71706
  init_http_handlers();
71470
71707
  init_openclaw_compat();
71471
71708
  init_lifecycle();
71709
+ init_tool_audit();
71472
71710
  init_types();
71473
71711
  isUsingManagedRoutes = false;
71474
71712
  }
@@ -97520,7 +97758,7 @@ var init_index = __esm2({
97520
97758
  init_skill_invoker();
97521
97759
  await init_skill_telemetry();
97522
97760
  await init_policy_enforcer();
97523
- VERSION = true ? "0.23.14" : "0.0.0-dev";
97761
+ VERSION = true ? "0.23.18" : "0.0.0-dev";
97524
97762
  }
97525
97763
  });
97526
97764
  await init_index();
@@ -118831,8 +119069,8 @@ import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync
118831
119069
  import { join as join6 } from "node:path";
118832
119070
  import { homedir, hostname as hostname3 } from "node:os";
118833
119071
  var TRUST_SUBDIR = "host-trust";
118834
- var DIR_MODE3 = 448;
118835
- var FILE_MODE3 = 384;
119072
+ var DIR_MODE2 = 448;
119073
+ var FILE_MODE2 = 384;
118836
119074
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
118837
119075
  var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
118838
119076
  var HostTrustError = class extends Error {
@@ -118859,15 +119097,15 @@ function grant(deviceId, root3) {
118859
119097
  const id = sanitize(deviceId);
118860
119098
  const dir = trustDir(root3);
118861
119099
  const p2 = join6(dir, id);
118862
- mkdirSync3(dir, { recursive: true, mode: DIR_MODE3 });
119100
+ mkdirSync3(dir, { recursive: true, mode: DIR_MODE2 });
118863
119101
  writeFileSync(
118864
119102
  p2,
118865
119103
  `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
118866
119104
  `,
118867
- { mode: FILE_MODE3 }
119105
+ { mode: FILE_MODE2 }
118868
119106
  );
118869
- chmodSync2(dir, DIR_MODE3);
118870
- chmodSync2(p2, FILE_MODE3);
119107
+ chmodSync2(dir, DIR_MODE2);
119108
+ chmodSync2(p2, FILE_MODE2);
118871
119109
  }
118872
119110
  function revoke(deviceId, root3) {
118873
119111
  const id = sanitize(deviceId);
@@ -133641,9 +133879,14 @@ function wireBridge(channel, session, target, opts = {}) {
133641
133879
  });
133642
133880
  channel.on("message", (text, metadata) => {
133643
133881
  if (metadata?.roomId) return;
133644
- log("inbound 1:1 DM from owner");
133882
+ const ownerAttested = metadata?.ownerDm1to1 === true;
133883
+ if (!ownerAttested) {
133884
+ log("inbound 1:1 DM WITHOUT owner attestation \u2014 delivering to the locked listener (no tools)");
133885
+ } else {
133886
+ log("inbound 1:1 DM from owner");
133887
+ }
133645
133888
  target.setDm();
133646
- session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: true });
133889
+ session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: ownerAttested });
133647
133890
  });
133648
133891
  const workerCapable = !!opts.workspaceDir;
133649
133892
  const osIsolated = opts.osIsolated !== void 0 ? opts.osIsolated : process.env.AV_WORKER_OS_ISOLATED === "1" || process.env.AV_WORKER_OS_ISOLATED === "true";
@@ -133784,7 +134027,7 @@ async function main() {
133784
134027
  "[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"
133785
134028
  );
133786
134029
  }
133787
- console.error(`[bridge] version: ${true ? "0.7.4" : "dev"}`);
134030
+ console.error(`[bridge] version: ${true ? "0.7.6" : "dev"}`);
133788
134031
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133789
134032
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133790
134033
  if (cfg.armRoom) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "type": "module",
5
- "description": "AgentVault Claude Bridge \u2014 daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
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",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {