@securecoms/agent-bridge 0.26.5 → 0.27.0

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.
@@ -33,6 +33,7 @@ import { type OwnerAuthorityStore, type OwnerPin } from "@agentvault/connect-cli
33
33
  import type { BridgeConfig } from "./config.js";
34
34
  import { LatencyRecorder } from "./latency.js";
35
35
  import type { AttachmentMeta, VaultRefEntry } from "./attachments.js";
36
+ import { type MemoryNoteResultFrame } from "./memory/note-frames.js";
36
37
  import { type ChatMessage } from "./voice/fast-prompt.js";
37
38
  import { VOICE_CALL_START_MARKER } from "./voice/prompt-compiler.js";
38
39
  import { type VaultListNode, type VaultReadExpectations } from "./vault.js";
@@ -65,6 +66,9 @@ export type BridgeToolGrant = {
65
66
  * note. Every
66
67
  * other audience, an unpinned or anomalous owner, and the kill switch
67
68
  * `SECURECOMS_OWNER_DM_DEFAULT_GRANTS` (anything but unset/`on`) get `[]`.
69
+ * PR R adds exactly one more non-empty list, ROOM_DEFAULT_READ_GRANTS
70
+ * (`current_time` only), for an INTERNAL room's same-org human sender; see
71
+ * authorizationFor.
68
72
  * SEC-06 is LIVE for these two tools: docs/security/sec-06-runtime-bounds-audit.md.
69
73
  */
70
74
  /**
@@ -75,18 +79,84 @@ export type BridgeToolGrant = {
75
79
  * and `http_request` are deliberately absent.
76
80
  */
77
81
  export declare const OWNER_DM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
82
+ /**
83
+ * PR R (spec §7A.1a; controller ruling 2026-10-05, audit r1: current_time
84
+ * only). Granted only in a room classified INTERNAL from the server roster
85
+ * (every row, leaf rows included, in the agent owner's org), to a same-org
86
+ * human sender, while the roster is as fresh as the agent's MLS epoch, while
87
+ * a declared per-tool consumer is attached (audit C-B1), and while
88
+ * SECURECOMS_ROOM_DEFAULT_GRANTS is on (plan OQ-3). Frozen; the runtime intersects
89
+ * it with ROOM_SAFE_READ_TOOLS (turn-loop.ts).
90
+ */
91
+ export declare const ROOM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
78
92
  /**
79
93
  * Kill switch for OWNER_DM_DEFAULT_READ_GRANTS, read once at BridgeAgent
80
94
  * construction (daemon startup). Unset or exactly `on` keeps the grants;
81
95
  * any other value (`off`, `0`, `false`, empty) sends `[]` on every turn.
82
96
  */
83
97
  export declare function ownerDmDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
98
+ /**
99
+ * Kill switch for ROOM_DEFAULT_READ_GRANTS (plan OQ-3, RULED), read once at
100
+ * BridgeAgent construction exactly like ownerDmDefaultGrantsEnabled. Unset or
101
+ * exactly `on` keeps the room default; any other value sends `[]` on every
102
+ * room turn. It never touches the owner DM.
103
+ */
104
+ export declare function roomDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
84
105
  export interface BridgeAuthorization {
85
106
  readonly version: 1;
86
107
  readonly audience: AgentAudience;
87
108
  readonly sender_membership: "active" | "unknown";
88
109
  readonly tool_grants: readonly BridgeToolGrant[];
110
+ /**
111
+ * Audit r2 R2-T1 (plan OQ-6 ruling): "owner" ONLY on authorizationFor's
112
+ * ownerRelayed branch: the channel's server-marked relay sent a turn whose
113
+ * relayed speaker is the channel's pinned owner. Set by the daemon, never
114
+ * copied from message text. A harness connector honours a grant on
115
+ * audience owner_dm or on this mark, nothing else.
116
+ */
117
+ readonly relay_speaker?: "owner";
118
+ /**
119
+ * Markdown memory 7a (spec §8.3, OQ-B): true ONLY on authorizationFor's
120
+ * ownerDirect branch with memoryProvider "securecoms": the channel's pinned
121
+ * owner (never "anomalous") typed this turn in their DM, the roster is
122
+ * fresh at the held epoch and group (or the DM is cycle-fresh), and the
123
+ * owner-DM kill switch is on. Present only when true. An OFFER HINT: the
124
+ * bridge's W1 to W8 are the gate. Never set on the ownerRelayed branch
125
+ * (OQ-C, Eric approved 2026-10-06): voice turns read notes, never write,
126
+ * and never on any turn with a relay speaker claim (review I-1).
127
+ * NOT a tool grant: it is not, and must never be, in `tool_grants`.
128
+ * Reaches only the per-tool runtime consumer: withConsumerGrants strips it
129
+ * for a harness socket and the HTTP/SSE paths (review m-3).
130
+ */
131
+ readonly memory_write?: true;
132
+ }
133
+ /**
134
+ * Spec §8.2: what ws-server tells applyMemoryNote about the socket a
135
+ * memory_note frame arrived on. Read live: W2 is re-evaluated after awaits.
136
+ */
137
+ export interface MemoryNoteContext {
138
+ /**
139
+ * THIS socket's in-flight record for a message id (the original, never the
140
+ * consumer copy) and the token of that delivery. A nack, backoff and
141
+ * redelivery on the same socket reuses the record object but gets a new
142
+ * token (review m-1).
143
+ */
144
+ lookup(messageId: string): {
145
+ record: BridgeMessage;
146
+ token: number;
147
+ } | undefined;
148
+ /** The socket's own session facts (OQ-J): consumer and the sticky harness claim. */
149
+ readonly session: {
150
+ readonly consumer: "runtime" | "unknown";
151
+ readonly harnessClaimed?: boolean;
152
+ };
153
+ /** The delivery token of the frame's message id, captured when the frame arrived; undefined when not in flight. */
154
+ token: number | undefined;
155
+ /** Per-socket request ids still being decided (M-2). At most MAX_PENDING_NOTE_REQUESTS (review m-3). */
156
+ pendingRequests: Set<string>;
89
157
  }
158
+ /** Review m-3: pending memory_note requests per socket; a frame beyond this refuses `invalid` with no echo. */
159
+ export declare const MAX_PENDING_NOTE_REQUESTS = 8;
90
160
  export interface BridgeMessage {
91
161
  channel_id: string;
92
162
  message_id: string;
@@ -114,6 +184,27 @@ export interface BridgeMessage {
114
184
  * @mention filter reads (not recomputed differently).
115
185
  */
116
186
  sender_is_agent?: boolean;
187
+ /**
188
+ * Audit r2 N-L3 (PR-1): this turn is a voice transcript. W8 never quotes
189
+ * such a record. Present only when true. Set when either holds:
190
+ * - the turn is relay-attributed (`speaker` set). `speaker` is honoured
191
+ * only from a sender the server roster marks `is_relay`, so on this path
192
+ * the mark does not depend on the body. The room relay's `sendTurn`
193
+ * (connect-room-relay/src/turns.ts) also sets `transcribed: true`, and
194
+ * the hosted voice worker reaches the bridge only through that relay;
195
+ * - the E2E body says `transcribed: true`. The owner's own web client sets
196
+ * it on an on-device transcript (connect-web MessagingShell.tsx
197
+ * `sendConfirmedVoiceToChannel`, reached from CallPanel's
198
+ * `sendAgentMessage` via `confirmedSendBody`).
199
+ * LIMITATION (spec §15): on the owner-device path the mark is an
200
+ * UNAUTHENTICATED claim in the body. A modified or legacy client, or a
201
+ * device added under the owner's identity by a compromised server (the
202
+ * ADR-0010 gap), can send speech-derived text without it, and the bridge
203
+ * then treats it as typed. That adds no capability (the sender could type
204
+ * the same words), so the mark is fail-closed but not a cryptographic
205
+ * guarantee.
206
+ */
207
+ transcribed?: true;
117
208
  /**
118
209
  * Track 5: materialized attachment metadata for `artifact_share` messages.
119
210
  * Each entry corresponds to one downloaded+decrypted file saved to disk.
@@ -517,6 +608,15 @@ export declare class SendForbiddenError extends Error {
517
608
  readonly channelId: string;
518
609
  constructor(channelId: string);
519
610
  }
611
+ /**
612
+ * Audit M-B1 (controller ruling 2026-10-05): a reply to a room-granted turn
613
+ * would be sealed at an epoch the room was not classified at, and the room is
614
+ * not internal there. Dropped. A SendForbiddenError, so both transports treat
615
+ * it as terminal (send_forbidden: the runtime acks, nothing redelivers).
616
+ */
617
+ export declare class RoomClassChangedSendError extends SendForbiddenError {
618
+ constructor(channelId: string);
619
+ }
520
620
  /**
521
621
  * Stranded sender (spec 2026-09-26 §7.4): the backend refused a publish with
522
622
  * 409 `mls.group_superseded`: the envelope was sealed under a group that is no
@@ -896,6 +996,21 @@ export declare class BridgeAgent {
896
996
  private readonly selfHealSweepEnabled;
897
997
  /** Read once at startup; see ownerDmDefaultGrantsEnabled. */
898
998
  private readonly ownerDmDefaultGrants;
999
+ /** Read once at startup; see roomDefaultGrantsEnabled. */
1000
+ private readonly roomDefaultGrants;
1001
+ /**
1002
+ * Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
1003
+ * cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
1004
+ */
1005
+ private readonly ownerDmCycleFresh;
1006
+ /**
1007
+ * Audit M-B1: room-granted turns, keyed by the inbound message id, with the
1008
+ * channel, MLS group and epoch the room was classified at (review I-1: an
1009
+ * epoch NUMBER alone repeats across channels and across a supersede). A
1010
+ * reply to one is sealed only there, or re-classified once
1011
+ * (publishBodyForReply). Forgotten at the runtime's ack.
1012
+ */
1013
+ private readonly roomGrantEpochs;
899
1014
  /** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
900
1015
  private static readonly SELF_HEAL_SWEEP_INTERVAL_MS;
901
1016
  /**
@@ -1015,6 +1130,56 @@ export declare class BridgeAgent {
1015
1130
  private startedAt;
1016
1131
  private readonly store;
1017
1132
  private readonly memory;
1133
+ /**
1134
+ * Markdown memory 7a: the one owner of every note mutation and of the
1135
+ * delivery cache (spec §6.3, §7.2). One per store, so one per identity.
1136
+ */
1137
+ private readonly notes;
1138
+ /**
1139
+ * Fix round 1 (concern 1): the applied clear generation when each pending
1140
+ * runtime turn was FIRST staged (W7b compares it). Keyed by message id,
1141
+ * persisted inside the sealed runtime_inbox payload and restored with it;
1142
+ * never a BridgeMessage field. Removed when the turn is acked or shed.
1143
+ */
1144
+ private readonly ingestGeneration;
1145
+ /** Spec §11.2 (OQ-L): set by refetchRuntimeConfig (Task 8); notes are on until a payload says otherwise. */
1146
+ private memoryConfig;
1147
+ /**
1148
+ * Task 8 fix round 1 (review I-1, N-L5): set once the BOOT refetch has
1149
+ * returned, on success or failure. Until then prepareNotesForTurn only
1150
+ * fills and never runs the legacy import: the boot drain pulls turns before
1151
+ * the boot refetch, and an import there would move facts into notes that a
1152
+ * clear issued while the machine was offline then deletes for good.
1153
+ * warmRestoredNotes runs the import for the buffered (drained) turns after.
1154
+ */
1155
+ private bootConfigSettled;
1156
+ /**
1157
+ * Final review M-1: false until a config refetch has returned a payload in
1158
+ * THIS process. Until then notes are off for reads and writes
1159
+ * (notesProviderOn), whatever memoryConfig says: the last applied payload
1160
+ * may have turned notes off, and that state is not persisted. Distinct from
1161
+ * bootConfigSettled, which turns true when the boot refetch RETURNED, on
1162
+ * success or failure (settled is not notes-on).
1163
+ */
1164
+ private memoryConfigConfirmed;
1165
+ /**
1166
+ * Final review I-1: true once a /v1/ws consumer completed a hello that
1167
+ * declares it reads notes (MEMORY_NOTES_FEATURE) as the runtime, with no
1168
+ * harness claim on that socket (ws-server). A 0.9.x runtime's hello does not
1169
+ * declare it, a harness socket never qualifies, and a socket that sends no
1170
+ * hello never does. Until then no legacy fact is moved into notes (those
1171
+ * consumers recall facts, never notes) and the report says
1172
+ * memory_notes: false. Per process and never reset: a runtime restart
1173
+ * closes the socket but is not a downgrade.
1174
+ */
1175
+ private notesConsumerSeen;
1176
+ /** Review M-1: the refetch now running, and the ONE follow-up every caller during it shares. */
1177
+ private refetchInFlight;
1178
+ private refetchFollowUp;
1179
+ /** Review M-4: the (payload, applied) pair last logged as regressed. */
1180
+ private regressedLogged;
1181
+ /** Review M-5: an out-of-range applied generation is logged once. */
1182
+ private generationRangeLogged;
1018
1183
  /** Task 2: warn-once-per-channel guard for the enclave-map fallback (below). */
1019
1184
  private enclaveFallbackWarned;
1020
1185
  /** Task 2: dedupe guard so ingestOnce writes a channel's enclave at most once
@@ -1076,6 +1241,7 @@ export declare class BridgeAgent {
1076
1241
  * loose until the next save.
1077
1242
  */
1078
1243
  refetchRuntimeConfig(): Promise<void>;
1244
+ private refetchRuntimeConfigOnce;
1079
1245
  /**
1080
1246
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
1081
1247
  *
@@ -1509,6 +1675,25 @@ export declare class BridgeAgent {
1509
1675
  * there so a retry can resend the exact bytes).
1510
1676
  */
1511
1677
  private publishBody;
1678
+ /**
1679
+ * Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
1680
+ * only at the epoch the room was classified at. On a move, the room is
1681
+ * re-classified ONCE from a roster fetched at the new epoch: still internal,
1682
+ * the reply is sealed at that epoch; anything else, it is dropped. The check
1683
+ * throws before an envelope is built, so nothing is stored or posted.
1684
+ *
1685
+ * `inReplyTo` is the STORED intent's inbound id, not the send's options, so
1686
+ * the redelivery replay (replayReplyIntents, which has no in_reply_to in its
1687
+ * options) is checked the same as a live send.
1688
+ */
1689
+ private publishBodyForReply;
1690
+ /**
1691
+ * Re-read the room at the agent's current group and epoch; true only if it
1692
+ * is still internal there. Review I-1: the entry must come from THIS re-read
1693
+ * (a fail-soft refresh leaves an older entry whose epoch label can repeat
1694
+ * across a supersede), and the group must not move around the reads.
1695
+ */
1696
+ private reclassifyForReply;
1512
1697
  private enforceBodyRateFloor;
1513
1698
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
1514
1699
  private afterPublish;
@@ -1706,6 +1891,101 @@ export declare class BridgeAgent {
1706
1891
  * never falls back to the runtime's retired plaintext JSON files.
1707
1892
  */
1708
1893
  private memoryForTurn;
1894
+ /**
1895
+ * Spec §11.2 (OQ-L), §6.5: apply one parsed `payload.memory`. An unreadable
1896
+ * value turns notes off and deletes nothing. A readable one goes through
1897
+ * NotesBook.clear, which purges only on a HIGHER generation (under the note
1898
+ * mutex) and resyncs on equal or lower; a lower one logs
1899
+ * memory.clear_generation_regressed. Content-free logs only.
1900
+ *
1901
+ * While a purge is pending (the payload is above the applied generation)
1902
+ * notes are held OFF, and they come back on only after the purge committed.
1903
+ * A purge that throws therefore leaves notes off (fail closed); it is
1904
+ * caught here (review M-2) so the rest of the config still applies and
1905
+ * reports, and the next refetch retries it. A refetch that fails before
1906
+ * this point keeps the last applied state.
1907
+ */
1908
+ private applyPayloadMemory;
1909
+ /**
1910
+ * Review M-5: the report key only for a value the backend accepts
1911
+ * (0..MAX_CLEAR_GENERATION); otherwise the key is omitted, so one bad
1912
+ * mirror value cannot 422 the whole report, and the event logs once.
1913
+ */
1914
+ private reportedClearGeneration;
1915
+ /**
1916
+ * W1 and R1: the provider is securecoms, a config payload has arrived in
1917
+ * this process (final review M-1), and the parsed payload keeps notes on.
1918
+ */
1919
+ private notesProviderOn;
1920
+ /**
1921
+ * Final review I-1: ws-server calls this when a socket completes a hello as
1922
+ * the runtime (no harness claim) that declares MEMORY_NOTES_FEATURE. The
1923
+ * first one also refetches the runtime config, so the report says
1924
+ * memory_notes: true now rather than at the next 6-hour refetch.
1925
+ */
1926
+ noteNotesConsumer(): void;
1927
+ /**
1928
+ * R2 (spec §7.3): the frozen marks that only the full owner predicate in
1929
+ * authorizationFor sets. `memory_write` (the pinned owner typed this turn)
1930
+ * or `relay_speaker === "owner"` (the pinned owner spoke it through the
1931
+ * relay; read only, never write, OQ-C). Strict `=== true` / `=== "owner"`:
1932
+ * `audience === "owner_dm"` alone is not enough (§0.1 item 7).
1933
+ *
1934
+ * Review I-1 (spec §8.3): the owner-DM kill switch is re-checked HERE, at
1935
+ * read time, and so is the provider (R1). A turn restored from
1936
+ * runtime_inbox carries the marks frozen by the previous process; with
1937
+ * SECURECOMS_OWNER_DM_DEFAULT_GRANTS now off, or the provider no longer
1938
+ * securecoms, it gets no notes, and the boot warm runs no legacy import.
1939
+ */
1940
+ private notesReadable;
1941
+ /** The applied clear generation, for a socket that gets no notes (R5 decided first, review m-2). */
1942
+ notesAppliedGeneration(): number;
1943
+ /**
1944
+ * ws-server flush (synchronous). R1 to R3 here; R5 in withConsumerNotes; R4
1945
+ * holds because nothing else calls this. Reads only the generation-checked
1946
+ * cache (N-M1): a miss gives no notes. `generation` is the applied clear
1947
+ * generation the socket records for W7b.
1948
+ */
1949
+ notesForDelivery(m: BridgeMessage): {
1950
+ notes?: string;
1951
+ generation: number;
1952
+ };
1953
+ /**
1954
+ * §7.2 Warm and §10 import, BEFORE the turn is staged. Never throws.
1955
+ * The import runs only for a `memory_write` turn (M-6: owner-DM channels
1956
+ * only); the turn's own frozen authorization is the proof NotesBook checks.
1957
+ */
1958
+ private prepareNotesForTurn;
1959
+ /**
1960
+ * §7.2 + N-L5: after the boot refetch applied any clear, warm each channel
1961
+ * holding an R2-eligible restored turn, before start() returns (bin.ts
1962
+ * attaches /v1/ws only after start()). `this.buffer` also holds the turns
1963
+ * the boot drain pulled; their prepare ran fill-only (review I-1), so the
1964
+ * legacy import for them runs here, after the purge.
1965
+ */
1966
+ private warmRestoredNotes;
1967
+ /**
1968
+ * Spec §8.2: the write gate for one `memory_note` frame, fail closed, in
1969
+ * table order: W1, well formed (and a request_id not already pending on
1970
+ * this socket, M-2), W2, W3, W4(a), W4(b), W8, W5, then W2 and W1 once more
1971
+ * after the await, then under the note mutex W7, W2 and W1 again after the
1972
+ * seal and right before the synchronous commit, and W7b (the generation
1973
+ * stamped when the turn was first staged, re-checked inside the store
1974
+ * transaction; a turn with no stamp refuses `cleared`). Every refusal logs `memory.note_refused
1975
+ * { reason_class }` and nothing else (§8.6); no note text, frame body or id
1976
+ * is ever logged. Never rejects: an unexpected throw refuses with `store`.
1977
+ *
1978
+ * Everything that decides WHERE and WHETHER a note is written comes from
1979
+ * the bridge-held in-flight record that `ctx.lookup` returns for THIS
1980
+ * socket, never from the frame: the channel (W3), the frozen
1981
+ * authorization (W4(a)), the sender (W4(b)), and the source text and the
1982
+ * `transcribed` mark (W8). The note text that reaches NotesBook is the
1983
+ * owner's own span, produced only by `prepareStoredNote` (W8 then W5).
1984
+ *
1985
+ * LEAF LOCK: heldPosition (W4(b)) is awaited BEFORE the note mutex is
1986
+ * taken; nothing under the mutex awaits it.
1987
+ */
1988
+ applyMemoryNote(raw: Record<string, unknown>, ctx: MemoryNoteContext): Promise<MemoryNoteResultFrame>;
1709
1989
  memoryContext(conversationId: string, limit?: number): Promise<import("@agentvault/connect-mls-store").MemoryContextResult>;
1710
1990
  /**
1711
1991
  * Voice front desk, step 1 — decided SYNCHRONOUSLY inside the ingest cycle,
@@ -1947,6 +2227,19 @@ export declare class BridgeAgent {
1947
2227
  private enclavesToCover;
1948
2228
  /** A live roster for a channel, or undefined if missing OR expired past the TTL. */
1949
2229
  private freshRoster;
2230
+ /** The whole cache entry when fresh, else undefined (TTL; the epoch is checked by the caller). */
2231
+ private freshRosterEntry;
2232
+ /** The agent's held MLS group id for a channel; null when none (fail closed). */
2233
+ private heldGroupFor;
2234
+ /**
2235
+ * Review I-1 + fix round 2: the agent's held (group, epoch) for a channel.
2236
+ * The group is read on BOTH sides of the epoch read, so the epoch provably
2237
+ * belongs to it (a supersede in between installs a new group whose epoch
2238
+ * counter restarts); a group that moved, or none held, is null (fail closed).
2239
+ */
2240
+ private heldPosition;
2241
+ /** The agent's own MLS epoch for a channel; null when no group is held (fail closed). */
2242
+ private heldEpochFor;
1950
2243
  /**
1951
2244
  * Resolve one channel's roster (members + display names + is-agent) from the
1952
2245
  * GET members endpoint into the roster cache. Called AT MOST ONCE per ingest
@@ -1955,6 +2248,15 @@ export declare class BridgeAgent {
1955
2248
  * attribution must NEVER block or drop a message.
1956
2249
  */
1957
2250
  private refreshChannelRoster;
2251
+ /**
2252
+ * S-1 (roster) + §7A.5 + audit M-B4: once per ingest cycle, per distinct
2253
+ * channel, re-read the roster when the TTL lapsed OR the epoch moved since
2254
+ * the fetch, then record which DMs are epoch-fresh for the rest of this
2255
+ * cycle. Fail-soft inside refreshChannelRoster (never blocks ingest).
2256
+ */
2257
+ private refreshRostersForCycle;
2258
+ /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
2259
+ private authorizeTurn;
1958
2260
  /**
1959
2261
  * Classify the sender only from server-authenticated channel/roster data.
1960
2262
  * Missing or stale metadata is UNKNOWN with zero grants, never a permissive
@@ -1972,6 +2274,13 @@ export declare class BridgeAgent {
1972
2274
  * relayed `speaker_identity_id` (passed by the caller only for relay
1973
2275
  * turns) is that same pinned owner, whose roster row is the agent's
1974
2276
  * owner. A speaker claim from any non-relay sender gets nothing.
2277
+ * Both need the roster fetched at `heldEpoch` (the agent's current MLS
2278
+ * epoch for the channel), or the DM marked epoch-fresh at this ingest
2279
+ * cycle's refresh (audit M-B4).
2280
+ * PR R: ROOM_DEFAULT_READ_GRANTS only when the room kill switch is on, a
2281
+ * per-tool consumer is attached, the roster was fetched at `heldEpoch`
2282
+ * (no cycle carve-out), the room is INTERNAL (roomIsCrossOrg === false
2283
+ * over every row, leaf rows included) and the audience is team_room.
1975
2284
  * Everything else gets the frozen empty list.
1976
2285
  *
1977
2286
  * LIMIT (harness gate review H2, 2026-10-05): "pinned structural owner"
@@ -2000,6 +2309,15 @@ export declare class BridgeAgent {
2000
2309
  * startup: `on` = default grants enabled
2001
2310
  * meta_fresh / roster_fresh whether the channel meta / roster caches were
2002
2311
  * fresh (either stale forces audience=unknown)
2312
+ * room_class the room's class from the server roster (`na`
2313
+ * for a DM): internal | cross_org | unknown
2314
+ * epoch_fresh the roster was fetched at the agent's current
2315
+ * MLS epoch for the channel
2316
+ * room_kill_switch SECURECOMS_ROOM_DEFAULT_GRANTS as read at startup
2317
+ * per_tool_consumer a consumer that declared per-tool grant
2318
+ * enforcement is attached
2319
+ * memory_write the turn carries memory_write (Markdown memory
2320
+ * 7a, spec §8.3): a boolean, never content
2003
2321
  */
2004
2322
  private logAuthzDecision;
2005
2323
  /**
@@ -2270,6 +2588,19 @@ export declare class BridgeAgent {
2270
2588
  isRuntimeMessageRetired(messageId: string): boolean;
2271
2589
  /** Complete the durable handoff only after the runtime explicitly acks. */
2272
2590
  ackRuntimeMessages(messageIds: string[]): void;
2591
+ /** The sealed runtime_inbox payload: the message plus its ingest stamp (never on the message object). */
2592
+ private stagedPayload;
2593
+ /**
2594
+ * Seal `message` into runtime_inbox with the current ingest stamp and record
2595
+ * the stamp that is actually persisted. Task 7 n-1: a same-id re-stage is
2596
+ * compared on the MESSAGE only, never the stamp, so a clear between two
2597
+ * stagings of one message is not a payload conflict (which would fail the
2598
+ * stage and leave the backend row redelivered forever). The row is not
2599
+ * rewritten, so the FIRST staging's stamp stays authoritative: the owner's
2600
+ * clear still covers a turn first ingested before it. A prior row with no
2601
+ * valid stamp gets none, so its write refuses `cleared`.
2602
+ */
2603
+ private stageSealed;
2273
2604
  private restoreRuntimeInbox;
2274
2605
  /**
2275
2606
  * Seal a runtime frame before the backend queue id may be acknowledged.
@@ -2326,6 +2657,18 @@ export declare class BridgeAgent {
2326
2657
  clearHarnessReport(report: HarnessReport): void;
2327
2658
  /** The latest harness report, or null (GET /v1/status). */
2328
2659
  harnessReport(): HarnessReport | null;
2660
+ /**
2661
+ * Audit C-B1: the attached /v1/ws consumer that declared per-tool grant
2662
+ * enforcement (its hello carried "per_tool_grants" and no harness report),
2663
+ * or null. authorizationFor issues a room grant only while one is attached
2664
+ * (Task R2); every delivery path also strips room grants for any other
2665
+ * consumer (consumer-grants.ts). The token is the socket's session object.
2666
+ */
2667
+ private perToolConsumer;
2668
+ notePerToolConsumer(token: object): void;
2669
+ /** Forget `token` when its socket closes; a newer consumer's token is kept. */
2670
+ clearPerToolConsumer(token: object): void;
2671
+ perToolConsumerAttached(): boolean;
2329
2672
  health(): {
2330
2673
  status: "ok";
2331
2674
  identity_id: string;
@@ -0,0 +1,23 @@
1
+ import type { BridgeMessage } from "./bridge-agent.js";
2
+ /**
3
+ * The /v1/ws hello feature a consumer sends to declare per-tool grant
4
+ * enforcement. The runtime exports the same value (connect-agent-runtime
5
+ * data-plane.ts PER_TOOL_GRANTS_FEATURE); the two packages share no
6
+ * dependency, so ws-server.test.ts pins them equal and drives the bridge with
7
+ * the runtime's own hello frame.
8
+ */
9
+ export declare const PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
10
+ /**
11
+ * `m` unchanged for a per-tool consumer; otherwise `m` with any room grant
12
+ * stripped to []. The owner's own turns, which a harness has always been
13
+ * granted for, pass unchanged: the owner's DM, and the owner-spoken relayed
14
+ * voice turn the daemon marks relay_speaker "owner" (audit r2 R2-T1; the SAME
15
+ * rule the connectors apply, Task R2a). Everything else with grants is
16
+ * stripped: an allowlist fails closed on any new granted audience.
17
+ *
18
+ * Never mutates `m`: the buffer and in-flight records keep the original, so a
19
+ * later per-tool consumer still receives it. The stripped copy keeps every
20
+ * other field (audience, relay_speaker) as is, except `memory_write`, which
21
+ * no non-per-tool consumer ever receives (Markdown memory 7a, review m-3).
22
+ */
23
+ export declare function withConsumerGrants(m: BridgeMessage, perToolConsumer: boolean): BridgeMessage;
@@ -16,6 +16,16 @@ export declare function writeSse(res: {
16
16
  writableEnded?: boolean;
17
17
  write: (s: string) => boolean;
18
18
  }, msgs: BridgeMessage[], onWriteError?: (error: unknown) => void): BridgeMessage[];
19
+ /**
20
+ * Audit C-B1 / r2 R2-T2: deliver one SSE batch. An SSE consumer never declares
21
+ * per-tool enforcement, so the WRITTEN copies have room grants stripped; the
22
+ * unwritten tail is requeued as the ORIGINAL messages, never a stripped copy,
23
+ * so a later per-tool consumer still receives the grant.
24
+ */
25
+ export declare function deliverSseBatch(res: Parameters<typeof writeSse>[0], batch: BridgeMessage[], agent: {
26
+ ackRuntimeMessages(ids: string[]): void;
27
+ requeueFront(msgs: BridgeMessage[]): void;
28
+ }, onWriteError?: (error: unknown) => void): void;
19
29
  /**
20
30
  * Once-per-connection write-failure reporter (#335). A throwing `res.write`
21
31
  * means the socket is dying — teardown follows via the request's `'close'` —