@graphorin/server 0.6.1 → 0.7.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.
Files changed (181) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +3 -3
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +35 -607
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +8 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +19 -24
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +1 -1
  59. package/dist/package.js.map +1 -1
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.js +19 -12
  99. package/dist/ws/dispatcher.js.map +1 -1
  100. package/dist/ws/index.d.ts +2 -2
  101. package/dist/ws/index.js +3 -3
  102. package/dist/ws/replay-buffer.d.ts +35 -1
  103. package/dist/ws/replay-buffer.d.ts.map +1 -1
  104. package/dist/ws/replay-buffer.js +35 -3
  105. package/dist/ws/replay-buffer.js.map +1 -1
  106. package/dist/ws/upgrade.d.ts.map +1 -1
  107. package/dist/ws/upgrade.js +24 -18
  108. package/dist/ws/upgrade.js.map +1 -1
  109. package/package.json +30 -29
  110. package/src/app-audit-binding.ts +227 -0
  111. package/src/app-daemons.ts +73 -0
  112. package/src/app-lifecycle.ts +476 -0
  113. package/src/app-metrics.ts +144 -0
  114. package/src/app-middleware.ts +149 -0
  115. package/src/app-routes.ts +305 -0
  116. package/src/app-ws.ts +111 -0
  117. package/src/app.ts +317 -0
  118. package/src/commentary/audit-bridge.ts +135 -0
  119. package/src/commentary/built-in-patterns.ts +79 -0
  120. package/src/commentary/index.ts +32 -0
  121. package/src/commentary/sanitizer.ts +238 -0
  122. package/src/commentary/types.ts +130 -0
  123. package/src/config.ts +472 -0
  124. package/src/consolidator/daemon.ts +141 -0
  125. package/src/consolidator/index.ts +14 -0
  126. package/src/errors/index.ts +247 -0
  127. package/src/health/checks.ts +322 -0
  128. package/src/health/index.ts +34 -0
  129. package/src/health/routes.ts +154 -0
  130. package/src/index.ts +243 -0
  131. package/src/internal/context.ts +77 -0
  132. package/src/internal/ids.ts +17 -0
  133. package/src/internal/json.ts +47 -0
  134. package/src/internal/wire-error.ts +44 -0
  135. package/src/lifecycle/hooks.ts +66 -0
  136. package/src/lifecycle/index.ts +16 -0
  137. package/src/lifecycle/pre-bind.ts +138 -0
  138. package/src/metrics/catalog.ts +112 -0
  139. package/src/metrics/index.ts +12 -0
  140. package/src/metrics/registry.ts +337 -0
  141. package/src/metrics/tools-bridge.ts +124 -0
  142. package/src/middleware/audit.ts +114 -0
  143. package/src/middleware/auth.ts +195 -0
  144. package/src/middleware/cors.ts +72 -0
  145. package/src/middleware/csrf.ts +98 -0
  146. package/src/middleware/idempotency.ts +336 -0
  147. package/src/middleware/index.ts +38 -0
  148. package/src/middleware/rate-limit.ts +71 -0
  149. package/src/middleware/request-state.ts +79 -0
  150. package/src/middleware/scope.ts +161 -0
  151. package/src/registry/index.ts +278 -0
  152. package/src/replay/index.ts +14 -0
  153. package/src/replay/routes.ts +255 -0
  154. package/src/routes/agents.ts +363 -0
  155. package/src/routes/audit.ts +207 -0
  156. package/src/routes/auth.ts +80 -0
  157. package/src/routes/health.ts +42 -0
  158. package/src/routes/index.ts +16 -0
  159. package/src/routes/mcp.ts +97 -0
  160. package/src/routes/memory.ts +152 -0
  161. package/src/routes/sessions.ts +250 -0
  162. package/src/routes/skills.ts +91 -0
  163. package/src/routes/tokens.ts +166 -0
  164. package/src/routes/workflows.ts +616 -0
  165. package/src/runtime/retention.ts +284 -0
  166. package/src/runtime/run-state.ts +422 -0
  167. package/src/sse/events.ts +303 -0
  168. package/src/sse/index.ts +7 -0
  169. package/src/tools-audit-bridge.ts +120 -0
  170. package/src/triggers/daemon.ts +198 -0
  171. package/src/triggers/index.ts +16 -0
  172. package/src/triggers/routes.ts +139 -0
  173. package/src/workflows/timer-daemon.ts +102 -0
  174. package/src/ws/dispatcher.ts +537 -0
  175. package/src/ws/index.ts +45 -0
  176. package/src/ws/replay-buffer.ts +209 -0
  177. package/src/ws/subjects.ts +162 -0
  178. package/src/ws/ticket.ts +174 -0
  179. package/src/ws/upgrade.ts +507 -0
  180. package/dist/lifecycle/index.js +0 -3
  181. package/dist/routes/index.js +0 -12
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Stateless delivery-layer commentary-phase trace sanitizer. Walks
3
+ * outbound `ServerMessage` event frames and applies the configured
4
+ * pattern catalogue to the JSON-stringified payload; matched
5
+ * fragments are wrapped in the configured envelope (default) or
6
+ * stripped (per the `'strip'` policy) or passed through (per the
7
+ * `'pass-through'` policy - operator opt-out for trusted
8
+ * deployments).
9
+ *
10
+ * The sanitizer runs AFTER the `ServerMessage` Zod-validation pass
11
+ * in `@graphorin/protocol` and BEFORE the message hits the wire (so
12
+ * the protocol schema is unchanged; the sanitization is a pure
13
+ * transform applied to validated payloads). It is bytes-equal across
14
+ * the WS / SSE / REST transports - the same input + same policy
15
+ * produces the same output regardless of which transport invoked
16
+ * the sanitizer.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+
21
+ import { createHash } from 'node:crypto';
22
+ import type { ServerEventFrame } from '@graphorin/protocol';
23
+
24
+ import {
25
+ DEFAULT_APPLY_TO_EVENTS,
26
+ DEFAULT_DELIVERY_COMMENTARY_PATTERNS,
27
+ } from './built-in-patterns.js';
28
+ import type {
29
+ DeliveryCommentaryConfig,
30
+ DeliveryCommentaryDecision,
31
+ DeliveryCommentaryPattern,
32
+ DeliveryCommentaryPolicy,
33
+ DeliveryCommentaryReason,
34
+ DeliveryCommentarySink,
35
+ DeliveryCommentaryTransport,
36
+ } from './types.js';
37
+
38
+ const DEFAULT_OPEN = '<<<commentary>>>';
39
+ const DEFAULT_CLOSE = '<<</commentary>>>';
40
+
41
+ /**
42
+ * Public surface returned by {@link createDeliveryCommentarySanitizer}.
43
+ *
44
+ * @stable
45
+ */
46
+ export interface DeliveryCommentarySanitizer {
47
+ readonly policy: DeliveryCommentaryPolicy;
48
+ readonly applyToEvents: ReadonlyArray<string>;
49
+ readonly patterns: ReadonlyArray<DeliveryCommentaryPattern>;
50
+ /**
51
+ * Sanitize the payload of a single `event` frame. Returns the
52
+ * (possibly-replaced) frame; emits an audit decision via the
53
+ * configured sink when the sanitizer mutated the payload.
54
+ *
55
+ * The frame is returned unchanged when:
56
+ * - the policy is `'pass-through'`,
57
+ * - the event type is not in `applyToEvents`,
58
+ * - or no pattern matched the JSON-stringified payload.
59
+ */
60
+ sanitize(frame: ServerEventFrame, transport: DeliveryCommentaryTransport): ServerEventFrame;
61
+ }
62
+
63
+ /**
64
+ * Build a stateless delivery-layer sanitizer. Tests can swap the
65
+ * `sink` for an in-memory recorder; production wires the
66
+ * `@graphorin/security/audit` `appendAudit` helper.
67
+ *
68
+ * @stable
69
+ */
70
+ export function createDeliveryCommentarySanitizer(
71
+ config: DeliveryCommentaryConfig = {},
72
+ ): DeliveryCommentarySanitizer {
73
+ const policy: DeliveryCommentaryPolicy = config.policy ?? 'wrap';
74
+ const applyToEvents: ReadonlyArray<string> = config.applyToEvents ?? DEFAULT_APPLY_TO_EVENTS;
75
+ const applyToSet = new Set(applyToEvents);
76
+ const patterns: ReadonlyArray<DeliveryCommentaryPattern> =
77
+ config.patterns ?? DEFAULT_DELIVERY_COMMENTARY_PATTERNS;
78
+ const open = config.wrapOpen ?? DEFAULT_OPEN;
79
+ const close = config.wrapClose ?? DEFAULT_CLOSE;
80
+ const sink: DeliveryCommentarySink | undefined = config.sink;
81
+
82
+ function sanitize(
83
+ frame: ServerEventFrame,
84
+ transport: DeliveryCommentaryTransport,
85
+ ): ServerEventFrame {
86
+ if (policy === 'pass-through') return frame;
87
+ if (!applyToSet.has(frame.type)) return frame;
88
+ const before = JSON.stringify(frame.payload ?? null);
89
+ const allReasons = new Set<DeliveryCommentaryReason>();
90
+ let mutated = false;
91
+ const next = walkAndSanitize(frame.payload, (text) => {
92
+ const out = applyToText(text, patterns, policy, open, close);
93
+ for (const reason of out.reasons) allReasons.add(reason);
94
+ if (out.transformed !== text) mutated = true;
95
+ return out.transformed;
96
+ });
97
+ if (!mutated) return frame;
98
+ const after = JSON.stringify(next ?? null);
99
+ const decision: DeliveryCommentaryDecision = {
100
+ transport,
101
+ boundary: 'event-emission',
102
+ policy,
103
+ applied: true,
104
+ reasons: [...allReasons],
105
+ matchedPattern: [...allReasons][0],
106
+ sha256OfBefore: sha256(before),
107
+ sha256OfAfter: sha256(after),
108
+ eventType: frame.type,
109
+ };
110
+ try {
111
+ sink?.onDecision(decision);
112
+ } catch {
113
+ // Sink errors must never block the wire - swallow + continue.
114
+ }
115
+ return Object.freeze({ ...frame, payload: next });
116
+ }
117
+
118
+ return Object.freeze({
119
+ policy,
120
+ applyToEvents,
121
+ patterns,
122
+ sanitize,
123
+ });
124
+ }
125
+
126
+ interface ApplyResult {
127
+ readonly transformed: string;
128
+ readonly reasons: ReadonlyArray<DeliveryCommentaryReason>;
129
+ }
130
+
131
+ /**
132
+ * Recursively walks any JSON-shaped value (`null`, primitive, array,
133
+ * object) and applies the supplied transform to every string leaf.
134
+ * Returns a structurally-cloned copy when at least one leaf was
135
+ * rewritten; returns the original input by reference otherwise.
136
+ */
137
+ function walkAndSanitize(value: unknown, transform: (s: string) => string): unknown {
138
+ if (typeof value === 'string') {
139
+ const next = transform(value);
140
+ return next === value ? value : next;
141
+ }
142
+ if (Array.isArray(value)) {
143
+ let mutated = false;
144
+ const next: unknown[] = new Array(value.length);
145
+ for (let i = 0; i < value.length; i += 1) {
146
+ const child = walkAndSanitize(value[i], transform);
147
+ if (child !== value[i]) mutated = true;
148
+ next[i] = child;
149
+ }
150
+ return mutated ? next : value;
151
+ }
152
+ if (value !== null && typeof value === 'object') {
153
+ let mutated = false;
154
+ const next: Record<string, unknown> = {};
155
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
156
+ const child = walkAndSanitize(val, transform);
157
+ if (child !== val) mutated = true;
158
+ next[key] = child;
159
+ }
160
+ return mutated ? next : value;
161
+ }
162
+ return value;
163
+ }
164
+
165
+ function applyToText(
166
+ text: string,
167
+ patterns: ReadonlyArray<DeliveryCommentaryPattern>,
168
+ policy: DeliveryCommentaryPolicy,
169
+ open: string,
170
+ close: string,
171
+ ): ApplyResult {
172
+ // Split the body into already-wrapped + plain segments so we never
173
+ // re-scan inside an existing wrap envelope. This makes the sanitizer
174
+ // idempotent on the same payload (a second pass produces bytes-equal
175
+ // output, satisfying the cross-cut composition contract with
176
+ // Phase 11 session-output sanitization + Phase 11 replay
177
+ // sanitization).
178
+ const segments = splitByWrapEnvelope(text, open, close);
179
+ const matchedReasons = new Set<DeliveryCommentaryReason>();
180
+ const next: string[] = [];
181
+ for (const segment of segments) {
182
+ if (segment.kind === 'wrapped') {
183
+ next.push(segment.text);
184
+ continue;
185
+ }
186
+ let segText = segment.text;
187
+ for (const pattern of patterns) {
188
+ const regex = freshRegex(pattern.regex);
189
+ let didMatch = false;
190
+ segText = segText.replace(regex, (match) => {
191
+ didMatch = true;
192
+ if (policy === 'strip') return '';
193
+ return `${open}${match}${close}`;
194
+ });
195
+ if (didMatch) matchedReasons.add(pattern.reason);
196
+ }
197
+ next.push(segText);
198
+ }
199
+ return {
200
+ transformed: next.join(''),
201
+ reasons: [...matchedReasons],
202
+ };
203
+ }
204
+
205
+ function freshRegex(re: RegExp): RegExp {
206
+ return new RegExp(re.source, re.flags);
207
+ }
208
+
209
+ function splitByWrapEnvelope(
210
+ text: string,
211
+ open: string,
212
+ close: string,
213
+ ): ReadonlyArray<{ readonly kind: 'plain' | 'wrapped'; readonly text: string }> {
214
+ const out: { kind: 'plain' | 'wrapped'; text: string }[] = [];
215
+ let cursor = 0;
216
+ while (cursor < text.length) {
217
+ const openAt = text.indexOf(open, cursor);
218
+ if (openAt < 0) {
219
+ out.push({ kind: 'plain', text: text.slice(cursor) });
220
+ break;
221
+ }
222
+ if (openAt > cursor) {
223
+ out.push({ kind: 'plain', text: text.slice(cursor, openAt) });
224
+ }
225
+ const closeAt = text.indexOf(close, openAt + open.length);
226
+ if (closeAt < 0) {
227
+ out.push({ kind: 'plain', text: text.slice(openAt) });
228
+ break;
229
+ }
230
+ out.push({ kind: 'wrapped', text: text.slice(openAt, closeAt + close.length) });
231
+ cursor = closeAt + close.length;
232
+ }
233
+ return out;
234
+ }
235
+
236
+ function sha256(value: string): string {
237
+ return createHash('sha256').update(value, 'utf8').digest('hex');
238
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Types for the delivery-layer commentary-phase trace sanitization
3
+ * applied by the WebSocket dispatcher (`@graphorin/server/ws`) and
4
+ * the SSE event-emission boundary (`@graphorin/server/sse`).
5
+ *
6
+ * The catalogue + per-policy enum + audit row shape is intentionally
7
+ * structural so the server's delivery layer stays self-contained. A
8
+ * deployment that wants to share patterns with the session-output
9
+ * boundary in `@graphorin/sessions` can pass that package's
10
+ * `BUILT_IN_COMMENTARY_PATTERNS` through {@link DeliveryCommentaryConfig}.patterns;
11
+ * the shape is bytes-equal across the two layers (the
12
+ * defense-in-depth posture is the load-bearing property - see
13
+ * Phase 11 sanitizer for the storage-write boundary, this module
14
+ * for the wire-emission boundary).
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ /**
20
+ * Operator-facing policy. Identical semantics to the session-output
21
+ * sanitizer in `@graphorin/sessions/commentary` so the two layers
22
+ * are bytes-equal on idempotent re-application.
23
+ *
24
+ * - `'wrap'` (default) - wraps the matched fragment in a
25
+ * `<<<commentary>>>...<<</commentary>>>` envelope so downstream
26
+ * consumers can choose to render or hide based on context.
27
+ * - `'strip'` - removes the matched fragment entirely.
28
+ * - `'pass-through'` - disables the sanitization (operator opt-in
29
+ * for trusted deployments).
30
+ *
31
+ * @stable
32
+ */
33
+ export type DeliveryCommentaryPolicy = 'wrap' | 'strip' | 'pass-through';
34
+
35
+ /**
36
+ * Discriminator surfaced on audit rows + counter labels for the
37
+ * transport that produced the sanitization decision. The cardinality
38
+ * is bounded (3-valued).
39
+ *
40
+ * @stable
41
+ */
42
+ export type DeliveryCommentaryTransport = 'ws' | 'sse' | 'rest';
43
+
44
+ /**
45
+ * Stable label for each detection pattern. Surfaced in the audit row
46
+ * + the counter label cardinality is bounded.
47
+ *
48
+ * @stable
49
+ */
50
+ export type DeliveryCommentaryReason =
51
+ | 'tool.call.start-payload-signature'
52
+ | 'tool.call.delta-payload-signature'
53
+ | 'tool.call.end-payload-signature'
54
+ | 'tool.execute.end-payload-signature'
55
+ | 'agent.fanout-event-signature'
56
+ | 'context.compacted-event-signature'
57
+ | 'agent.model.fellback-event-signature';
58
+
59
+ /**
60
+ * Single pattern entry in the {@link DEFAULT_DELIVERY_COMMENTARY_PATTERNS}
61
+ * catalogue. The `regex` is matched against the JSON payload of the
62
+ * `event` frame (after `JSON.stringify(payload)`); deployments that
63
+ * want to match against the wire-format string instead can supply
64
+ * their own catalogue.
65
+ *
66
+ * @stable
67
+ */
68
+ export interface DeliveryCommentaryPattern {
69
+ readonly reason: DeliveryCommentaryReason;
70
+ readonly regex: RegExp;
71
+ readonly description: string;
72
+ }
73
+
74
+ /**
75
+ * Per-emission decision recorded by the sanitizer. Mirrored on the
76
+ * audit row + the counter increment.
77
+ *
78
+ * @stable
79
+ */
80
+ export interface DeliveryCommentaryDecision {
81
+ readonly transport: DeliveryCommentaryTransport;
82
+ readonly boundary: 'event-emission';
83
+ readonly policy: DeliveryCommentaryPolicy;
84
+ readonly applied: boolean;
85
+ readonly reasons: ReadonlyArray<DeliveryCommentaryReason>;
86
+ readonly matchedPattern: string | undefined;
87
+ readonly sha256OfBefore: string;
88
+ readonly sha256OfAfter: string;
89
+ readonly eventType: string;
90
+ }
91
+
92
+ /**
93
+ * Single audit + counter sink consumed by the sanitizer when a
94
+ * decision fires. Wiring is optional - operators that do not need
95
+ * audit telemetry can skip the sink and the sanitizer becomes a
96
+ * pure transform.
97
+ *
98
+ * @stable
99
+ */
100
+ export interface DeliveryCommentarySink {
101
+ /**
102
+ * Called once per applied decision. Implementations should be
103
+ * non-throwing; the sanitizer wraps the call in `try/catch` so a
104
+ * misbehaving sink never blocks the wire.
105
+ */
106
+ onDecision(decision: DeliveryCommentaryDecision): void;
107
+ }
108
+
109
+ /**
110
+ * Public configuration accepted by the WS / SSE / REST event-
111
+ * emission sanitizer. Shape mirrors the per-server
112
+ * `WsConfig.commentarySanitization` field documented in the runtime
113
+ * spec.
114
+ *
115
+ * @stable
116
+ */
117
+ export interface DeliveryCommentaryConfig {
118
+ readonly policy?: DeliveryCommentaryPolicy;
119
+ /**
120
+ * Whitelist of `event.type` literals to sanitize. The default
121
+ * covers the user-visible commentary surface (`text.delta` plus
122
+ * the two tool-result variants); operators extend the list as
123
+ * their UI rendering boundary expands.
124
+ */
125
+ readonly applyToEvents?: ReadonlyArray<string>;
126
+ readonly patterns?: ReadonlyArray<DeliveryCommentaryPattern>;
127
+ readonly wrapOpen?: string;
128
+ readonly wrapClose?: string;
129
+ readonly sink?: DeliveryCommentarySink;
130
+ }