@agentvault/grok-bridge 0.4.3 → 0.4.4

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 +190 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -64589,11 +64589,11 @@ var init_mls_kp_pool = __esm2({
64589
64589
  }
64590
64590
  });
64591
64591
  function ownIdentity() {
64592
- const v2 = true ? "0.23.30" : FALLBACK;
64592
+ const v2 = true ? "0.23.31" : FALLBACK;
64593
64593
  return `${PACKAGE}@${v2}`;
64594
64594
  }
64595
64595
  function buildSha() {
64596
- const raw = true ? "1fff5d6" : "";
64596
+ const raw = true ? "3e1243a-dirty" : "";
64597
64597
  const cleaned = (raw ?? "").trim();
64598
64598
  return !cleaned || cleaned === "unknown" ? null : cleaned;
64599
64599
  }
@@ -66341,6 +66341,118 @@ var init_channel = __esm2({
66341
66341
  * Encrypt and send a message to ALL owner devices (fanout).
66342
66342
  * Each session gets the same plaintext encrypted independently.
66343
66343
  */
66344
+ /**
66345
+ * #952 — sends held because MLS is expected for this conversation but the
66346
+ * group is not joined yet.
66347
+ *
66348
+ * ⚠️ IN MEMORY ONLY. These entries hold PLAINTEXT. `_persisted` is written to
66349
+ * disk, and the product's entire premise is that plaintext does not rest
66350
+ * where it does not have to; a crash losing a held greeting is the correct
66351
+ * trade. This is why the existing `outboundQueue` could not be reused — that
66352
+ * one is persisted, and holds already-DR-encrypted bytes, which is exactly
66353
+ * the encryption we are trying not to perform yet.
66354
+ */
66355
+ _mlsHold = /* @__PURE__ */ new Map();
66356
+ /** How long a send waits for the Welcome. The two measured bursts were each
66357
+ * confined to a single minute, with the group already existing server-side
66358
+ * in the same second, so this is generous rather than tight. */
66359
+ static MLS_HOLD_MS = 3e4;
66360
+ /** Bound on held sends per group — see `_holdForMls`. */
66361
+ static MLS_HOLD_MAX = 50;
66362
+ /**
66363
+ * Should this 1:1 send wait for MLS rather than go out over Double Ratchet?
66364
+ *
66365
+ * The discriminator exists at the moment of the bug: `_sessionGroupIds`
66366
+ * already carries the conversation's group id — the server told us the group
66367
+ * exists — while `_mlsGroups` has no usable manager for it. Group expected,
66368
+ * group not ready.
66369
+ *
66370
+ * The negative case is load-bearing. A conversation with NO group id is
66371
+ * legitimately Double Ratchet, and holding it would break owner-to-agent
66372
+ * messaging outright, which is far worse than the bug this fixes. "Present"
66373
+ * is also not "ready": a manager parked at epoch 0 has not joined (#1092),
66374
+ * and reading presence as readiness is how #991 shipped.
66375
+ */
66376
+ _shouldHoldForMls(convGroupId, mlsGroup, mlsGroupId) {
66377
+ if (!convGroupId) return false;
66378
+ const ready = Boolean(mlsGroup?.isInitialized) && Boolean(mlsGroupId) && Number(mlsGroup?.epoch) > 0;
66379
+ return !ready;
66380
+ }
66381
+ /** Park a send until the Welcome lands, or until the hold expires. */
66382
+ _holdForMls(convGroupId, convId, plaintext, options) {
66383
+ let entry = this._mlsHold.get(convGroupId);
66384
+ if (!entry) {
66385
+ const timer = setTimeout(() => {
66386
+ void this._releaseHeldOverDr(convGroupId);
66387
+ }, _SecureChannel.MLS_HOLD_MS);
66388
+ timer.unref?.();
66389
+ entry = { items: [], timer };
66390
+ this._mlsHold.set(convGroupId, entry);
66391
+ }
66392
+ if (entry.items.length >= _SecureChannel.MLS_HOLD_MAX) {
66393
+ entry.items.shift();
66394
+ console.warn(
66395
+ `[SecureChannel] MLS hold full for group ${convGroupId.slice(0, 8)} \u2014 dropped the oldest held message`
66396
+ );
66397
+ }
66398
+ entry.items.push({ convId, plaintext, options });
66399
+ console.log(
66400
+ `[SecureChannel] Holding send for group ${convGroupId.slice(0, 8)} until its Welcome lands (held=${entry.items.length})`
66401
+ );
66402
+ }
66403
+ _takeHold(convGroupId) {
66404
+ const entry = this._mlsHold.get(convGroupId);
66405
+ if (!entry) return null;
66406
+ clearTimeout(entry.timer);
66407
+ this._mlsHold.delete(convGroupId);
66408
+ return entry;
66409
+ }
66410
+ /** Re-drive one held send through the normal path, now that MLS is ready. */
66411
+ async _resendHeld(item) {
66412
+ await this.send(item.plaintext, {
66413
+ ...item.options ?? {},
66414
+ conversationId: item.convId,
66415
+ bypassMlsHold: true
66416
+ });
66417
+ }
66418
+ /** The Welcome landed — release everything held for this group, in order. */
66419
+ async _flushMlsHold(convGroupId) {
66420
+ const entry = this._takeHold(convGroupId);
66421
+ if (!entry) return;
66422
+ console.log(
66423
+ `[SecureChannel] Welcome landed for group ${convGroupId.slice(0, 8)} \u2014 releasing ${entry.items.length} held message(s) over MLS`
66424
+ );
66425
+ for (const item of entry.items) {
66426
+ try {
66427
+ await this._resendHeld(item);
66428
+ } catch (err) {
66429
+ console.warn("[SecureChannel] Held message failed to send after Welcome:", err);
66430
+ }
66431
+ }
66432
+ }
66433
+ /**
66434
+ * The hold expired. Send over Double Ratchet exactly as before.
66435
+ *
66436
+ * This is deliberately NOT a drop. #853 and #831 are Welcomes that never
66437
+ * arrive; under those an unbounded hold turns a lost minute into an agent
66438
+ * that never speaks again. The hold removes the race — it must not invent a
66439
+ * new failure mode. The warning is the part that is new: today the loss is
66440
+ * silent.
66441
+ */
66442
+ async _releaseHeldOverDr(convGroupId) {
66443
+ const entry = this._takeHold(convGroupId);
66444
+ if (!entry) return;
66445
+ console.warn(
66446
+ `[SecureChannel] No Welcome for group ${convGroupId.slice(0, 8)} within ${_SecureChannel.MLS_HOLD_MS}ms \u2014 releasing ${entry.items.length} message(s) over Double Ratchet. The recipient may not hold a ratchet with this device, in which case they are undeliverable (#952).`
66447
+ );
66448
+ for (const item of entry.items) {
66449
+ try {
66450
+ await this._resendHeld(item);
66451
+ } catch (err) {
66452
+ console.warn("[SecureChannel] Held message failed on Double Ratchet release:", err);
66453
+ }
66454
+ }
66455
+ }
66344
66456
  async send(plaintext, options) {
66345
66457
  if (isAbstention(plaintext)) return;
66346
66458
  if (this._state === "error" || this._state === "idle") {
@@ -66502,6 +66614,10 @@ var init_channel = __esm2({
66502
66614
  );
66503
66615
  addressedMlsGroupIds.push(mlsGroupId);
66504
66616
  if (convGroupId) sentSharedGroupIds.add(convGroupId);
66617
+ } else if (!options?.bypassMlsHold && this._shouldHoldForMls(convGroupId, mlsGroup, mlsGroupId)) {
66618
+ this._holdForMls(convGroupId, convId, plaintext, options);
66619
+ sentCount++;
66620
+ continue;
66505
66621
  } else {
66506
66622
  const encrypted = session.ratchet.encrypt(plaintext);
66507
66623
  const transport = encryptedMessageToTransport(encrypted);
@@ -66671,7 +66787,7 @@ var init_channel = __esm2({
66671
66787
  */
66672
66788
  sendActivitySpan(spanData) {
66673
66789
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
66674
- const pluginVersion = true ? "0.23.30" : "0.0.0-dev";
66790
+ const pluginVersion = true ? "0.23.31" : "0.0.0-dev";
66675
66791
  const agentName = this.config.agentName ?? "Agent";
66676
66792
  const resource = {
66677
66793
  "service.name": "agentvault-agent",
@@ -68609,7 +68725,7 @@ var init_channel = __esm2({
68609
68725
  agentVersion: this.config.agentVersion ?? "0.0.0",
68610
68726
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68611
68727
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68612
- pluginVersion: true ? "0.23.30" : "0.0.0-dev"
68728
+ pluginVersion: true ? "0.23.31" : "0.0.0-dev"
68613
68729
  });
68614
68730
  this._telemetryReporter.startAutoFlush(3e4);
68615
68731
  }
@@ -68933,7 +69049,7 @@ var init_channel = __esm2({
68933
69049
  agentVersion: this.config.agentVersion ?? "0.0.0",
68934
69050
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68935
69051
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68936
- pluginVersion: true ? "0.23.30" : "0.0.0-dev"
69052
+ pluginVersion: true ? "0.23.31" : "0.0.0-dev"
68937
69053
  });
68938
69054
  this._telemetryReporter.startAutoFlush(3e4);
68939
69055
  }
@@ -69349,11 +69465,13 @@ var init_channel = __esm2({
69349
69465
  let text = plaintext;
69350
69466
  let messageType = "text";
69351
69467
  let topicId = data.topic_id;
69468
+ let mlsAttachment = null;
69352
69469
  try {
69353
69470
  const parsed = JSON.parse(plaintext);
69354
69471
  text = parsed.text ?? plaintext;
69355
69472
  messageType = parsed.type ?? "text";
69356
69473
  topicId = parsed.topicId ?? topicId;
69474
+ if (parsed.attachment) mlsAttachment = parsed.attachment;
69357
69475
  } catch {
69358
69476
  }
69359
69477
  if (messageType === "history_catchup_request") {
@@ -69420,6 +69538,9 @@ var init_channel = __esm2({
69420
69538
  console.log(`[SecureChannel] Session ${convId.slice(0, 8)} activated via MLS message`);
69421
69539
  }
69422
69540
  }
69541
+ if (mlsAttachment) {
69542
+ text = await this._augmentWithAttachment(mlsAttachment, text);
69543
+ }
69423
69544
  const metadata = {
69424
69545
  messageId: data.message_id ?? `mls_${Date.now()}`,
69425
69546
  conversationId: convId ?? this._primaryConversationId,
@@ -69669,6 +69790,65 @@ ${messageText}`;
69669
69790
  * Download an encrypted attachment blob, decrypt it, verify integrity,
69670
69791
  * and save the plaintext file to disk.
69671
69792
  */
69793
+ /** Where an attachment may be written — always directly inside `attachDir` (#1143).
69794
+ *
69795
+ * The filename arrives INSIDE the envelope and is chosen by whoever sent the
69796
+ * message. `join(attachDir, info.filename)` resolves `..` normally, so
69797
+ * `../../agentvault.json` wrote over the device credential store. Only the
69798
+ * final path component is honoured, and anything that is not a usable name
69799
+ * falls back to a fixed one.
69800
+ */
69801
+ _safeAttachmentPath(attachDir, filename) {
69802
+ const raw = String(filename ?? "");
69803
+ const last = raw.split(/[\\/]/).pop() ?? "";
69804
+ const cleaned = last.replace(/\0/g, "").trim();
69805
+ const safe = cleaned === "" || cleaned === "." || cleaned === ".." ? "attachment.bin" : cleaned;
69806
+ return join4(attachDir, safe);
69807
+ }
69808
+ /** Download an attachment and fold it into the text the agent sees (#1139).
69809
+ *
69810
+ * Extracted so the MLS receive path can use it too. It used to live inline in
69811
+ * `_handleIncomingMessage`, which meant attachments worked on the Double
69812
+ * Ratchet path ONLY — and hermes drops DR by design (`secure_channel.py:594`,
69813
+ * "DR fallback is logged-and-dropped"), so no single transport could serve
69814
+ * both client families. That is why an attachment reached ohtani and never
69815
+ * reached smartcoder2 on 2026-08-31.
69816
+ *
69817
+ * Returns the text unchanged when there is no attachment or the fetch fails —
69818
+ * but a failure is LOGGED and announced in the text, never silent.
69819
+ */
69820
+ async _augmentWithAttachment(attachmentInfo, baseText) {
69821
+ if (!attachmentInfo) return baseText;
69822
+ try {
69823
+ const { filePath, decrypted } = await this._downloadAndDecryptAttachment(attachmentInfo);
69824
+ const mime = String(attachmentInfo.mime ?? "application/octet-stream");
69825
+ const name = String(attachmentInfo.filename ?? "attachment");
69826
+ const textMimes = ["text/", "application/json", "application/xml", "application/csv"];
69827
+ if (textMimes.some((m2) => mime.startsWith(m2))) {
69828
+ const content = new TextDecoder().decode(decrypted);
69829
+ return `[Attachment: ${name} (${mime}) saved to ${filePath}]
69830
+ ---
69831
+ ${content}
69832
+ ---
69833
+
69834
+ ${baseText}`;
69835
+ }
69836
+ if (mime.startsWith("image/")) {
69837
+ return `[Image attachment: ${name} saved to ${filePath}]
69838
+ Use your Read tool to view this image file.
69839
+
69840
+ ${baseText}`;
69841
+ }
69842
+ return `[Attachment: ${name} saved to ${filePath}]
69843
+
69844
+ ${baseText}`;
69845
+ } catch (err) {
69846
+ console.error("[SecureChannel] Failed to download attachment:", err);
69847
+ return `[Attachment: ${String(attachmentInfo.filename ?? "file")} was sent but could not be retrieved: ${err}]
69848
+
69849
+ ${baseText}`;
69850
+ }
69851
+ }
69672
69852
  async _downloadAndDecryptAttachment(info) {
69673
69853
  const attachDir = join4(this.config.dataDir, "attachments");
69674
69854
  await mkdir3(attachDir, { recursive: true });
@@ -69688,7 +69868,7 @@ ${messageText}`;
69688
69868
  const fileKey = base64ToBytes(info.fileKey);
69689
69869
  const fileNonce = base64ToBytes(info.fileNonce);
69690
69870
  const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
69691
- const filePath = join4(attachDir, info.filename);
69871
+ const filePath = this._safeAttachmentPath(attachDir, info.filename);
69692
69872
  await writeFile3(filePath, decrypted);
69693
69873
  console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
69694
69874
  return { filePath, decrypted };
@@ -71109,6 +71289,7 @@ ${messageText}`;
71109
71289
  await this._persistState();
71110
71290
  }
71111
71291
  console.log(`[SecureChannel] Joined shared 1:1 MLS group for group ${conversationGroupId.slice(0, 8)} via Welcome (epoch=${mgr.epoch})`);
71292
+ await this._flushMlsHold(conversationGroupId);
71112
71293
  return;
71113
71294
  }
71114
71295
  if (conversationId) {
@@ -99222,7 +99403,7 @@ var init_index = __esm2({
99222
99403
  await init_skill_telemetry();
99223
99404
  await init_policy_enforcer();
99224
99405
  init_room_protocol();
99225
- VERSION = true ? "0.23.30" : "0.0.0-dev";
99406
+ VERSION = true ? "0.23.31" : "0.0.0-dev";
99226
99407
  }
99227
99408
  });
99228
99409
  await init_index();
@@ -100148,7 +100329,7 @@ async function main() {
100148
100329
  const info = parseInfoFlags(process.argv.slice(2));
100149
100330
  if (info.showVersion || info.showHelp) {
100150
100331
  const version2 = buildClientVersion2(
100151
- true ? "0.4.3" : void 0
100332
+ true ? "0.4.4" : void 0
100152
100333
  );
100153
100334
  if (info.showVersion) printLine(version2);
100154
100335
  if (info.showHelp) {
@@ -100201,7 +100382,7 @@ async function main() {
100201
100382
  agentName: cfg.agentName,
100202
100383
  platform: "node",
100203
100384
  clientVersion: buildClientVersion2(
100204
- true ? "0.4.3" : void 0
100385
+ true ? "0.4.4" : void 0
100205
100386
  )
100206
100387
  });
100207
100388
  channelRef = channel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/grok-bridge",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "description": "AgentVault Grok Bridge — daemon bridging a Grok agent into E2E-encrypted AgentVault direct messages and rooms.",
6
6
  "main": "dist/index.js",