@agentvault/claude-bridge 0.7.0 → 0.7.2

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.
@@ -19,8 +19,30 @@ export declare function trustDir(root?: string): string;
19
19
  */
20
20
  export declare function isTrusted(deviceId: string, root?: string): boolean;
21
21
  export declare function grant(deviceId: string, root?: string): void;
22
- /** Returns true only if a marker was actually removed. */
22
+ /**
23
+ * Returns true only if a real marker was actually removed.
24
+ *
25
+ * M1. This used `existsSync` + `rmSync(p, { force: true, recursive: true })`.
26
+ * `existsSync` is true for a DIRECTORY, so a directory squatting at a marker
27
+ * path was recursively deleted — contents and all — and reported as a
28
+ * successful revoke. Reached in practice through `untrust-host --all`
29
+ * (approve-cli.ts:131), which revokes everything `listTrusted()` returns.
30
+ *
31
+ * Only a regular file is a marker, which is already `isTrusted`'s rule; `lstat`
32
+ * so a symlink is never followed or counted. Anything else was never a grant,
33
+ * so removing it is not this function's job — and refusing keeps `recursive`
34
+ * out of this file entirely.
35
+ *
36
+ * The invariant callers actually depend on is preserved: after `revoke()`,
37
+ * `isTrusted()` is false, because it rejects non-files too.
38
+ */
23
39
  export declare function revoke(deviceId: string, root?: string): boolean;
40
+ /**
41
+ * Filtered through `isTrusted` rather than the filename alone, so the listing
42
+ * cannot disagree with the decision. A name-only filter reported directories
43
+ * and symlinks as trusted devices, which both over-stated trust to an operator
44
+ * reading the list and fed non-markers to `revoke()` in the `--all` path.
45
+ */
24
46
  export declare function listTrusted(root?: string): string[];
25
47
  /**
26
48
  * Resolve this agent's device-id from its persisted identity. `trust-host` runs
package/dist/index.js CHANGED
@@ -349,7 +349,7 @@ var init_systemd = __esm({
349
349
 
350
350
  // src/service/backend.ts
351
351
  import { spawnSync } from "node:child_process";
352
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync5, chmodSync as chmodSync2 } from "node:fs";
352
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync4, chmodSync as chmodSync3 } from "node:fs";
353
353
  import { userInfo } from "node:os";
354
354
  function defaultDeps() {
355
355
  return {
@@ -362,9 +362,9 @@ function defaultDeps() {
362
362
  },
363
363
  writeFile: (p2, d10) => writeFileSync3(p2, d10),
364
364
  mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
365
- chmod: (p2, m6) => chmodSync2(p2, m6),
365
+ chmod: (p2, m6) => chmodSync3(p2, m6),
366
366
  rm: (p2) => rmSync4(p2, { force: true }),
367
- exists: (p2) => existsSync5(p2)
367
+ exists: (p2) => existsSync4(p2)
368
368
  };
369
369
  }
370
370
  function selectBackend(platform, deps) {
@@ -64679,6 +64679,10 @@ var init_channel = __esm2({
64679
64679
  _sessionGroupIds = /* @__PURE__ */ new Map();
64680
64680
  /** Consecutive MLS decrypt failure count per group key for epoch recovery. */
64681
64681
  _mlsDecryptFailCounts = /* @__PURE__ */ new Map();
64682
+ /** #474: per-group consecutive processCommit failures. Mirrors the decrypt
64683
+ * counter above — a commit that fails is divergence, and divergence must
64684
+ * self-heal rather than be logged and dropped. */
64685
+ _mlsCommitFailCounts = /* @__PURE__ */ new Map();
64682
64686
  static MAX_MLS_DECRYPT_FAILS = 3;
64683
64687
  /** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
64684
64688
  _mlsKeyPackage = null;
@@ -65559,7 +65563,7 @@ var init_channel = __esm2({
65559
65563
  */
65560
65564
  sendActivitySpan(spanData) {
65561
65565
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65562
- const pluginVersion = true ? "0.23.11" : "0.0.0-dev";
65566
+ const pluginVersion = true ? "0.23.12" : "0.0.0-dev";
65563
65567
  const agentName = this.config.agentName ?? "Agent";
65564
65568
  const resource = {
65565
65569
  "service.name": "agentvault-agent",
@@ -67298,7 +67302,7 @@ var init_channel = __esm2({
67298
67302
  agentVersion: this.config.agentVersion ?? "0.0.0",
67299
67303
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67300
67304
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67301
- pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67305
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
67302
67306
  });
67303
67307
  this._telemetryReporter.startAutoFlush(3e4);
67304
67308
  }
@@ -67616,7 +67620,7 @@ var init_channel = __esm2({
67616
67620
  agentVersion: this.config.agentVersion ?? "0.0.0",
67617
67621
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67618
67622
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67619
- pluginVersion: true ? "0.23.11" : "0.0.0-dev"
67623
+ pluginVersion: true ? "0.23.12" : "0.0.0-dev"
67620
67624
  });
67621
67625
  this._telemetryReporter.startAutoFlush(3e4);
67622
67626
  }
@@ -69012,14 +69016,9 @@ ${messageText}`;
69012
69016
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69013
69017
  console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69014
69018
  } catch (err) {
69015
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
69016
- if (isOldEpoch) {
69017
- console.log(`[SecureChannel] Discarding old-epoch commit for room ${roomId.slice(0, 8)}`);
69018
- } else {
69019
- console.error(`[SecureChannel] MLS commit failed for room ${roomId.slice(0, 8)}:`, err);
69020
- throw err;
69021
- }
69019
+ await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
69022
69020
  }
69021
+ this._mlsCommitFailCounts.delete(roomId);
69023
69022
  } else {
69024
69023
  this._bufferMlsCommit(groupId, epoch, data);
69025
69024
  console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69037,14 +69036,9 @@ ${messageText}`;
69037
69036
  await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
69038
69037
  console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69039
69038
  } catch (err) {
69040
- const isOldEpoch = err?.message?.includes("epoch") || err?.message?.includes("old") || err?.message?.includes("Validation");
69041
- if (isOldEpoch) {
69042
- console.log(`[SecureChannel] Discarding old-epoch commit for A2A ${chId.slice(0, 8)}`);
69043
- } else {
69044
- console.error(`[SecureChannel] MLS commit failed for A2A ${chId.slice(0, 8)}:`, err);
69045
- throw err;
69046
- }
69039
+ await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
69047
69040
  }
69041
+ this._mlsCommitFailCounts.delete(`a2a:${chId}`);
69048
69042
  } else {
69049
69043
  this._bufferMlsCommit(groupId, epoch, data);
69050
69044
  console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
@@ -69101,6 +69095,52 @@ ${messageText}`;
69101
69095
  * sends us one. Deduped to a 30s window per group so a burst of undecryptable
69102
69096
  * rows doesn't spam request-welcome. Fire-and-forget.
69103
69097
  */
69098
+ /**
69099
+ * #474 — a failed `processCommit` is DIVERGENCE, and divergence must
69100
+ * self-heal. It previously did neither: the room stayed broken forever.
69101
+ *
69102
+ * Two swallow paths existed, and only the second was ever reported:
69103
+ *
69104
+ * 1. `isOldEpoch` substring-matched the error text for "epoch" / "old" /
69105
+ * "Validation" and discarded deliberately. `ValidationError: Desired gen
69106
+ * in the past` contains "Validation", so the LOUDEST production failure
69107
+ * was filed as benign — 1,439 occurrences on one agent, all thrown away
69108
+ * by a string match.
69109
+ * 2. Everything else re-threw to the WS dispatcher, which logs and drops.
69110
+ *
69111
+ * Result: ~1,600 commit failures across 13 rooms and ZERO resyncs. Classifying
69112
+ * divergence by grepping an error message is the root of it — error text is
69113
+ * not a protocol, and the one case that must never be silent was the one that
69114
+ * happened to contain the magic word.
69115
+ *
69116
+ * So: no classification. COUNT, and after MAX_MLS_DECRYPT_FAILS consecutive
69117
+ * failures run the same ladder the decrypt path has used since #440 — replay
69118
+ * the missed commits via /sync, and fall back to re-join only if that cannot
69119
+ * catch up. Counting rather than resyncing on first failure matters: a
69120
+ * genuinely stale commit is normal, and resyncing on each one would have
69121
+ * fired ~1,600 Welcome requests.
69122
+ *
69123
+ * Never re-throws. The caller's only handler logs and drops, so throwing here
69124
+ * is indistinguishable from swallowing — it just moves the swallow one frame up.
69125
+ */
69126
+ async _onCommitFailure(groupKey, mlsGroupId, err, label) {
69127
+ const count = (this._mlsCommitFailCounts.get(groupKey) ?? 0) + 1;
69128
+ this._mlsCommitFailCounts.set(groupKey, count);
69129
+ console.error(
69130
+ `[SecureChannel] MLS commit failed (${count}/${_SecureChannel.MAX_MLS_DECRYPT_FAILS}) for ${label}:`,
69131
+ err
69132
+ );
69133
+ if (count < _SecureChannel.MAX_MLS_DECRYPT_FAILS) return;
69134
+ console.warn(`[SecureChannel] MLS commit resync triggered for ${label} \u2014 ${count} consecutive failures`);
69135
+ try {
69136
+ if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
69137
+ await this._triggerMlsEpochRecovery(groupKey, mlsGroupId);
69138
+ }
69139
+ this._mlsCommitFailCounts.delete(groupKey);
69140
+ } catch (recoverErr) {
69141
+ console.error(`[SecureChannel] MLS commit resync FAILED for ${label}:`, recoverErr);
69142
+ }
69143
+ }
69104
69144
  async _requestWelcomeSelfHeal(groupId) {
69105
69145
  const now = Date.now();
69106
69146
  const last = this._welcomeSelfHealInflight.get(groupId) ?? 0;
@@ -69902,9 +69942,43 @@ ${messageText}`;
69902
69942
  const errMsg = addErr instanceof Error ? addErr.message : String(addErr);
69903
69943
  if (errMsg.includes("already in the group")) {
69904
69944
  console.log(`[SecureChannel] A2A member already in group \u2014 removing and re-adding ${req.requesting_device_id.slice(0, 8)}`);
69945
+ const preRemoveState = JSON.stringify(mlsGroup.exportState());
69946
+ let removeCommitPublished = false;
69905
69947
  try {
69906
69948
  const identityBytes = new TextEncoder().encode(req.requesting_device_id);
69907
- await mlsGroup.removeMember(identityBytes);
69949
+ const leafIndex = mlsGroup.memberLeafIndex(identityBytes);
69950
+ if (leafIndex === null) {
69951
+ console.log(`[SecureChannel] A2A re-add: no leaf index for ${req.requesting_device_id.slice(0, 8)} \u2014 cannot remove`);
69952
+ continue;
69953
+ }
69954
+ const removeResult = await mlsGroup.removeMember(leafIndex);
69955
+ try {
69956
+ const removeResp = await fetch(
69957
+ `${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
69958
+ {
69959
+ method: "POST",
69960
+ headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
69961
+ body: JSON.stringify({
69962
+ group_id: chState.mlsGroupId,
69963
+ device_id: this._deviceId,
69964
+ commit: Buffer.from(removeResult.commit).toString("hex"),
69965
+ commit_epoch: Number(mlsGroup.epoch)
69966
+ })
69967
+ }
69968
+ );
69969
+ if (removeResp.ok) {
69970
+ removeCommitPublished = true;
69971
+ } else {
69972
+ const detail = await removeResp.text().catch(() => "");
69973
+ console.warn(`[SecureChannel] A2A remove-commit distribute failed (${removeResp.status}): ${detail}`);
69974
+ }
69975
+ } catch (distErr) {
69976
+ console.warn(`[SecureChannel] A2A remove-commit distribute network error:`, distErr);
69977
+ }
69978
+ if (!removeCommitPublished) {
69979
+ mlsGroup.importState(JSON.parse(preRemoveState));
69980
+ continue;
69981
+ }
69908
69982
  const kpRes2 = await fetch(
69909
69983
  `${this.config.apiUrl}/api/v1/mls/key-packages/batch?device_ids=${req.requesting_device_id}`,
69910
69984
  { headers: { Authorization: `Bearer ${this._deviceJwt}` } }
@@ -69928,6 +70002,12 @@ ${messageText}`;
69928
70002
  }
69929
70003
  } catch (readdErr) {
69930
70004
  console.log(`[SecureChannel] A2A remove+re-add FAILED: ${readdErr instanceof Error ? readdErr.message : String(readdErr)}`);
70005
+ if (!removeCommitPublished) {
70006
+ try {
70007
+ mlsGroup.importState(JSON.parse(preRemoveState));
70008
+ } catch {
70009
+ }
70010
+ }
69931
70011
  continue;
69932
70012
  }
69933
70013
  } else {
@@ -97395,7 +97475,7 @@ var init_index = __esm2({
97395
97475
  init_skill_invoker();
97396
97476
  await init_skill_telemetry();
97397
97477
  await init_policy_enforcer();
97398
- VERSION = true ? "0.23.11" : "0.0.0-dev";
97478
+ VERSION = true ? "0.23.12" : "0.0.0-dev";
97399
97479
  }
97400
97480
  });
97401
97481
  await init_index();
@@ -118702,10 +118782,12 @@ import { realpathSync as realpathSync2 } from "node:fs";
118702
118782
  import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
118703
118783
 
118704
118784
  // src/host-trust.ts
118705
- import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, existsSync as existsSync3, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync4 } from "node:fs";
118785
+ import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync4, chmodSync as chmodSync2 } from "node:fs";
118706
118786
  import { join as join6 } from "node:path";
118707
118787
  import { homedir, hostname as hostname3 } from "node:os";
118708
118788
  var TRUST_SUBDIR = "host-trust";
118789
+ var DIR_MODE3 = 448;
118790
+ var FILE_MODE3 = 384;
118709
118791
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
118710
118792
  var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
118711
118793
  var HostTrustError = class extends Error {
@@ -118731,23 +118813,33 @@ function isTrusted(deviceId, root3) {
118731
118813
  function grant(deviceId, root3) {
118732
118814
  const id = sanitize(deviceId);
118733
118815
  const dir = trustDir(root3);
118734
- mkdirSync3(dir, { recursive: true });
118816
+ const p2 = join6(dir, id);
118817
+ mkdirSync3(dir, { recursive: true, mode: DIR_MODE3 });
118735
118818
  writeFileSync(
118736
- join6(dir, id),
118819
+ p2,
118737
118820
  `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
118738
- `
118821
+ `,
118822
+ { mode: FILE_MODE3 }
118739
118823
  );
118824
+ chmodSync2(dir, DIR_MODE3);
118825
+ chmodSync2(p2, FILE_MODE3);
118740
118826
  }
118741
118827
  function revoke(deviceId, root3) {
118742
118828
  const id = sanitize(deviceId);
118743
118829
  const p2 = join6(trustDir(root3), id);
118744
- if (!existsSync3(p2)) return false;
118745
- rmSync2(p2, { force: true, recursive: true });
118830
+ try {
118831
+ if (!lstatSync2(p2).isFile()) return false;
118832
+ } catch {
118833
+ return false;
118834
+ }
118835
+ rmSync2(p2, { force: true });
118746
118836
  return true;
118747
118837
  }
118748
118838
  function listTrusted(root3) {
118749
118839
  try {
118750
- return readdirSync2(trustDir(root3)).filter((n10) => ID_RE.test(n10));
118840
+ return readdirSync2(trustDir(root3)).filter(
118841
+ (n10) => ID_RE.test(n10) && isTrusted(n10, root3)
118842
+ );
118751
118843
  } catch {
118752
118844
  return [];
118753
118845
  }
@@ -133227,7 +133319,7 @@ var ArmingState = class {
133227
133319
  };
133228
133320
 
133229
133321
  // src/approve-cli.ts
133230
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync3, existsSync as existsSync4 } from "node:fs";
133322
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync3, existsSync as existsSync3 } from "node:fs";
133231
133323
  import { join as join7 } from "node:path";
133232
133324
  var APPROVALS_SUBDIR = "arm-approvals";
133233
133325
  var ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
@@ -133254,7 +133346,7 @@ function writeApproval(dataDir, requestId, roomId) {
133254
133346
  }
133255
133347
  function drainApprovals(dataDir) {
133256
133348
  const dir = join7(dataDir, APPROVALS_SUBDIR);
133257
- if (!existsSync4(dir)) return [];
133349
+ if (!existsSync3(dir)) return [];
133258
133350
  const out = [];
133259
133351
  for (const name of readdirSync3(dir)) {
133260
133352
  if (!ID_RE2.test(name)) continue;
@@ -133473,7 +133565,14 @@ function wireBridge(channel, session, target, opts = {}) {
133473
133565
  e7.hushedUntil ? `room ${e7.roomId.slice(0, 8)} hushed until ${e7.hushedUntil} \u2014 holding` : `room ${e7.roomId.slice(0, 8)} hush cleared`
133474
133566
  );
133475
133567
  });
133568
+ let warnedMissingSenderIsAgent = false;
133476
133569
  channel.on("room_message", (e7) => {
133570
+ if (typeof e7.senderIsAgent !== "boolean" && !warnedMissingSenderIsAgent) {
133571
+ warnedMissingSenderIsAgent = true;
133572
+ log(
133573
+ "WARNING: inbound room_message has no `senderIsAgent` \u2014 replies to humans in rooms will be DROPPED whenever the model answers in plain text instead of calling say (#630). Most likely cause: a stale bundled plugin dist. Rebuild the bridge (npm run build in packages/claude-room-bridge) and reinstall."
133574
+ );
133575
+ }
133477
133576
  if (opts.roomFilter && e7.roomId !== opts.roomFilter) return;
133478
133577
  if (hush.isHushed(e7.roomId)) {
133479
133578
  log(`inbound from ${e7.senderName} in ${e7.roomId.slice(0, 8)} \u2014 room hushed, holding (not replying)`);
@@ -133632,7 +133731,7 @@ async function main() {
133632
133731
  "[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"
133633
133732
  );
133634
133733
  }
133635
- console.error(`[bridge] version: ${true ? "0.7.0" : "dev"}`);
133734
+ console.error(`[bridge] version: ${true ? "0.7.2" : "dev"}`);
133636
133735
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
133637
133736
  console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
133638
133737
  if (cfg.armRoom) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",