@agentvault/claude-bridge 0.8.1 → 0.8.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.
package/dist/bridge.d.ts CHANGED
@@ -212,6 +212,12 @@ export declare function pollApprovalsOnce(arming: ArmingState, dataDir: string,
212
212
  */
213
213
  export declare function roomTurnPrefix(roomId: string, senderName: string, roomName?: string): string;
214
214
  export declare function wireBridge(channel: RoomChannel, session: RoomSession, target: ActiveTarget, opts?: {
215
+ /**
216
+ * #1107: this agent's own name, used to recognise its own @mention. Threaded
217
+ * from the bridge config. Without it the agent can only ever answer @all —
218
+ * `_makeOwesReply` warns once, loudly, rather than going quietly deaf.
219
+ */
220
+ agentName?: string;
215
221
  roomFilter?: string;
216
222
  armRoom?: boolean;
217
223
  log?: (msg: string) => void;
package/dist/index.js CHANGED
@@ -1081,6 +1081,28 @@ var __toESM = (mod4, isNodeMode, target) => (target = mod4 != null ? __create(__
1081
1081
  isNodeMode || !mod4 || !mod4.__esModule ? __defProp2(target, "default", { value: mod4, enumerable: true }) : target,
1082
1082
  mod4
1083
1083
  ));
1084
+ function stripWrappers(s22) {
1085
+ let start = 0;
1086
+ let end = s22.length;
1087
+ while (start < end && WRAPPERS.includes(s22[start])) start++;
1088
+ while (end > start && WRAPPERS.includes(s22[end - 1])) end--;
1089
+ return s22.slice(start, end);
1090
+ }
1091
+ function isAbstention(text) {
1092
+ if (typeof text !== "string") return false;
1093
+ return stripWrappers(text).toLowerCase().startsWith(SENTINEL);
1094
+ }
1095
+ var SENTINEL;
1096
+ var WRAPPERS;
1097
+ var INSTRUCTION;
1098
+ var init_abstention = __esm2({
1099
+ "src/abstention.ts"() {
1100
+ "use strict";
1101
+ SENTINEL = "[[no-reply]]";
1102
+ WRAPPERS = " \r\n`\"'*_";
1103
+ INSTRUCTION = "not every message needs an answer from you: if this one is not addressed to you and asks nothing of you, reply with exactly " + SENTINEL + " and nothing else \u2014 that is discarded, not posted, and is how you stay quiet; never post a message SAYING that you are staying quiet";
1104
+ }
1105
+ });
1084
1106
  function toEpoch(v22) {
1085
1107
  if (typeof v22 === "bigint") return v22;
1086
1108
  if (typeof v22 === "number") {
@@ -63759,6 +63781,39 @@ var init_mls_group = __esm2({
63759
63781
  const s22 = this._requireState();
63760
63782
  return s22.groupContext.groupId;
63761
63783
  }
63784
+ /**
63785
+ * Proof that this client actually holds the group's keys at its current
63786
+ * epoch. Throws if not initialized.
63787
+ *
63788
+ * RFC 9420 §8.2's `epoch_authenticator`, derived from the epoch secret. Two
63789
+ * properties make it the thing this codebase has been missing:
63790
+ *
63791
+ * * every member at the same epoch computes the SAME value, so it is
63792
+ * comparable across clients and can be checked by a server;
63793
+ * * it cannot be computed without the key schedule, so possessing it is
63794
+ * evidence of membership rather than a claim about it. RFC 9420 names
63795
+ * this as its purpose: "to authenticate the shared group state".
63796
+ *
63797
+ * It is safe to publish. It is a KDF output over the epoch secret, not the
63798
+ * secret, and the RFC intends it to be shown to out-of-band verifiers.
63799
+ *
63800
+ * WHY THIS EXISTS. `mls_groups.member_device_ids` records membership; the
63801
+ * ability to decrypt lives only in one browser's localStorage. Nothing
63802
+ * compared the two, and nothing could — so a device in the roster holding no
63803
+ * keys was indistinguishable from a healthy one, on every signal the server
63804
+ * has. Measured on production 2026-08-26: one owner device carried the keys
63805
+ * for 19 of the 21 owner leaves on the platform, and one 1:1 group had been
63806
+ * unreadable by its owner's browser for two months while every status column
63807
+ * read healthy.
63808
+ *
63809
+ * `epoch` alone cannot do this job: it is a number the client reports about
63810
+ * itself, it collides across groups, and a client that has lost its state
63811
+ * still remembers one.
63812
+ */
63813
+ get epochAuthenticator() {
63814
+ const s22 = this._requireState();
63815
+ return s22.keySchedule.epochAuthenticator;
63816
+ }
63762
63817
  /** The cipher suite name in use. */
63763
63818
  get ciphersuiteName() {
63764
63819
  return this._ciphersuiteName;
@@ -63909,11 +63964,84 @@ var init_mls_group = __esm2({
63909
63964
  return { commit: commitBytes, welcome: welcomeBytes };
63910
63965
  }
63911
63966
  /**
63912
- * Remove a member from the group by leaf index.
63967
+ * Admit N members with TWO commits total, replacing any stale leaf they hold.
63968
+ *
63969
+ * `addMembers` cannot do this alone: MLS rejects an Add for an identity
63970
+ * already in the tree, so a member that lost its local state must be removed
63971
+ * first. Doing that pair PER MEMBER is what the fulfiller did, and it is not
63972
+ * merely slow — it is self-defeating.
63973
+ *
63974
+ * Live 2026-08-28 14:02:21-28Z: the owner sweep re-admitted 7 room members
63975
+ * sequentially, each as its own removeMember + addMembers. Room 007 ran
63976
+ * **epoch 71 -> 85 in seven seconds**, and the Welcome minted for member n
63977
+ * named an epoch that member n+1's commit had already replaced — so six of
63978
+ * seven were stale before they finished joining, and one (`smartcode`) could
63979
+ * not replay forward afterwards at all.
63980
+ *
63981
+ * 🎯 A REPAIR THAT MOVES THE EPOCH ONCE PER MEMBER IS RACING ITSELF.
63913
63982
  *
63914
- * @param memberIndex - The leaf index of the member to remove.
63915
- * @returns The commit message to broadcast.
63983
+ * ⚠️ **Why two commits and not one.** RFC 9420 §12.3 allows Remove and Add in
63984
+ * a single commit, and ts-mls builds it happily — but with MORE THAN ONE of
63985
+ * each, a REMAINING member fails to process it:
63986
+ *
63987
+ * OpenError at applyUpdatePathSecret (ts-mls/src/createCommit.ts:471)
63988
+ * at updatePrivateKeyPath (processMessages.ts:397)
63989
+ *
63990
+ * One remove + one add in a single commit is fine; 2+2 is not. That is
63991
+ * pinned by a test in `mls-group.test.ts` so this cannot be "optimised" back
63992
+ * into one commit without the failure showing up first. Two commits still
63993
+ * turns 7 members from 14 epochs into 2, and every joiner lands current.
63994
+ *
63995
+ * @param keyPackages - Fresh KeyPackages, one per member to admit. Members
63996
+ * already holding a leaf are removed by the first commit; members with no
63997
+ * leaf are plain adds. Duplicate identities keep the first KeyPackage.
63998
+ * @returns Every commit to broadcast IN ORDER, and the Welcome to deliver to
63999
+ * EVERY member in the batch (each finds its own secrets inside it).
63916
64000
  */
64001
+ async reAdmitMembers(keyPackages) {
64002
+ this._requireState();
64003
+ await this._ensureCiphersuite();
64004
+ const seenIdentities = [];
64005
+ const toAdd = [];
64006
+ const leavesToRemove = [];
64007
+ for (const kp of keyPackages) {
64008
+ const identity = keyPackageIdentity(kp);
64009
+ if (identity !== null) {
64010
+ if (seenIdentities.some((id) => bytesEqual(id, identity)))
64011
+ continue;
64012
+ seenIdentities.push(identity);
64013
+ const leaf = this.memberLeafIndex(identity);
64014
+ if (leaf !== null)
64015
+ leavesToRemove.push(leaf);
64016
+ }
64017
+ toAdd.push(kp);
64018
+ }
64019
+ if (toAdd.length === 0) {
64020
+ throw new Error("reAdmitMembers: no members to admit");
64021
+ }
64022
+ const commits = [];
64023
+ if (leavesToRemove.length > 0) {
64024
+ const { commit } = await this.removeMembers(leavesToRemove);
64025
+ commits.push({ commit, epoch: this.epoch });
64026
+ }
64027
+ const added = await this.addMembers(toAdd);
64028
+ commits.push({ commit: added.commit, epoch: this.epoch });
64029
+ return { commits, welcome: added.welcome };
64030
+ }
64031
+ async removeMembers(memberIndices) {
64032
+ const s22 = this._requireState();
64033
+ const cs = await this._ensureCiphersuite();
64034
+ if (memberIndices.length === 0) {
64035
+ throw new Error("removeMembers: no members to remove");
64036
+ }
64037
+ const extraProposals = memberIndices.map((removed) => ({
64038
+ proposalType: "remove",
64039
+ remove: { removed }
64040
+ }));
64041
+ const result = await createCommit({ state: s22, cipherSuite: cs, pskIndex: this._pskIndex }, { extraProposals });
64042
+ this.state = result.newState;
64043
+ return { commit: encodeMlsMessage(result.commit) };
64044
+ }
63917
64045
  async removeMember(memberIndex) {
63918
64046
  const s22 = this._requireState();
63919
64047
  const cs = await this._ensureCiphersuite();
@@ -64468,7 +64596,7 @@ var init_mls_kp_pool = __esm2({
64468
64596
  }
64469
64597
  });
64470
64598
  function ownIdentity() {
64471
- const v22 = true ? "0.23.27" : FALLBACK;
64599
+ const v22 = true ? "0.23.29" : FALLBACK;
64472
64600
  return `${PACKAGE}@${v22}`;
64473
64601
  }
64474
64602
  function buildClientVersion(override) {
@@ -65380,6 +65508,7 @@ var SecureChannel;
65380
65508
  var init_channel = __esm2({
65381
65509
  async "src/channel.ts"() {
65382
65510
  "use strict";
65511
+ init_abstention();
65383
65512
  init_mls_commit_classify();
65384
65513
  await init_libsodium_wrappers();
65385
65514
  await init_dist();
@@ -65809,6 +65938,18 @@ var init_channel = __esm2({
65809
65938
  get ownHubAddress() {
65810
65939
  return this._persisted?.hubAddress ?? "";
65811
65940
  }
65941
+ /** This agent's AgentVault hub_id. "" until enrolment persists it.
65942
+ *
65943
+ * Lives in the credential store, NOT in the channel config — which is where
65944
+ * the memory-recall helper was reading it from. `channel.config.hubId` is
65945
+ * unset for every agent enrolled through the normal flow, so
65946
+ * `channel.config.hubId || ""` produced `/api/v1/agents//memory/search`, a
65947
+ * 404 on every inbound message, swallowed by `if (!res.ok) return ""`.
65948
+ * Memory recall was silently dead while the hub_id sat right here.
65949
+ */
65950
+ get ownHubId() {
65951
+ return this._persisted?.hubId ?? "";
65952
+ }
65812
65953
  /** Public best-effort trigger to (re)resolve this agent's display_name.
65813
65954
  * Called lazily from the 1:1 DM path when the name is still unknown, so a
65814
65955
  * resolve that failed at connect (or a fresh agent that hadn't registered
@@ -66197,6 +66338,7 @@ var init_channel = __esm2({
66197
66338
  * Each session gets the same plaintext encrypted independently.
66198
66339
  */
66199
66340
  async send(plaintext, options) {
66341
+ if (isAbstention(plaintext)) return;
66200
66342
  if (this._state === "error" || this._state === "idle") {
66201
66343
  throw new Error("Channel is not ready");
66202
66344
  }
@@ -66525,7 +66667,7 @@ var init_channel = __esm2({
66525
66667
  */
66526
66668
  sendActivitySpan(spanData) {
66527
66669
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
66528
- const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
66670
+ const pluginVersion = true ? "0.23.29" : "0.0.0-dev";
66529
66671
  const agentName = this.config.agentName ?? "Agent";
66530
66672
  const resource = {
66531
66673
  "service.name": "agentvault-agent",
@@ -66719,6 +66861,7 @@ var init_channel = __esm2({
66719
66861
  * Uses MLS (preferred) or pairwise fan-out as fallback.
66720
66862
  */
66721
66863
  async sendToRoom(roomId, plaintext, opts) {
66864
+ if (isAbstention(plaintext)) return;
66722
66865
  if (!this._persisted?.rooms?.[roomId]) {
66723
66866
  throw new Error(`Room ${roomId} not found`);
66724
66867
  }
@@ -68462,7 +68605,7 @@ var init_channel = __esm2({
68462
68605
  agentVersion: this.config.agentVersion ?? "0.0.0",
68463
68606
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68464
68607
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68465
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68608
+ pluginVersion: true ? "0.23.29" : "0.0.0-dev"
68466
68609
  });
68467
68610
  this._telemetryReporter.startAutoFlush(3e4);
68468
68611
  }
@@ -68786,7 +68929,7 @@ var init_channel = __esm2({
68786
68929
  agentVersion: this.config.agentVersion ?? "0.0.0",
68787
68930
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68788
68931
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68789
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68932
+ pluginVersion: true ? "0.23.29" : "0.0.0-dev"
68790
68933
  });
68791
68934
  this._telemetryReporter.startAutoFlush(3e4);
68792
68935
  }
@@ -72930,6 +73073,45 @@ var init_gateway_send = __esm2({
72930
73073
  init_openclaw_compat();
72931
73074
  }
72932
73075
  });
73076
+ function parseMentions(text) {
73077
+ const mentions = [];
73078
+ const re22 = /@(\w[\w]*)/gi;
73079
+ let match;
73080
+ while ((match = re22.exec(text)) !== null) {
73081
+ mentions.push(match[1].toLowerCase());
73082
+ }
73083
+ return mentions;
73084
+ }
73085
+ function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
73086
+ const mentions = parseMentions(plaintext);
73087
+ if (mentions.length === 0) return false;
73088
+ if (mentions.includes("all") || mentions.includes("everyone")) {
73089
+ return senderIsAgent !== true;
73090
+ }
73091
+ const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
73092
+ if (mentions.some((m22) => ownNames.has(m22))) return true;
73093
+ if (!selfResolved) return true;
73094
+ return false;
73095
+ }
73096
+ function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
73097
+ const names = /* @__PURE__ */ new Set();
73098
+ const add4 = (n22) => {
73099
+ if (!n22) return;
73100
+ const lower = n22.toLowerCase();
73101
+ names.add(lower);
73102
+ names.add(lower.split(/[\s(]/)[0]);
73103
+ };
73104
+ add4(agentName);
73105
+ add4(accountId);
73106
+ add4(roomDisplayName);
73107
+ add4(ownDisplayName);
73108
+ return names;
73109
+ }
73110
+ var init_room_protocol = __esm2({
73111
+ "src/room-protocol.ts"() {
73112
+ "use strict";
73113
+ }
73114
+ });
72933
73115
  var init_llm_response_parser = __esm2({
72934
73116
  "src/llm-response-parser.ts"() {
72935
73117
  "use strict";
@@ -72963,6 +73145,7 @@ var isUsingManagedRoutes;
72963
73145
  var init_openclaw_entry = __esm2({
72964
73146
  "src/openclaw-entry.ts"() {
72965
73147
  "use strict";
73148
+ init_room_protocol();
72966
73149
  init_account_config();
72967
73150
  init_fetch_interceptor();
72968
73151
  init_http_handlers();
@@ -99021,7 +99204,8 @@ var init_index = __esm2({
99021
99204
  init_skill_invoker();
99022
99205
  await init_skill_telemetry();
99023
99206
  await init_policy_enforcer();
99024
- VERSION = true ? "0.23.27" : "0.0.0-dev";
99207
+ init_room_protocol();
99208
+ VERSION = true ? "0.23.29" : "0.0.0-dev";
99025
99209
  }
99026
99210
  });
99027
99211
  await init_index();
@@ -135189,9 +135373,32 @@ function roomTurnPrefix(roomId, senderName, roomName) {
135189
135373
  const room = roomName ? `"${roomName}"` : roomId.slice(0, 8);
135190
135374
  return `[room ${room} | ${senderName}]`;
135191
135375
  }
135376
+ function _makeOwesReply(agentName, channel, log) {
135377
+ let warnedNoIdentity = false;
135378
+ return (e7) => {
135379
+ const own = agentName ?? "";
135380
+ const hub = channel.ownDisplayName ?? "";
135381
+ if (!own && !hub && !warnedNoIdentity) {
135382
+ warnedNoIdentity = true;
135383
+ log(
135384
+ "WARNING: this bridge has no agentName and no hub display_name, so it cannot recognise its own @mention (#1107). It will answer @all and any @-addressed message (fail-open), but targeted replies will be unreliable. Set the agent name in the bridge config."
135385
+ );
135386
+ }
135387
+ return owesRoomReply(
135388
+ e7.plaintext,
135389
+ own,
135390
+ own,
135391
+ e7.senderIsAgent,
135392
+ void 0,
135393
+ hub,
135394
+ Boolean(own || hub)
135395
+ );
135396
+ };
135397
+ }
135192
135398
  function wireBridge(channel, session, target, opts = {}) {
135193
135399
  const log = opts.log ?? (() => {
135194
135400
  });
135401
+ const _owesReply = _makeOwesReply(opts.agentName, channel, log);
135195
135402
  let workAllowed = false;
135196
135403
  const workAllowedGetter = () => workAllowed;
135197
135404
  opts.onWorkAllowed?.(workAllowedGetter);
@@ -135243,7 +135450,7 @@ function wireBridge(channel, session, target, opts = {}) {
135243
135450
  target.setRoom(e7.roomId);
135244
135451
  session.push(`${roomTurnPrefix(e7.roomId, e7.senderName, roomNames.get(e7.roomId))}: ${e7.plaintext}`, target.snapshotReply(channel, log), {
135245
135452
  autoReplyOnText: false,
135246
- replyExpected: e7.senderIsAgent === false,
135453
+ replyExpected: _owesReply(e7),
135247
135454
  armed: () => arming.isArmed(e7.roomId)
135248
135455
  });
135249
135456
  });
@@ -135402,7 +135609,7 @@ async function main() {
135402
135609
  "[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"
135403
135610
  );
135404
135611
  }
135405
- logLine(`[bridge] version: ${true ? "0.8.1" : "dev"}`);
135612
+ logLine(`[bridge] version: ${true ? "0.8.2" : "dev"}`);
135406
135613
  logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
135407
135614
  logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
135408
135615
  if (cfg.armRoom) {
@@ -135434,7 +135641,7 @@ async function main() {
135434
135641
  // its default would render "@agentvault/agentvault@0.7.x" — the wrong
135435
135642
  // package name attached to the bridge's version number, which is worse
135436
135643
  // than either alone.
135437
- clientVersion: `@agentvault/claude-bridge@${true ? "0.8.1" : "dev"}`
135644
+ clientVersion: `@agentvault/claude-bridge@${true ? "0.8.2" : "dev"}`
135438
135645
  });
135439
135646
  const agentSystemPrompt = cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`;
135440
135647
  const deviceJwt = () => {
@@ -135514,6 +135721,9 @@ async function main() {
135514
135721
  {
135515
135722
  roomFilter: cfg.roomFilter,
135516
135723
  armRoom: cfg.armRoom,
135724
+ // #1107: needed to recognise this agent's own @mention. Without it the
135725
+ // agent can only answer @all.
135726
+ agentName: cfg.agentName,
135517
135727
  log: (m6) => logLine("[bridge] " + m6),
135518
135728
  // Slice 2 Plan C (T11): live arm/disarm + local-approval poll + heartbeat.
135519
135729
  workspaceDir: cfg.workspaceDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.8.1",
3
+ "version": "0.8.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",