@agentvault/grok-bridge 0.4.1 → 0.4.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
@@ -9,9 +9,6 @@
9
9
  * Those are Claude-shaped; Grok's gate is the pre_tool_use hook and arrives with
10
10
  * the tools-on posture.
11
11
  */
12
- /** `RoomSession` is declared ONCE in this package, by grok-session.ts. The Claude
13
- * bridge declares its own in its bridge.ts; that declaration was NOT copied.
14
- * Type-only, so this cannot create a runtime import cycle. */
15
12
  import type { RoomSession } from "./grok-session.js";
16
13
  export interface RoomMessage {
17
14
  roomId: string;
@@ -167,6 +164,12 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
167
164
  */
168
165
  export declare function roomTurnPrefix(roomId: string, senderName: string, roomName?: string): string;
169
166
  export declare function wireBridge(channel: RoomChannel, session: RoomSession, target: ActiveTarget, opts?: {
167
+ /**
168
+ * #1107: this agent's own name, used to recognise its own @mention. Threaded
169
+ * from the bridge config. Without it the agent can only ever answer @all —
170
+ * `_makeOwesReply` warns once, loudly, rather than going quietly deaf.
171
+ */
172
+ agentName?: string;
170
173
  roomFilter?: string;
171
174
  log?: (msg: string) => void;
172
175
  }): void;
package/dist/index.js CHANGED
@@ -1074,6 +1074,28 @@ var __toESM = (mod4, isNodeMode, target) => (target = mod4 != null ? __create(__
1074
1074
  isNodeMode || !mod4 || !mod4.__esModule ? __defProp2(target, "default", { value: mod4, enumerable: true }) : target,
1075
1075
  mod4
1076
1076
  ));
1077
+ function stripWrappers(s2) {
1078
+ let start = 0;
1079
+ let end = s2.length;
1080
+ while (start < end && WRAPPERS.includes(s2[start])) start++;
1081
+ while (end > start && WRAPPERS.includes(s2[end - 1])) end--;
1082
+ return s2.slice(start, end);
1083
+ }
1084
+ function isAbstention(text) {
1085
+ if (typeof text !== "string") return false;
1086
+ return stripWrappers(text).toLowerCase().startsWith(SENTINEL);
1087
+ }
1088
+ var SENTINEL;
1089
+ var WRAPPERS;
1090
+ var INSTRUCTION;
1091
+ var init_abstention = __esm2({
1092
+ "src/abstention.ts"() {
1093
+ "use strict";
1094
+ SENTINEL = "[[no-reply]]";
1095
+ WRAPPERS = " \r\n`\"'*_";
1096
+ 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";
1097
+ }
1098
+ });
1077
1099
  function toEpoch(v2) {
1078
1100
  if (typeof v2 === "bigint") return v2;
1079
1101
  if (typeof v2 === "number") {
@@ -63752,6 +63774,39 @@ var init_mls_group = __esm2({
63752
63774
  const s2 = this._requireState();
63753
63775
  return s2.groupContext.groupId;
63754
63776
  }
63777
+ /**
63778
+ * Proof that this client actually holds the group's keys at its current
63779
+ * epoch. Throws if not initialized.
63780
+ *
63781
+ * RFC 9420 §8.2's `epoch_authenticator`, derived from the epoch secret. Two
63782
+ * properties make it the thing this codebase has been missing:
63783
+ *
63784
+ * * every member at the same epoch computes the SAME value, so it is
63785
+ * comparable across clients and can be checked by a server;
63786
+ * * it cannot be computed without the key schedule, so possessing it is
63787
+ * evidence of membership rather than a claim about it. RFC 9420 names
63788
+ * this as its purpose: "to authenticate the shared group state".
63789
+ *
63790
+ * It is safe to publish. It is a KDF output over the epoch secret, not the
63791
+ * secret, and the RFC intends it to be shown to out-of-band verifiers.
63792
+ *
63793
+ * WHY THIS EXISTS. `mls_groups.member_device_ids` records membership; the
63794
+ * ability to decrypt lives only in one browser's localStorage. Nothing
63795
+ * compared the two, and nothing could — so a device in the roster holding no
63796
+ * keys was indistinguishable from a healthy one, on every signal the server
63797
+ * has. Measured on production 2026-08-26: one owner device carried the keys
63798
+ * for 19 of the 21 owner leaves on the platform, and one 1:1 group had been
63799
+ * unreadable by its owner's browser for two months while every status column
63800
+ * read healthy.
63801
+ *
63802
+ * `epoch` alone cannot do this job: it is a number the client reports about
63803
+ * itself, it collides across groups, and a client that has lost its state
63804
+ * still remembers one.
63805
+ */
63806
+ get epochAuthenticator() {
63807
+ const s2 = this._requireState();
63808
+ return s2.keySchedule.epochAuthenticator;
63809
+ }
63755
63810
  /** The cipher suite name in use. */
63756
63811
  get ciphersuiteName() {
63757
63812
  return this._ciphersuiteName;
@@ -63902,11 +63957,84 @@ var init_mls_group = __esm2({
63902
63957
  return { commit: commitBytes, welcome: welcomeBytes };
63903
63958
  }
63904
63959
  /**
63905
- * Remove a member from the group by leaf index.
63960
+ * Admit N members with TWO commits total, replacing any stale leaf they hold.
63961
+ *
63962
+ * `addMembers` cannot do this alone: MLS rejects an Add for an identity
63963
+ * already in the tree, so a member that lost its local state must be removed
63964
+ * first. Doing that pair PER MEMBER is what the fulfiller did, and it is not
63965
+ * merely slow — it is self-defeating.
63966
+ *
63967
+ * Live 2026-08-28 14:02:21-28Z: the owner sweep re-admitted 7 room members
63968
+ * sequentially, each as its own removeMember + addMembers. Room 007 ran
63969
+ * **epoch 71 -> 85 in seven seconds**, and the Welcome minted for member n
63970
+ * named an epoch that member n+1's commit had already replaced — so six of
63971
+ * seven were stale before they finished joining, and one (`smartcode`) could
63972
+ * not replay forward afterwards at all.
63906
63973
  *
63907
- * @param memberIndex - The leaf index of the member to remove.
63908
- * @returns The commit message to broadcast.
63974
+ * 🎯 A REPAIR THAT MOVES THE EPOCH ONCE PER MEMBER IS RACING ITSELF.
63975
+ *
63976
+ * ⚠️ **Why two commits and not one.** RFC 9420 §12.3 allows Remove and Add in
63977
+ * a single commit, and ts-mls builds it happily — but with MORE THAN ONE of
63978
+ * each, a REMAINING member fails to process it:
63979
+ *
63980
+ * OpenError at applyUpdatePathSecret (ts-mls/src/createCommit.ts:471)
63981
+ * at updatePrivateKeyPath (processMessages.ts:397)
63982
+ *
63983
+ * One remove + one add in a single commit is fine; 2+2 is not. That is
63984
+ * pinned by a test in `mls-group.test.ts` so this cannot be "optimised" back
63985
+ * into one commit without the failure showing up first. Two commits still
63986
+ * turns 7 members from 14 epochs into 2, and every joiner lands current.
63987
+ *
63988
+ * @param keyPackages - Fresh KeyPackages, one per member to admit. Members
63989
+ * already holding a leaf are removed by the first commit; members with no
63990
+ * leaf are plain adds. Duplicate identities keep the first KeyPackage.
63991
+ * @returns Every commit to broadcast IN ORDER, and the Welcome to deliver to
63992
+ * EVERY member in the batch (each finds its own secrets inside it).
63909
63993
  */
63994
+ async reAdmitMembers(keyPackages) {
63995
+ this._requireState();
63996
+ await this._ensureCiphersuite();
63997
+ const seenIdentities = [];
63998
+ const toAdd = [];
63999
+ const leavesToRemove = [];
64000
+ for (const kp of keyPackages) {
64001
+ const identity = keyPackageIdentity(kp);
64002
+ if (identity !== null) {
64003
+ if (seenIdentities.some((id) => bytesEqual(id, identity)))
64004
+ continue;
64005
+ seenIdentities.push(identity);
64006
+ const leaf = this.memberLeafIndex(identity);
64007
+ if (leaf !== null)
64008
+ leavesToRemove.push(leaf);
64009
+ }
64010
+ toAdd.push(kp);
64011
+ }
64012
+ if (toAdd.length === 0) {
64013
+ throw new Error("reAdmitMembers: no members to admit");
64014
+ }
64015
+ const commits = [];
64016
+ if (leavesToRemove.length > 0) {
64017
+ const { commit } = await this.removeMembers(leavesToRemove);
64018
+ commits.push({ commit, epoch: this.epoch });
64019
+ }
64020
+ const added = await this.addMembers(toAdd);
64021
+ commits.push({ commit: added.commit, epoch: this.epoch });
64022
+ return { commits, welcome: added.welcome };
64023
+ }
64024
+ async removeMembers(memberIndices) {
64025
+ const s2 = this._requireState();
64026
+ const cs = await this._ensureCiphersuite();
64027
+ if (memberIndices.length === 0) {
64028
+ throw new Error("removeMembers: no members to remove");
64029
+ }
64030
+ const extraProposals = memberIndices.map((removed) => ({
64031
+ proposalType: "remove",
64032
+ remove: { removed }
64033
+ }));
64034
+ const result = await createCommit({ state: s2, cipherSuite: cs, pskIndex: this._pskIndex }, { extraProposals });
64035
+ this.state = result.newState;
64036
+ return { commit: encodeMlsMessage(result.commit) };
64037
+ }
63910
64038
  async removeMember(memberIndex) {
63911
64039
  const s2 = this._requireState();
63912
64040
  const cs = await this._ensureCiphersuite();
@@ -64461,7 +64589,7 @@ var init_mls_kp_pool = __esm2({
64461
64589
  }
64462
64590
  });
64463
64591
  function ownIdentity() {
64464
- const v2 = true ? "0.23.27" : FALLBACK;
64592
+ const v2 = true ? "0.23.29" : FALLBACK;
64465
64593
  return `${PACKAGE}@${v2}`;
64466
64594
  }
64467
64595
  function buildClientVersion(override) {
@@ -65373,6 +65501,7 @@ var SecureChannel;
65373
65501
  var init_channel = __esm2({
65374
65502
  async "src/channel.ts"() {
65375
65503
  "use strict";
65504
+ init_abstention();
65376
65505
  init_mls_commit_classify();
65377
65506
  await init_libsodium_wrappers();
65378
65507
  await init_dist();
@@ -65802,6 +65931,18 @@ var init_channel = __esm2({
65802
65931
  get ownHubAddress() {
65803
65932
  return this._persisted?.hubAddress ?? "";
65804
65933
  }
65934
+ /** This agent's AgentVault hub_id. "" until enrolment persists it.
65935
+ *
65936
+ * Lives in the credential store, NOT in the channel config — which is where
65937
+ * the memory-recall helper was reading it from. `channel.config.hubId` is
65938
+ * unset for every agent enrolled through the normal flow, so
65939
+ * `channel.config.hubId || ""` produced `/api/v1/agents//memory/search`, a
65940
+ * 404 on every inbound message, swallowed by `if (!res.ok) return ""`.
65941
+ * Memory recall was silently dead while the hub_id sat right here.
65942
+ */
65943
+ get ownHubId() {
65944
+ return this._persisted?.hubId ?? "";
65945
+ }
65805
65946
  /** Public best-effort trigger to (re)resolve this agent's display_name.
65806
65947
  * Called lazily from the 1:1 DM path when the name is still unknown, so a
65807
65948
  * resolve that failed at connect (or a fresh agent that hadn't registered
@@ -66190,6 +66331,7 @@ var init_channel = __esm2({
66190
66331
  * Each session gets the same plaintext encrypted independently.
66191
66332
  */
66192
66333
  async send(plaintext, options) {
66334
+ if (isAbstention(plaintext)) return;
66193
66335
  if (this._state === "error" || this._state === "idle") {
66194
66336
  throw new Error("Channel is not ready");
66195
66337
  }
@@ -66518,7 +66660,7 @@ var init_channel = __esm2({
66518
66660
  */
66519
66661
  sendActivitySpan(spanData) {
66520
66662
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
66521
- const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
66663
+ const pluginVersion = true ? "0.23.29" : "0.0.0-dev";
66522
66664
  const agentName = this.config.agentName ?? "Agent";
66523
66665
  const resource = {
66524
66666
  "service.name": "agentvault-agent",
@@ -66712,6 +66854,7 @@ var init_channel = __esm2({
66712
66854
  * Uses MLS (preferred) or pairwise fan-out as fallback.
66713
66855
  */
66714
66856
  async sendToRoom(roomId, plaintext, opts) {
66857
+ if (isAbstention(plaintext)) return;
66715
66858
  if (!this._persisted?.rooms?.[roomId]) {
66716
66859
  throw new Error(`Room ${roomId} not found`);
66717
66860
  }
@@ -68455,7 +68598,7 @@ var init_channel = __esm2({
68455
68598
  agentVersion: this.config.agentVersion ?? "0.0.0",
68456
68599
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68457
68600
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68458
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68601
+ pluginVersion: true ? "0.23.29" : "0.0.0-dev"
68459
68602
  });
68460
68603
  this._telemetryReporter.startAutoFlush(3e4);
68461
68604
  }
@@ -68779,7 +68922,7 @@ var init_channel = __esm2({
68779
68922
  agentVersion: this.config.agentVersion ?? "0.0.0",
68780
68923
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68781
68924
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68782
- pluginVersion: true ? "0.23.27" : "0.0.0-dev"
68925
+ pluginVersion: true ? "0.23.29" : "0.0.0-dev"
68783
68926
  });
68784
68927
  this._telemetryReporter.startAutoFlush(3e4);
68785
68928
  }
@@ -72923,6 +73066,45 @@ var init_gateway_send = __esm2({
72923
73066
  init_openclaw_compat();
72924
73067
  }
72925
73068
  });
73069
+ function parseMentions(text) {
73070
+ const mentions = [];
73071
+ const re2 = /@(\w[\w]*)/gi;
73072
+ let match;
73073
+ while ((match = re2.exec(text)) !== null) {
73074
+ mentions.push(match[1].toLowerCase());
73075
+ }
73076
+ return mentions;
73077
+ }
73078
+ function owesRoomReply(plaintext, agentName, accountId, senderIsAgent, roomDisplayName, ownDisplayName, selfResolved = true) {
73079
+ const mentions = parseMentions(plaintext);
73080
+ if (mentions.length === 0) return false;
73081
+ if (mentions.includes("all") || mentions.includes("everyone")) {
73082
+ return senderIsAgent !== true;
73083
+ }
73084
+ const ownNames = ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName);
73085
+ if (mentions.some((m2) => ownNames.has(m2))) return true;
73086
+ if (!selfResolved) return true;
73087
+ return false;
73088
+ }
73089
+ function ownRoomNames(agentName, accountId, roomDisplayName, ownDisplayName) {
73090
+ const names = /* @__PURE__ */ new Set();
73091
+ const add4 = (n2) => {
73092
+ if (!n2) return;
73093
+ const lower = n2.toLowerCase();
73094
+ names.add(lower);
73095
+ names.add(lower.split(/[\s(]/)[0]);
73096
+ };
73097
+ add4(agentName);
73098
+ add4(accountId);
73099
+ add4(roomDisplayName);
73100
+ add4(ownDisplayName);
73101
+ return names;
73102
+ }
73103
+ var init_room_protocol = __esm2({
73104
+ "src/room-protocol.ts"() {
73105
+ "use strict";
73106
+ }
73107
+ });
72926
73108
  var init_llm_response_parser = __esm2({
72927
73109
  "src/llm-response-parser.ts"() {
72928
73110
  "use strict";
@@ -72956,6 +73138,7 @@ var isUsingManagedRoutes;
72956
73138
  var init_openclaw_entry = __esm2({
72957
73139
  "src/openclaw-entry.ts"() {
72958
73140
  "use strict";
73141
+ init_room_protocol();
72959
73142
  init_account_config();
72960
73143
  init_fetch_interceptor();
72961
73144
  init_http_handlers();
@@ -99014,7 +99197,8 @@ var init_index = __esm2({
99014
99197
  init_skill_invoker();
99015
99198
  await init_skill_telemetry();
99016
99199
  await init_policy_enforcer();
99017
- VERSION = true ? "0.23.27" : "0.0.0-dev";
99200
+ init_room_protocol();
99201
+ VERSION = true ? "0.23.29" : "0.0.0-dev";
99018
99202
  }
99019
99203
  });
99020
99204
  await init_index();
@@ -99767,9 +99951,32 @@ function roomTurnPrefix(roomId, senderName, roomName) {
99767
99951
  const room = roomName ? `"${roomName}"` : roomId.slice(0, 8);
99768
99952
  return `[room ${room} | ${senderName}]`;
99769
99953
  }
99954
+ function _makeOwesReply(agentName, channel, log) {
99955
+ let warnedNoIdentity = false;
99956
+ return (e) => {
99957
+ const own = agentName ?? "";
99958
+ const hub = channel.ownDisplayName ?? "";
99959
+ if (!own && !hub && !warnedNoIdentity) {
99960
+ warnedNoIdentity = true;
99961
+ log(
99962
+ "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."
99963
+ );
99964
+ }
99965
+ return owesRoomReply(
99966
+ e.plaintext,
99967
+ own,
99968
+ own,
99969
+ e.senderIsAgent,
99970
+ void 0,
99971
+ hub,
99972
+ Boolean(own || hub)
99973
+ );
99974
+ };
99975
+ }
99770
99976
  function wireBridge(channel, session, target, opts = {}) {
99771
99977
  const log = opts.log ?? (() => {
99772
99978
  });
99979
+ const _owesReply = _makeOwesReply(opts.agentName, channel, log);
99773
99980
  channel.on("error", (err) => {
99774
99981
  const msg = err instanceof Error ? err.message : String(err);
99775
99982
  log(`channel error (non-fatal, not a reconnect trigger): ${msg}`);
@@ -99808,7 +100015,7 @@ function wireBridge(channel, session, target, opts = {}) {
99808
100015
  target.setRoom(e.roomId);
99809
100016
  session.push(`${roomTurnPrefix(e.roomId, e.senderName, roomNames.get(e.roomId))}: ${e.plaintext}`, target.snapshotReply(channel, log), {
99810
100017
  autoReplyOnText: false,
99811
- replyExpected: e.senderIsAgent === false
100018
+ replyExpected: _owesReply(e)
99812
100019
  });
99813
100020
  });
99814
100021
  channel.on("message", (text, metadata) => {
@@ -99917,7 +100124,7 @@ async function main() {
99917
100124
  const info = parseInfoFlags(process.argv.slice(2));
99918
100125
  if (info.showVersion || info.showHelp) {
99919
100126
  const version2 = buildClientVersion2(
99920
- true ? "0.4.1" : void 0
100127
+ true ? "0.4.2" : void 0
99921
100128
  );
99922
100129
  if (info.showVersion) printLine(version2);
99923
100130
  if (info.showHelp) {
@@ -99970,7 +100177,7 @@ async function main() {
99970
100177
  agentName: cfg.agentName,
99971
100178
  platform: "node",
99972
100179
  clientVersion: buildClientVersion2(
99973
- true ? "0.4.1" : void 0
100180
+ true ? "0.4.2" : void 0
99974
100181
  )
99975
100182
  });
99976
100183
  channelRef = channel;
@@ -100050,6 +100257,9 @@ async function main() {
100050
100257
  const target = new ActiveTarget();
100051
100258
  wireBridge(channel, session, target, {
100052
100259
  roomFilter: cfg.roomFilter,
100260
+ // #1107: needed to recognise this agent's own @mention. Without it the
100261
+ // agent can only answer @all.
100262
+ agentName: cfg.agentName,
100053
100263
  log: (m2) => logLine("[grok-bridge] " + m2)
100054
100264
  });
100055
100265
  attachLifecycle2(channel, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/grok-bridge",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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",