@agentvault/claude-bridge 0.8.1 → 0.8.3

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.
63982
+ *
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:
63913
63986
  *
63914
- * @param memberIndex - The leaf index of the member to remove.
63915
- * @returns The commit message to broadcast.
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,17 +64596,28 @@ 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.30" : FALLBACK;
64472
64600
  return `${PACKAGE}@${v22}`;
64473
64601
  }
64602
+ function buildSha() {
64603
+ const raw = true ? "1fff5d6" : "";
64604
+ const cleaned = (raw ?? "").trim();
64605
+ return !cleaned || cleaned === "unknown" ? null : cleaned;
64606
+ }
64607
+ function withBuildSha(identity) {
64608
+ const sha = buildSha();
64609
+ if (!sha || identity.includes("+")) return identity;
64610
+ const stamped = `${identity}+${sha}`;
64611
+ return stamped.length <= MAX_LEN ? stamped : identity;
64612
+ }
64474
64613
  function buildClientVersion(override) {
64475
64614
  try {
64476
64615
  const candidate = (override ?? "").trim();
64477
64616
  if (candidate) {
64478
- const cleaned = candidate.replace(DISALLOWED, "").slice(0, MAX_LEN);
64617
+ const cleaned = withBuildSha(candidate.replace(DISALLOWED, "").slice(0, MAX_LEN));
64479
64618
  if (cleaned && !cleaned.includes("..")) return cleaned;
64480
64619
  }
64481
- return ownIdentity().replace(DISALLOWED, "").slice(0, MAX_LEN);
64620
+ return withBuildSha(ownIdentity().replace(DISALLOWED, "").slice(0, MAX_LEN));
64482
64621
  } catch {
64483
64622
  return FALLBACK;
64484
64623
  }
@@ -65380,6 +65519,7 @@ var SecureChannel;
65380
65519
  var init_channel = __esm2({
65381
65520
  async "src/channel.ts"() {
65382
65521
  "use strict";
65522
+ init_abstention();
65383
65523
  init_mls_commit_classify();
65384
65524
  await init_libsodium_wrappers();
65385
65525
  await init_dist();
@@ -65809,6 +65949,18 @@ var init_channel = __esm2({
65809
65949
  get ownHubAddress() {
65810
65950
  return this._persisted?.hubAddress ?? "";
65811
65951
  }
65952
+ /** This agent's AgentVault hub_id. "" until enrolment persists it.
65953
+ *
65954
+ * Lives in the credential store, NOT in the channel config — which is where
65955
+ * the memory-recall helper was reading it from. `channel.config.hubId` is
65956
+ * unset for every agent enrolled through the normal flow, so
65957
+ * `channel.config.hubId || ""` produced `/api/v1/agents//memory/search`, a
65958
+ * 404 on every inbound message, swallowed by `if (!res.ok) return ""`.
65959
+ * Memory recall was silently dead while the hub_id sat right here.
65960
+ */
65961
+ get ownHubId() {
65962
+ return this._persisted?.hubId ?? "";
65963
+ }
65812
65964
  /** Public best-effort trigger to (re)resolve this agent's display_name.
65813
65965
  * Called lazily from the 1:1 DM path when the name is still unknown, so a
65814
65966
  * resolve that failed at connect (or a fresh agent that hadn't registered
@@ -66197,6 +66349,7 @@ var init_channel = __esm2({
66197
66349
  * Each session gets the same plaintext encrypted independently.
66198
66350
  */
66199
66351
  async send(plaintext, options) {
66352
+ if (isAbstention(plaintext)) return;
66200
66353
  if (this._state === "error" || this._state === "idle") {
66201
66354
  throw new Error("Channel is not ready");
66202
66355
  }
@@ -66525,7 +66678,7 @@ var init_channel = __esm2({
66525
66678
  */
66526
66679
  sendActivitySpan(spanData) {
66527
66680
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
66528
- const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
66681
+ const pluginVersion = true ? "0.23.30" : "0.0.0-dev";
66529
66682
  const agentName = this.config.agentName ?? "Agent";
66530
66683
  const resource = {
66531
66684
  "service.name": "agentvault-agent",
@@ -66719,6 +66872,7 @@ var init_channel = __esm2({
66719
66872
  * Uses MLS (preferred) or pairwise fan-out as fallback.
66720
66873
  */
66721
66874
  async sendToRoom(roomId, plaintext, opts) {
66875
+ if (isAbstention(plaintext)) return;
66722
66876
  if (!this._persisted?.rooms?.[roomId]) {
66723
66877
  throw new Error(`Room ${roomId} not found`);
66724
66878
  }
@@ -68462,7 +68616,7 @@ var init_channel = __esm2({
68462
68616
  agentVersion: this.config.agentVersion ?? "0.0.0",
68463
68617
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68464
68618
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68465
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68619
+ pluginVersion: true ? "0.23.30" : "0.0.0-dev"
68466
68620
  });
68467
68621
  this._telemetryReporter.startAutoFlush(3e4);
68468
68622
  }
@@ -68786,7 +68940,7 @@ var init_channel = __esm2({
68786
68940
  agentVersion: this.config.agentVersion ?? "0.0.0",
68787
68941
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68788
68942
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68789
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68943
+ pluginVersion: true ? "0.23.30" : "0.0.0-dev"
68790
68944
  });
68791
68945
  this._telemetryReporter.startAutoFlush(3e4);
68792
68946
  }
@@ -72930,6 +73084,58 @@ var init_gateway_send = __esm2({
72930
73084
  init_openclaw_compat();
72931
73085
  }
72932
73086
  });
73087
+ function parseMentions(text) {
73088
+ const mentions = [];
73089
+ const re22 = new RegExp(MENTION_RE.source, MENTION_RE.flags);
73090
+ let match;
73091
+ while ((match = re22.exec(text)) !== null) {
73092
+ mentions.push(match[1].toLowerCase());
73093
+ }
73094
+ return mentions;
73095
+ }
73096
+ function resolveOwn(mention, names) {
73097
+ if (names.has(mention)) return mention;
73098
+ const parts = mention.split("-");
73099
+ for (let i2 = parts.length - 1; i2 > 0; i2--) {
73100
+ const candidate = parts.slice(0, i2).join("-");
73101
+ if (names.has(candidate)) return candidate;
73102
+ }
73103
+ return null;
73104
+ }
73105
+ function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
73106
+ const mentions = parseMentions(plaintext);
73107
+ if (mentions.length === 0) return false;
73108
+ const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
73109
+ if (mentions.some((m22) => resolveOwn(m22, ownNames) !== null)) return true;
73110
+ if (mentions.some((m22) => resolveOwn(m22, BROADCAST_NAMES) !== null)) {
73111
+ return senderIsAgent !== true;
73112
+ }
73113
+ if (!selfResolved) return true;
73114
+ return false;
73115
+ }
73116
+ function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
73117
+ const names = /* @__PURE__ */ new Set();
73118
+ const add4 = (n22) => {
73119
+ if (!n22) return;
73120
+ const lower = n22.toLowerCase();
73121
+ names.add(lower);
73122
+ names.add(lower.split(/[\s(]/)[0]);
73123
+ };
73124
+ add4(agentName);
73125
+ add4(accountId);
73126
+ add4(roomDisplayName);
73127
+ add4(ownDisplayName);
73128
+ return names;
73129
+ }
73130
+ var MENTION_RE;
73131
+ var BROADCAST_NAMES;
73132
+ var init_room_protocol = __esm2({
73133
+ "src/room-protocol.ts"() {
73134
+ "use strict";
73135
+ MENTION_RE = /@([\w-]+)/gi;
73136
+ BROADCAST_NAMES = /* @__PURE__ */ new Set(["all", "everyone"]);
73137
+ }
73138
+ });
72933
73139
  var init_llm_response_parser = __esm2({
72934
73140
  "src/llm-response-parser.ts"() {
72935
73141
  "use strict";
@@ -72963,6 +73169,7 @@ var isUsingManagedRoutes;
72963
73169
  var init_openclaw_entry = __esm2({
72964
73170
  "src/openclaw-entry.ts"() {
72965
73171
  "use strict";
73172
+ init_room_protocol();
72966
73173
  init_account_config();
72967
73174
  init_fetch_interceptor();
72968
73175
  init_http_handlers();
@@ -99021,7 +99228,8 @@ var init_index = __esm2({
99021
99228
  init_skill_invoker();
99022
99229
  await init_skill_telemetry();
99023
99230
  await init_policy_enforcer();
99024
- VERSION = true ? "0.23.27" : "0.0.0-dev";
99231
+ init_room_protocol();
99232
+ VERSION = true ? "0.23.30" : "0.0.0-dev";
99025
99233
  }
99026
99234
  });
99027
99235
  await init_index();
@@ -135189,9 +135397,32 @@ function roomTurnPrefix(roomId, senderName, roomName) {
135189
135397
  const room = roomName ? `"${roomName}"` : roomId.slice(0, 8);
135190
135398
  return `[room ${room} | ${senderName}]`;
135191
135399
  }
135400
+ function _makeOwesReply(agentName, channel, log) {
135401
+ let warnedNoIdentity = false;
135402
+ return (e7) => {
135403
+ const own = agentName ?? "";
135404
+ const hub = channel.ownDisplayName ?? "";
135405
+ if (!own && !hub && !warnedNoIdentity) {
135406
+ warnedNoIdentity = true;
135407
+ log(
135408
+ "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."
135409
+ );
135410
+ }
135411
+ return owesRoomReply(
135412
+ e7.plaintext,
135413
+ own,
135414
+ own,
135415
+ e7.senderIsAgent,
135416
+ void 0,
135417
+ hub,
135418
+ Boolean(own || hub)
135419
+ );
135420
+ };
135421
+ }
135192
135422
  function wireBridge(channel, session, target, opts = {}) {
135193
135423
  const log = opts.log ?? (() => {
135194
135424
  });
135425
+ const _owesReply = _makeOwesReply(opts.agentName, channel, log);
135195
135426
  let workAllowed = false;
135196
135427
  const workAllowedGetter = () => workAllowed;
135197
135428
  opts.onWorkAllowed?.(workAllowedGetter);
@@ -135243,7 +135474,7 @@ function wireBridge(channel, session, target, opts = {}) {
135243
135474
  target.setRoom(e7.roomId);
135244
135475
  session.push(`${roomTurnPrefix(e7.roomId, e7.senderName, roomNames.get(e7.roomId))}: ${e7.plaintext}`, target.snapshotReply(channel, log), {
135245
135476
  autoReplyOnText: false,
135246
- replyExpected: e7.senderIsAgent === false,
135477
+ replyExpected: _owesReply(e7),
135247
135478
  armed: () => arming.isArmed(e7.roomId)
135248
135479
  });
135249
135480
  });
@@ -135402,7 +135633,7 @@ async function main() {
135402
135633
  "[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
135634
  );
135404
135635
  }
135405
- logLine(`[bridge] version: ${true ? "0.8.1" : "dev"}`);
135636
+ logLine(`[bridge] version: ${true ? "0.8.3" : "dev"}`);
135406
135637
  logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
135407
135638
  logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
135408
135639
  if (cfg.armRoom) {
@@ -135434,7 +135665,7 @@ async function main() {
135434
135665
  // its default would render "@agentvault/agentvault@0.7.x" — the wrong
135435
135666
  // package name attached to the bridge's version number, which is worse
135436
135667
  // than either alone.
135437
- clientVersion: `@agentvault/claude-bridge@${true ? "0.8.1" : "dev"}`
135668
+ clientVersion: `@agentvault/claude-bridge@${true ? "0.8.3" : "dev"}`
135438
135669
  });
135439
135670
  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
135671
  const deviceJwt = () => {
@@ -135514,6 +135745,9 @@ async function main() {
135514
135745
  {
135515
135746
  roomFilter: cfg.roomFilter,
135516
135747
  armRoom: cfg.armRoom,
135748
+ // #1107: needed to recognise this agent's own @mention. Without it the
135749
+ // agent can only answer @all.
135750
+ agentName: cfg.agentName,
135517
135751
  log: (m6) => logLine("[bridge] " + m6),
135518
135752
  // Slice 2 Plan C (T11): live arm/disarm + local-approval poll + heartbeat.
135519
135753
  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.3",
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",