@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.
- package/README.md +91 -0
- package/connectors/hermes/securecoms_authz.py +16 -4
- package/connectors/hermes/tests/test_securecoms_authz.py +31 -6
- package/connectors/openclaw/openclaw-entry.js +14 -6
- package/connectors/openclaw/openclaw-entry.toolgate.test.ts +20 -9
- package/dist/bin.d.ts +23 -0
- package/dist/bin.js +1433 -89
- package/dist/bridge-agent.d.ts +343 -0
- package/dist/consumer-grants.d.ts +23 -0
- package/dist/http-server.d.ts +10 -0
- package/dist/index.js +1419 -84
- package/dist/memory/consumer-notes.d.ts +34 -0
- package/dist/memory/memory-config.d.ts +10 -0
- package/dist/memory/note-frames.d.ts +44 -0
- package/dist/memory/note-provenance.d.ts +40 -0
- package/dist/memory/notes-book.d.ts +169 -0
- package/dist/memory/secret-scan.d.ts +5 -0
- package/dist/room-tools.d.ts +23 -0
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-prompt.d.ts +5 -0
- package/dist/ws-server.d.ts +15 -0
- package/package.json +1 -1
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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'` —
|