@agentvault/claude-bridge 0.7.5 → 0.7.7

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 { readdir, readFile, rename, 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 as rename2, 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 as readdir2, readFile as readFile3, writeFile as writeFile2, rename as rename3, 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,8 +63990,8 @@ 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 });
63973
- await rename(tmp, bak);
63993
+ await writeSecureFile(tmp, data);
63994
+ await rename2(tmp, bak);
63974
63995
  } catch {
63975
63996
  }
63976
63997
  }
@@ -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) {
@@ -64108,19 +64126,30 @@ var init_transport2 = __esm2({
64108
64126
  });
64109
64127
  var openclaw_compat_exports = {};
64110
64128
  __export2(openclaw_compat_exports, {
64129
+ AGENT_EVENT_CANDIDATES: () => AGENT_EVENT_CANDIDATES,
64130
+ HEARTBEAT_CANDIDATES: () => HEARTBEAT_CANDIDATES,
64131
+ TRANSCRIPT_CANDIDATES: () => TRANSCRIPT_CANDIDATES,
64132
+ _resetCompatCacheForTest: () => _resetCompatCacheForTest,
64111
64133
  onAgentEvent: () => onAgentEvent,
64112
64134
  onSessionTranscriptUpdate: () => onSessionTranscriptUpdate,
64113
- requestHeartbeatNow: () => requestHeartbeatNow
64135
+ requestHeartbeatNow: () => requestHeartbeatNow,
64136
+ resolveSdkFn: () => resolveSdkFn
64114
64137
  });
64115
- async function requestHeartbeatNow(opts) {
64116
- if (_heartbeatFn === null) {
64138
+ async function resolveSdkFn(candidates, importer = defaultImporter) {
64139
+ for (const { specifier, symbol: symbol22 } of candidates) {
64117
64140
  try {
64118
- const mod4 = await import("openclaw/dist/plugin-sdk/infra/heartbeat-wake.js");
64119
- _heartbeatFn = mod4.requestHeartbeatNow ?? mod4.default?.requestHeartbeatNow ?? false;
64141
+ const mod4 = await importer(specifier);
64142
+ const found = mod4?.[symbol22] ?? mod4?.default?.[symbol22];
64143
+ if (typeof found === "function") return found;
64120
64144
  } catch {
64121
- _heartbeatFn = false;
64122
64145
  }
64123
64146
  }
64147
+ return false;
64148
+ }
64149
+ async function requestHeartbeatNow(opts) {
64150
+ if (_heartbeatFn === null) {
64151
+ _heartbeatFn = await resolveSdkFn(HEARTBEAT_CANDIDATES);
64152
+ }
64124
64153
  if (typeof _heartbeatFn === "function") {
64125
64154
  try {
64126
64155
  await _heartbeatFn(opts);
@@ -64133,12 +64162,7 @@ async function requestHeartbeatNow(opts) {
64133
64162
  }
64134
64163
  async function onAgentEvent(callback) {
64135
64164
  if (_agentEventFn === null) {
64136
- try {
64137
- const mod4 = await import("openclaw/dist/plugin-sdk/infra/agent-events.js");
64138
- _agentEventFn = mod4.onAgentEvent ?? mod4.default?.onAgentEvent ?? false;
64139
- } catch {
64140
- _agentEventFn = false;
64141
- }
64165
+ _agentEventFn = await resolveSdkFn(AGENT_EVENT_CANDIDATES);
64142
64166
  }
64143
64167
  if (typeof _agentEventFn === "function") {
64144
64168
  try {
@@ -64153,12 +64177,7 @@ async function onAgentEvent(callback) {
64153
64177
  }
64154
64178
  async function onSessionTranscriptUpdate(callback) {
64155
64179
  if (_transcriptFn === null) {
64156
- try {
64157
- const mod4 = await import("openclaw/dist/plugin-sdk/sessions/transcript-events.js");
64158
- _transcriptFn = mod4.onSessionTranscriptUpdate ?? mod4.default?.onSessionTranscriptUpdate ?? false;
64159
- } catch {
64160
- _transcriptFn = false;
64161
- }
64180
+ _transcriptFn = await resolveSdkFn(TRANSCRIPT_CANDIDATES);
64162
64181
  }
64163
64182
  if (typeof _transcriptFn === "function") {
64164
64183
  try {
@@ -64171,12 +64190,45 @@ async function onSessionTranscriptUpdate(callback) {
64171
64190
  return () => {
64172
64191
  };
64173
64192
  }
64193
+ function _resetCompatCacheForTest() {
64194
+ _heartbeatFn = null;
64195
+ _agentEventFn = null;
64196
+ _transcriptFn = null;
64197
+ }
64198
+ var defaultImporter;
64199
+ var HEARTBEAT_CANDIDATES;
64200
+ var AGENT_EVENT_CANDIDATES;
64201
+ var TRANSCRIPT_CANDIDATES;
64174
64202
  var _heartbeatFn;
64175
64203
  var _agentEventFn;
64176
64204
  var _transcriptFn;
64177
64205
  var init_openclaw_compat = __esm2({
64178
64206
  "src/openclaw-compat.ts"() {
64179
64207
  "use strict";
64208
+ defaultImporter = (specifier) => import(
64209
+ /* @vite-ignore */
64210
+ specifier
64211
+ );
64212
+ HEARTBEAT_CANDIDATES = [
64213
+ // 2026.7.x — exported subpath, and the symbol lost its `Now` suffix.
64214
+ { specifier: "openclaw/plugin-sdk/heartbeat-runtime", symbol: "requestHeartbeat" },
64215
+ // <=2026.6.x — deep dist path, original name.
64216
+ { specifier: "openclaw/dist/plugin-sdk/infra/heartbeat-wake.js", symbol: "requestHeartbeatNow" }
64217
+ ];
64218
+ AGENT_EVENT_CANDIDATES = [
64219
+ { specifier: "openclaw/plugin-sdk/agent-harness", symbol: "onAgentEvent" },
64220
+ { specifier: "openclaw/dist/plugin-sdk/infra/agent-events.js", symbol: "onAgentEvent" }
64221
+ ];
64222
+ TRANSCRIPT_CANDIDATES = [
64223
+ {
64224
+ specifier: "openclaw/plugin-sdk/memory-core-host-engine-foundation",
64225
+ symbol: "onSessionTranscriptUpdate"
64226
+ },
64227
+ {
64228
+ specifier: "openclaw/dist/plugin-sdk/sessions/transcript-events.js",
64229
+ symbol: "onSessionTranscriptUpdate"
64230
+ }
64231
+ ];
64180
64232
  _heartbeatFn = null;
64181
64233
  _agentEventFn = null;
64182
64234
  _transcriptFn = null;
@@ -64486,12 +64538,12 @@ async function handleWorkspaceUpload(data, workspaceDir) {
64486
64538
  if (!verified) {
64487
64539
  return { status: "error", error: "Invalid signature \u2014 file may have been tampered with" };
64488
64540
  }
64489
- await mkdir3(workspaceDir, { recursive: true });
64541
+ await mkdir2(workspaceDir, { recursive: true });
64490
64542
  const targetPath = join3(workspaceDir, data.filename);
64491
64543
  const tempPath = join3(workspaceDir, `.tmp_${randomUUID()}_${data.filename}`);
64492
64544
  try {
64493
- await writeFile3(tempPath, data.content, "utf-8");
64494
- await rename2(tempPath, targetPath);
64545
+ await writeFile2(tempPath, data.content, "utf-8");
64546
+ await rename3(tempPath, targetPath);
64495
64547
  } catch (err) {
64496
64548
  try {
64497
64549
  await unlink(tempPath);
@@ -64508,7 +64560,7 @@ async function handleWorkspaceUpload(data, workspaceDir) {
64508
64560
  }
64509
64561
  async function handleWorkspaceList(workspaceDir) {
64510
64562
  try {
64511
- const entries = await readdir(workspaceDir);
64563
+ const entries = await readdir2(workspaceDir);
64512
64564
  const files = [];
64513
64565
  for (const entry of entries) {
64514
64566
  if (!entry.endsWith(".md")) continue;
@@ -65669,7 +65721,7 @@ var init_channel = __esm2({
65669
65721
  */
65670
65722
  sendActivitySpan(spanData) {
65671
65723
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65672
- const pluginVersion = true ? "0.23.17" : "0.0.0-dev";
65724
+ const pluginVersion = true ? "0.23.19" : "0.0.0-dev";
65673
65725
  const agentName = this.config.agentName ?? "Agent";
65674
65726
  const resource = {
65675
65727
  "service.name": "agentvault-agent",
@@ -67534,7 +67586,7 @@ var init_channel = __esm2({
67534
67586
  agentVersion: this.config.agentVersion ?? "0.0.0",
67535
67587
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67536
67588
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67537
- pluginVersion: true ? "0.23.17" : "0.0.0-dev"
67589
+ pluginVersion: true ? "0.23.19" : "0.0.0-dev"
67538
67590
  });
67539
67591
  this._telemetryReporter.startAutoFlush(3e4);
67540
67592
  }
@@ -67857,7 +67909,7 @@ var init_channel = __esm2({
67857
67909
  agentVersion: this.config.agentVersion ?? "0.0.0",
67858
67910
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67859
67911
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67860
- pluginVersion: true ? "0.23.17" : "0.0.0-dev"
67912
+ pluginVersion: true ? "0.23.19" : "0.0.0-dev"
67861
67913
  });
67862
67914
  this._telemetryReporter.startAutoFlush(3e4);
67863
67915
  }
@@ -68341,6 +68393,10 @@ var init_channel = __esm2({
68341
68393
  conversationId: convId ?? this._primaryConversationId,
68342
68394
  timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
68343
68395
  topicId,
68396
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
68397
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
68398
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
68399
+ ownerDm1to1: true,
68344
68400
  messageType
68345
68401
  };
68346
68402
  this.emit("message", text, metadata);
@@ -68583,7 +68639,7 @@ ${messageText}`;
68583
68639
  */
68584
68640
  async _downloadAndDecryptAttachment(info) {
68585
68641
  const attachDir = join4(this.config.dataDir, "attachments");
68586
- await mkdir4(attachDir, { recursive: true });
68642
+ await mkdir3(attachDir, { recursive: true });
68587
68643
  const url22 = `${this.config.apiUrl}${info.blobUrl}`;
68588
68644
  const res = await fetch(url22, {
68589
68645
  headers: { Authorization: `Bearer ${this._deviceJwt}` }
@@ -68601,7 +68657,7 @@ ${messageText}`;
68601
68657
  const fileNonce = base64ToBytes(info.fileNonce);
68602
68658
  const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
68603
68659
  const filePath = join4(attachDir, info.filename);
68604
- await writeFile4(filePath, decrypted);
68660
+ await writeFile3(filePath, decrypted);
68605
68661
  console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
68606
68662
  return { filePath, decrypted };
68607
68663
  }
@@ -69621,7 +69677,7 @@ ${messageText}`;
69621
69677
  console.warn("[SecureChannel] KeyPackage pool replenish after join failed:", err);
69622
69678
  });
69623
69679
  console.log(`[SecureChannel] Welcome joined: group=${groupId?.slice(0, 8)} kpSource=${kpSource} welcomeLen=${welcomeBytes.length} candidates=${candidateCount} poolRemaining=${this._pendingKpBundles.length}`);
69624
- if (conversationGroupId) {
69680
+ if (conversationGroupId && !data.room_id) {
69625
69681
  const key = `1to1-group:${conversationGroupId}`;
69626
69682
  this._mlsGroups.set(key, mgr);
69627
69683
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
@@ -70442,7 +70498,11 @@ ${messageText}`;
70442
70498
  messageId: msg.message_id,
70443
70499
  conversationId: msg.conversation_id,
70444
70500
  timestamp: msg.created_at,
70445
- topicId
70501
+ topicId,
70502
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
70503
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
70504
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
70505
+ ownerDm1to1: true
70446
70506
  };
70447
70507
  this.emit("message", messageText, metadata);
70448
70508
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -70774,7 +70834,11 @@ ${messageText}`;
70774
70834
  messageId: msg.id,
70775
70835
  conversationId: msg.conversation_id,
70776
70836
  timestamp: msg.created_at,
70777
- topicId
70837
+ topicId,
70838
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
70839
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
70840
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
70841
+ ownerDm1to1: true
70778
70842
  };
70779
70843
  this.emit("message", messageText, metadata);
70780
70844
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -71055,7 +71119,11 @@ ${messageText}`;
71055
71119
  messageId: msg.id,
71056
71120
  conversationId: msg.conversation_id,
71057
71121
  timestamp: msg.created_at,
71058
- topicId
71122
+ topicId,
71123
+ // #621: positive attestation that this decrypted under the OWNER'S 1:1
71124
+ // material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
71125
+ // that a roomId was absent. The bridge's full-access DM lane requires it.
71126
+ ownerDm1to1: true
71059
71127
  };
71060
71128
  this.emit("message", messageText, metadata);
71061
71129
  Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
@@ -71658,6 +71726,16 @@ var init_fetch_interceptor = __esm2({
71658
71726
  traceStore = new AsyncLocalStorage();
71659
71727
  }
71660
71728
  });
71729
+ var init_tool_audit = __esm2({
71730
+ "src/tool-audit.ts"() {
71731
+ "use strict";
71732
+ }
71733
+ });
71734
+ var init_harness_compat = __esm2({
71735
+ "src/harness-compat.ts"() {
71736
+ "use strict";
71737
+ }
71738
+ });
71661
71739
  var isUsingManagedRoutes;
71662
71740
  var init_openclaw_entry = __esm2({
71663
71741
  "src/openclaw-entry.ts"() {
@@ -71667,6 +71745,8 @@ var init_openclaw_entry = __esm2({
71667
71745
  init_http_handlers();
71668
71746
  init_openclaw_compat();
71669
71747
  init_lifecycle();
71748
+ init_tool_audit();
71749
+ init_harness_compat();
71670
71750
  init_types();
71671
71751
  isUsingManagedRoutes = false;
71672
71752
  }
@@ -97718,7 +97798,7 @@ var init_index = __esm2({
97718
97798
  init_skill_invoker();
97719
97799
  await init_skill_telemetry();
97720
97800
  await init_policy_enforcer();
97721
- VERSION = true ? "0.23.17" : "0.0.0-dev";
97801
+ VERSION = true ? "0.23.19" : "0.0.0-dev";
97722
97802
  }
97723
97803
  });
97724
97804
  await init_index();
@@ -119029,8 +119109,8 @@ import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync
119029
119109
  import { join as join6 } from "node:path";
119030
119110
  import { homedir, hostname as hostname3 } from "node:os";
119031
119111
  var TRUST_SUBDIR = "host-trust";
119032
- var DIR_MODE3 = 448;
119033
- var FILE_MODE3 = 384;
119112
+ var DIR_MODE2 = 448;
119113
+ var FILE_MODE2 = 384;
119034
119114
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
119035
119115
  var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
119036
119116
  var HostTrustError = class extends Error {
@@ -119057,15 +119137,15 @@ function grant(deviceId, root3) {
119057
119137
  const id = sanitize(deviceId);
119058
119138
  const dir = trustDir(root3);
119059
119139
  const p2 = join6(dir, id);
119060
- mkdirSync3(dir, { recursive: true, mode: DIR_MODE3 });
119140
+ mkdirSync3(dir, { recursive: true, mode: DIR_MODE2 });
119061
119141
  writeFileSync(
119062
119142
  p2,
119063
119143
  `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
119064
119144
  `,
119065
- { mode: FILE_MODE3 }
119145
+ { mode: FILE_MODE2 }
119066
119146
  );
119067
- chmodSync2(dir, DIR_MODE3);
119068
- chmodSync2(p2, FILE_MODE3);
119147
+ chmodSync2(dir, DIR_MODE2);
119148
+ chmodSync2(p2, FILE_MODE2);
119069
119149
  }
119070
119150
  function revoke(deviceId, root3) {
119071
119151
  const id = sanitize(deviceId);
@@ -133358,6 +133438,16 @@ var WorkerQueue = class {
133358
133438
  this.running = false;
133359
133439
  }
133360
133440
  }
133441
+ /**
133442
+ * What the worker had produced, when the session can report it. Read on BOTH the
133443
+ * clean and the failing path: on a clean turn `said: false` with a non-zero
133444
+ * `composedChars` is the "composed but could never ship it" signature (agent-
133445
+ * authored room traffic, where `replyExpected` is false), which is otherwise
133446
+ * indistinguishable from a turn that did nothing.
133447
+ */
133448
+ snapshotOf(session) {
133449
+ return session?.snapshot?.();
133450
+ }
133361
133451
  /** Best-effort trap emit. A broken sink must never break the queue. */
133362
133452
  emitIncident(rec) {
133363
133453
  try {
@@ -133389,10 +133479,23 @@ var WorkerQueue = class {
133389
133479
  }, this.deps.timeoutMs);
133390
133480
  });
133391
133481
  await Promise.race([session.start(), timeout]);
133482
+ const okSnap = this.snapshotOf(session);
133483
+ this.emitIncident({
133484
+ at: new Date(startedAt).toISOString(),
133485
+ outcome: "ok",
133486
+ waitedMs,
133487
+ ranMs: Date.now() - startedAt,
133488
+ timeoutMs: this.deps.timeoutMs,
133489
+ queueDepthAtEnqueue: entry.queueDepthAtEnqueue,
133490
+ queueDepthAtStart,
133491
+ instructionChars: task.instruction.length,
133492
+ replyExpected: task.replyExpected,
133493
+ ...okSnap ? { session: okSnap } : {}
133494
+ });
133392
133495
  } catch (e7) {
133393
133496
  const err = e7;
133394
133497
  this.deps.log(`[worker-queue] task failed: ${err.message}`);
133395
- const snap = session?.snapshot?.();
133498
+ const snap = this.snapshotOf(session);
133396
133499
  this.emitIncident({
133397
133500
  at: new Date(startedAt).toISOString(),
133398
133501
  outcome: timedOut ? "timeout" : "error",
@@ -133403,6 +133506,7 @@ var WorkerQueue = class {
133403
133506
  queueDepthAtEnqueue: entry.queueDepthAtEnqueue,
133404
133507
  queueDepthAtStart,
133405
133508
  instructionChars: task.instruction.length,
133509
+ replyExpected: task.replyExpected,
133406
133510
  ...snap ? { session: snap } : {}
133407
133511
  });
133408
133512
  session?.abort();
@@ -133424,7 +133528,7 @@ function makeRouter(deps) {
133424
133528
  const replySink = reply ?? (() => {
133425
133529
  });
133426
133530
  const isOwnerDm = opts?.autoReplyOnText === true && opts?.armed === void 0 && deps.workAllowed();
133427
- const isArmedRoom = opts?.armed?.() === true && deps.workAllowed();
133531
+ const isArmedRoom = opts?.armed?.() === true && deps.workAllowed() && opts?.replyExpected !== false;
133428
133532
  if (isOwnerDm) {
133429
133533
  deps.queue.enqueue({
133430
133534
  instruction: text,
@@ -133839,9 +133943,14 @@ function wireBridge(channel, session, target, opts = {}) {
133839
133943
  });
133840
133944
  channel.on("message", (text, metadata) => {
133841
133945
  if (metadata?.roomId) return;
133842
- log("inbound 1:1 DM from owner");
133946
+ const ownerAttested = metadata?.ownerDm1to1 === true;
133947
+ if (!ownerAttested) {
133948
+ log("inbound 1:1 DM WITHOUT owner attestation \u2014 delivering to the locked listener (no tools)");
133949
+ } else {
133950
+ log("inbound 1:1 DM from owner");
133951
+ }
133843
133952
  target.setDm();
133844
- session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: true });
133953
+ session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: ownerAttested });
133845
133954
  });
133846
133955
  const workerCapable = !!opts.workspaceDir;
133847
133956
  const osIsolated = opts.osIsolated !== void 0 ? opts.osIsolated : process.env.AV_WORKER_OS_ISOLATED === "1" || process.env.AV_WORKER_OS_ISOLATED === "true";
@@ -133982,7 +134091,7 @@ async function main() {
133982
134091
  "[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"
133983
134092
  );
133984
134093
  }
133985
- console.error(`[bridge] version: ${true ? "0.7.5" : "dev"}`);
134094
+ console.error(`[bridge] version: ${true ? "0.7.7" : "dev"}`);
133986
134095
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133987
134096
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133988
134097
  if (cfg.armRoom) {
@@ -134065,7 +134174,7 @@ async function main() {
134065
134174
  // incident is visible in bridge.error.log at the moment it happens.
134066
134175
  onIncident: (rec) => {
134067
134176
  console.error(
134068
- `[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued)` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
134177
+ `[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued) replyExpected=${rec.replyExpected}` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
134069
134178
  );
134070
134179
  try {
134071
134180
  const dir = join11(cfg.dataDir, "logs");
@@ -17,19 +17,27 @@ export type WorkerTask = {
17
17
  armed?: () => boolean;
18
18
  };
19
19
  /**
20
- * Forensic record of a task that did NOT complete cleanly.
20
+ * Forensic record of EVERY task the queue runs clean or not.
21
21
  *
22
22
  * Exists because the 2026-07-24 "loopita went quiet for 5 minutes" incident was
23
23
  * diagnosable only by coincidence: the drop-trap happened to capture that the
24
24
  * reply landed 300.016s after the ack, and 300s is `WORKER_TIMEOUT_MS` exactly.
25
25
  * Nothing in the bridge recorded WHY the worker hung, and the logs carry no
26
26
  * timestamps at all, so the evidence was gone by the time we looked.
27
+ *
28
+ * 2026-08-06: widened from failures-only to every turn. Recording only failures
29
+ * made the SLOW case unmeasurable, and slow is the common case: loopita answered
30
+ * Ballroom2 in 21s when the room was quiet and 81s when a second agent was
31
+ * talking, and nothing said whether the extra 60s was `waitedMs` or `ranMs`.
32
+ * Those are different bugs with identical symptoms. A trap that only reports
33
+ * when it breaks cannot tell you it is the reason things are slow.
27
34
  */
28
35
  export type WorkerIncident = {
29
36
  /** ISO timestamp — the bridge's own logs are unstamped, so this is the anchor. */
30
37
  at: string;
31
- outcome: "timeout" | "error";
32
- error: string;
38
+ outcome: "ok" | "timeout" | "error";
39
+ /** Absent on a clean turn — its presence is what marks a real incident. */
40
+ error?: string;
33
41
  /**
34
42
  * enqueue -> start. THE load-bearing field: it separates "this task hung" from
35
43
  * "this task was stuck behind one that hung". Both look identical to the owner
@@ -44,6 +52,15 @@ export type WorkerIncident = {
44
52
  /** Depth still waiting when this task began — the blast radius of this hang. */
45
53
  queueDepthAtStart: number;
46
54
  instructionChars: number;
55
+ /**
56
+ * Whether a reply was expected of this turn — mirrors `WorkerTask.replyExpected`,
57
+ * i.e. `senderIsAgent === false` upstream. Without it a silent turn
58
+ * (composed=0, said=false) is unattributable: it could be agent-authored traffic
59
+ * that was never going to answer, or a turn that waited so long the previous one
60
+ * already answered. Ballroom2 on 2026-08-06 produced both readings and the record
61
+ * could not separate them.
62
+ */
63
+ replyExpected?: boolean;
47
64
  /** What the worker had produced when it died, when the session can report it. */
48
65
  session?: {
49
66
  composedChars: number;
@@ -91,6 +108,14 @@ export declare class WorkerQueue {
91
108
  /** Resolves when the queue has processed everything enqueued so far. */
92
109
  whenDrained(): Promise<void>;
93
110
  private drain;
111
+ /**
112
+ * What the worker had produced, when the session can report it. Read on BOTH the
113
+ * clean and the failing path: on a clean turn `said: false` with a non-zero
114
+ * `composedChars` is the "composed but could never ship it" signature (agent-
115
+ * authored room traffic, where `replyExpected` is false), which is otherwise
116
+ * indistinguishable from a turn that did nothing.
117
+ */
118
+ private snapshotOf;
94
119
  /** Best-effort trap emit. A broken sink must never break the queue. */
95
120
  private emitIncident;
96
121
  private runOne;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
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": {