@securecoms/agent-bridge 0.26.5 → 0.26.6

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.
@@ -65,6 +65,9 @@ export type BridgeToolGrant = {
65
65
  * note. Every
66
66
  * other audience, an unpinned or anomalous owner, and the kill switch
67
67
  * `SECURECOMS_OWNER_DM_DEFAULT_GRANTS` (anything but unset/`on`) get `[]`.
68
+ * PR R adds exactly one more non-empty list, ROOM_DEFAULT_READ_GRANTS
69
+ * (`current_time` only), for an INTERNAL room's same-org human sender; see
70
+ * authorizationFor.
68
71
  * SEC-06 is LIVE for these two tools: docs/security/sec-06-runtime-bounds-audit.md.
69
72
  */
70
73
  /**
@@ -75,17 +78,42 @@ export type BridgeToolGrant = {
75
78
  * and `http_request` are deliberately absent.
76
79
  */
77
80
  export declare const OWNER_DM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
81
+ /**
82
+ * PR R (spec §7A.1a; controller ruling 2026-10-05, audit r1: current_time
83
+ * only). Granted only in a room classified INTERNAL from the server roster
84
+ * (every row, leaf rows included, in the agent owner's org), to a same-org
85
+ * human sender, while the roster is as fresh as the agent's MLS epoch, while
86
+ * a declared per-tool consumer is attached (audit C-B1), and while
87
+ * SECURECOMS_ROOM_DEFAULT_GRANTS is on (plan OQ-3). Frozen; the runtime intersects
88
+ * it with ROOM_SAFE_READ_TOOLS (turn-loop.ts).
89
+ */
90
+ export declare const ROOM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
78
91
  /**
79
92
  * Kill switch for OWNER_DM_DEFAULT_READ_GRANTS, read once at BridgeAgent
80
93
  * construction (daemon startup). Unset or exactly `on` keeps the grants;
81
94
  * any other value (`off`, `0`, `false`, empty) sends `[]` on every turn.
82
95
  */
83
96
  export declare function ownerDmDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
97
+ /**
98
+ * Kill switch for ROOM_DEFAULT_READ_GRANTS (plan OQ-3, RULED), read once at
99
+ * BridgeAgent construction exactly like ownerDmDefaultGrantsEnabled. Unset or
100
+ * exactly `on` keeps the room default; any other value sends `[]` on every
101
+ * room turn. It never touches the owner DM.
102
+ */
103
+ export declare function roomDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
84
104
  export interface BridgeAuthorization {
85
105
  readonly version: 1;
86
106
  readonly audience: AgentAudience;
87
107
  readonly sender_membership: "active" | "unknown";
88
108
  readonly tool_grants: readonly BridgeToolGrant[];
109
+ /**
110
+ * Audit r2 R2-T1 (plan OQ-6 ruling): "owner" ONLY on authorizationFor's
111
+ * ownerRelayed branch: the channel's server-marked relay sent a turn whose
112
+ * relayed speaker is the channel's pinned owner. Set by the daemon, never
113
+ * copied from message text. A harness connector honours a grant on
114
+ * audience owner_dm or on this mark, nothing else.
115
+ */
116
+ readonly relay_speaker?: "owner";
89
117
  }
90
118
  export interface BridgeMessage {
91
119
  channel_id: string;
@@ -517,6 +545,15 @@ export declare class SendForbiddenError extends Error {
517
545
  readonly channelId: string;
518
546
  constructor(channelId: string);
519
547
  }
548
+ /**
549
+ * Audit M-B1 (controller ruling 2026-10-05): a reply to a room-granted turn
550
+ * would be sealed at an epoch the room was not classified at, and the room is
551
+ * not internal there. Dropped. A SendForbiddenError, so both transports treat
552
+ * it as terminal (send_forbidden: the runtime acks, nothing redelivers).
553
+ */
554
+ export declare class RoomClassChangedSendError extends SendForbiddenError {
555
+ constructor(channelId: string);
556
+ }
520
557
  /**
521
558
  * Stranded sender (spec 2026-09-26 §7.4): the backend refused a publish with
522
559
  * 409 `mls.group_superseded`: the envelope was sealed under a group that is no
@@ -896,6 +933,21 @@ export declare class BridgeAgent {
896
933
  private readonly selfHealSweepEnabled;
897
934
  /** Read once at startup; see ownerDmDefaultGrantsEnabled. */
898
935
  private readonly ownerDmDefaultGrants;
936
+ /** Read once at startup; see roomDefaultGrantsEnabled. */
937
+ private readonly roomDefaultGrants;
938
+ /**
939
+ * Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
940
+ * cycle's refresh (refreshRostersForCycle). Cleared at each cycle's start.
941
+ */
942
+ private readonly ownerDmCycleFresh;
943
+ /**
944
+ * Audit M-B1: room-granted turns, keyed by the inbound message id, with the
945
+ * channel, MLS group and epoch the room was classified at (review I-1: an
946
+ * epoch NUMBER alone repeats across channels and across a supersede). A
947
+ * reply to one is sealed only there, or re-classified once
948
+ * (publishBodyForReply). Forgotten at the runtime's ack.
949
+ */
950
+ private readonly roomGrantEpochs;
899
951
  /** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
900
952
  private static readonly SELF_HEAL_SWEEP_INTERVAL_MS;
901
953
  /**
@@ -1509,6 +1561,25 @@ export declare class BridgeAgent {
1509
1561
  * there so a retry can resend the exact bytes).
1510
1562
  */
1511
1563
  private publishBody;
1564
+ /**
1565
+ * Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
1566
+ * only at the epoch the room was classified at. On a move, the room is
1567
+ * re-classified ONCE from a roster fetched at the new epoch: still internal,
1568
+ * the reply is sealed at that epoch; anything else, it is dropped. The check
1569
+ * throws before an envelope is built, so nothing is stored or posted.
1570
+ *
1571
+ * `inReplyTo` is the STORED intent's inbound id, not the send's options, so
1572
+ * the redelivery replay (replayReplyIntents, which has no in_reply_to in its
1573
+ * options) is checked the same as a live send.
1574
+ */
1575
+ private publishBodyForReply;
1576
+ /**
1577
+ * Re-read the room at the agent's current group and epoch; true only if it
1578
+ * is still internal there. Review I-1: the entry must come from THIS re-read
1579
+ * (a fail-soft refresh leaves an older entry whose epoch label can repeat
1580
+ * across a supersede), and the group must not move around the reads.
1581
+ */
1582
+ private reclassifyForReply;
1512
1583
  private enforceBodyRateFloor;
1513
1584
  /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
1514
1585
  private afterPublish;
@@ -1947,6 +2018,19 @@ export declare class BridgeAgent {
1947
2018
  private enclavesToCover;
1948
2019
  /** A live roster for a channel, or undefined if missing OR expired past the TTL. */
1949
2020
  private freshRoster;
2021
+ /** The whole cache entry when fresh, else undefined (TTL; the epoch is checked by the caller). */
2022
+ private freshRosterEntry;
2023
+ /** The agent's held MLS group id for a channel; null when none (fail closed). */
2024
+ private heldGroupFor;
2025
+ /**
2026
+ * Review I-1 + fix round 2: the agent's held (group, epoch) for a channel.
2027
+ * The group is read on BOTH sides of the epoch read, so the epoch provably
2028
+ * belongs to it (a supersede in between installs a new group whose epoch
2029
+ * counter restarts); a group that moved, or none held, is null (fail closed).
2030
+ */
2031
+ private heldPosition;
2032
+ /** The agent's own MLS epoch for a channel; null when no group is held (fail closed). */
2033
+ private heldEpochFor;
1950
2034
  /**
1951
2035
  * Resolve one channel's roster (members + display names + is-agent) from the
1952
2036
  * GET members endpoint into the roster cache. Called AT MOST ONCE per ingest
@@ -1955,6 +2039,15 @@ export declare class BridgeAgent {
1955
2039
  * attribution must NEVER block or drop a message.
1956
2040
  */
1957
2041
  private refreshChannelRoster;
2042
+ /**
2043
+ * S-1 (roster) + §7A.5 + audit M-B4: once per ingest cycle, per distinct
2044
+ * channel, re-read the roster when the TTL lapsed OR the epoch moved since
2045
+ * the fetch, then record which DMs are epoch-fresh for the rest of this
2046
+ * cycle. Fail-soft inside refreshChannelRoster (never blocks ingest).
2047
+ */
2048
+ private refreshRostersForCycle;
2049
+ /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
2050
+ private authorizeTurn;
1958
2051
  /**
1959
2052
  * Classify the sender only from server-authenticated channel/roster data.
1960
2053
  * Missing or stale metadata is UNKNOWN with zero grants, never a permissive
@@ -1972,6 +2065,13 @@ export declare class BridgeAgent {
1972
2065
  * relayed `speaker_identity_id` (passed by the caller only for relay
1973
2066
  * turns) is that same pinned owner, whose roster row is the agent's
1974
2067
  * owner. A speaker claim from any non-relay sender gets nothing.
2068
+ * Both need the roster fetched at `heldEpoch` (the agent's current MLS
2069
+ * epoch for the channel), or the DM marked epoch-fresh at this ingest
2070
+ * cycle's refresh (audit M-B4).
2071
+ * PR R: ROOM_DEFAULT_READ_GRANTS only when the room kill switch is on, a
2072
+ * per-tool consumer is attached, the roster was fetched at `heldEpoch`
2073
+ * (no cycle carve-out), the room is INTERNAL (roomIsCrossOrg === false
2074
+ * over every row, leaf rows included) and the audience is team_room.
1975
2075
  * Everything else gets the frozen empty list.
1976
2076
  *
1977
2077
  * LIMIT (harness gate review H2, 2026-10-05): "pinned structural owner"
@@ -2000,6 +2100,13 @@ export declare class BridgeAgent {
2000
2100
  * startup: `on` = default grants enabled
2001
2101
  * meta_fresh / roster_fresh whether the channel meta / roster caches were
2002
2102
  * fresh (either stale forces audience=unknown)
2103
+ * room_class the room's class from the server roster (`na`
2104
+ * for a DM): internal | cross_org | unknown
2105
+ * epoch_fresh the roster was fetched at the agent's current
2106
+ * MLS epoch for the channel
2107
+ * room_kill_switch SECURECOMS_ROOM_DEFAULT_GRANTS as read at startup
2108
+ * per_tool_consumer a consumer that declared per-tool grant
2109
+ * enforcement is attached
2003
2110
  */
2004
2111
  private logAuthzDecision;
2005
2112
  /**
@@ -2326,6 +2433,18 @@ export declare class BridgeAgent {
2326
2433
  clearHarnessReport(report: HarnessReport): void;
2327
2434
  /** The latest harness report, or null (GET /v1/status). */
2328
2435
  harnessReport(): HarnessReport | null;
2436
+ /**
2437
+ * Audit C-B1: the attached /v1/ws consumer that declared per-tool grant
2438
+ * enforcement (its hello carried "per_tool_grants" and no harness report),
2439
+ * or null. authorizationFor issues a room grant only while one is attached
2440
+ * (Task R2); every delivery path also strips room grants for any other
2441
+ * consumer (consumer-grants.ts). The token is the socket's session object.
2442
+ */
2443
+ private perToolConsumer;
2444
+ notePerToolConsumer(token: object): void;
2445
+ /** Forget `token` when its socket closes; a newer consumer's token is kept. */
2446
+ clearPerToolConsumer(token: object): void;
2447
+ perToolConsumerAttached(): boolean;
2329
2448
  health(): {
2330
2449
  status: "ok";
2331
2450
  identity_id: string;
@@ -0,0 +1,22 @@
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.
21
+ */
22
+ 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'` —