@bookedsolid/rea 0.2.1 → 0.4.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 (65) hide show
  1. package/.husky/pre-push +15 -18
  2. package/README.md +41 -1
  3. package/THREAT_MODEL.md +100 -29
  4. package/dist/audit/append.d.ts +21 -8
  5. package/dist/audit/append.js +48 -83
  6. package/dist/audit/fs.d.ts +68 -0
  7. package/dist/audit/fs.js +171 -0
  8. package/dist/cli/audit.d.ts +40 -0
  9. package/dist/cli/audit.js +205 -0
  10. package/dist/cli/doctor.d.ts +19 -4
  11. package/dist/cli/doctor.js +172 -5
  12. package/dist/cli/index.js +26 -1
  13. package/dist/cli/init.js +93 -7
  14. package/dist/cli/install/pre-push.d.ts +335 -0
  15. package/dist/cli/install/pre-push.js +2818 -0
  16. package/dist/cli/serve.d.ts +64 -0
  17. package/dist/cli/serve.js +270 -2
  18. package/dist/cli/status.d.ts +90 -0
  19. package/dist/cli/status.js +399 -0
  20. package/dist/cli/utils.d.ts +4 -0
  21. package/dist/cli/utils.js +4 -0
  22. package/dist/gateway/audit/rotator.d.ts +116 -0
  23. package/dist/gateway/audit/rotator.js +289 -0
  24. package/dist/gateway/circuit-breaker.d.ts +17 -0
  25. package/dist/gateway/circuit-breaker.js +32 -3
  26. package/dist/gateway/downstream-pool.d.ts +2 -1
  27. package/dist/gateway/downstream-pool.js +2 -2
  28. package/dist/gateway/downstream.d.ts +39 -3
  29. package/dist/gateway/downstream.js +73 -14
  30. package/dist/gateway/log.d.ts +122 -0
  31. package/dist/gateway/log.js +334 -0
  32. package/dist/gateway/middleware/audit.d.ts +24 -1
  33. package/dist/gateway/middleware/audit.js +103 -58
  34. package/dist/gateway/middleware/blocked-paths.d.ts +0 -9
  35. package/dist/gateway/middleware/blocked-paths.js +439 -67
  36. package/dist/gateway/middleware/injection.d.ts +218 -13
  37. package/dist/gateway/middleware/injection.js +433 -51
  38. package/dist/gateway/middleware/kill-switch.d.ts +10 -1
  39. package/dist/gateway/middleware/kill-switch.js +20 -1
  40. package/dist/gateway/observability/metrics.d.ts +125 -0
  41. package/dist/gateway/observability/metrics.js +321 -0
  42. package/dist/gateway/server.d.ts +19 -0
  43. package/dist/gateway/server.js +99 -15
  44. package/dist/policy/loader.d.ts +47 -0
  45. package/dist/policy/loader.js +47 -0
  46. package/dist/policy/profiles.d.ts +13 -0
  47. package/dist/policy/profiles.js +12 -0
  48. package/dist/policy/types.d.ts +52 -0
  49. package/dist/registry/fingerprint.d.ts +73 -0
  50. package/dist/registry/fingerprint.js +81 -0
  51. package/dist/registry/fingerprints-store.d.ts +62 -0
  52. package/dist/registry/fingerprints-store.js +111 -0
  53. package/dist/registry/interpolate.d.ts +58 -0
  54. package/dist/registry/interpolate.js +121 -0
  55. package/dist/registry/loader.d.ts +2 -2
  56. package/dist/registry/loader.js +22 -1
  57. package/dist/registry/tofu-gate.d.ts +41 -0
  58. package/dist/registry/tofu-gate.js +189 -0
  59. package/dist/registry/tofu.d.ts +111 -0
  60. package/dist/registry/tofu.js +173 -0
  61. package/dist/registry/types.d.ts +9 -1
  62. package/package.json +3 -1
  63. package/profiles/bst-internal-no-codex.yaml +5 -0
  64. package/profiles/bst-internal.yaml +7 -0
  65. package/scripts/tarball-smoke.sh +197 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Structured gateway logger (G5).
3
+ *
4
+ * Minimal JSON-lines logger for `rea serve` and its collaborators. The existing
5
+ * codebase had `console.error`/`console.warn` scattered across the gateway;
6
+ * that worked for humans tailing stderr but made logs impossible to parse for
7
+ * future tooling (shipping to syslog, aggregating in a dashboard, greping
8
+ * per-session).
9
+ *
10
+ * Design constraints (from the task spec):
11
+ *
12
+ * 1. NO dependency — pino would be overkill, and dep weight matters for an
13
+ * install-anywhere CLI. ~30 lines of hand-rolled code is enough.
14
+ * 2. Respect `REA_LOG_LEVEL` (info default; debug for future verbose hooks).
15
+ * 3. Pretty-print on a TTY stderr; emit JSON lines on non-TTY (CI, redirected
16
+ * stderr, process-supervised runs).
17
+ * 4. Preserve the `[rea-serve]` / `[rea]` prefix convention — the helix smoke
18
+ * test greps for these. Pretty-mode prints them explicitly; JSON mode
19
+ * carries them as structured fields.
20
+ * 5. Never throw. A logger that can crash the gateway is a bad trade.
21
+ *
22
+ * ## What this is NOT
23
+ *
24
+ * - Not an OpenTelemetry integration — see {@link ./observability/metrics.ts}
25
+ * for counters/gauges. Logs and metrics are separate concerns.
26
+ * - Not the audit log — `.rea/audit.jsonl` is a hash-chained, tamper-evident
27
+ * record of tool calls. This module is free-form operator observability.
28
+ * - Not a file logger. Records go to stderr only. If you need durable logs,
29
+ * redirect the process's stderr.
30
+ */
31
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
32
+ /** Structured fields every record carries. Additional fields are allowed. */
33
+ export interface LogFields {
34
+ /** Short verb-ish name — `downstream.connect`, `circuit.open`, etc. */
35
+ event: string;
36
+ /** Running gateway session id, when known. Omitted from startup records. */
37
+ session_id?: string;
38
+ /** Name of a downstream MCP server, when the record is server-scoped. */
39
+ server_name?: string;
40
+ /** Human-readable message. JSON mode carries it verbatim. */
41
+ message: string;
42
+ /** Any additional context. Values must be JSON-serializable. */
43
+ [key: string]: unknown;
44
+ }
45
+ export interface Logger {
46
+ debug(fields: LogFields): void;
47
+ info(fields: LogFields): void;
48
+ warn(fields: LogFields): void;
49
+ error(fields: LogFields): void;
50
+ /** Spawn a logger that merges `base` into every record. */
51
+ child(base: Partial<LogFields>): Logger;
52
+ }
53
+ /**
54
+ * Pre-emit field redactor. Called for every string-valued field in a
55
+ * record just before serialization. Returns the redacted string.
56
+ *
57
+ * SECURITY: downstream error messages may carry credential material
58
+ * (env var names, argv fragments, file paths). Without a hook the raw
59
+ * text reaches stderr unchanged. `createLogger` in `rea serve` installs
60
+ * a redactor compiled from the same SECRET_PATTERNS used by the redact
61
+ * middleware so log records carry the same protection as audit records.
62
+ *
63
+ * The hook must be synchronous, total (never throw), and idempotent on
64
+ * already-redacted strings. It MUST NOT perform I/O.
65
+ */
66
+ export type FieldRedactor = (value: string) => string;
67
+ export interface LoggerOptions {
68
+ /** Minimum level to emit. Default from REA_LOG_LEVEL env, else 'info'. */
69
+ level?: LogLevel;
70
+ /** Sink for serialized records. Defaults to `process.stderr`. */
71
+ stream?: NodeJS.WritableStream;
72
+ /**
73
+ * Force output mode. Default: auto — JSON when `stream.isTTY !== true`,
74
+ * pretty when the stream is a TTY.
75
+ */
76
+ mode?: 'json' | 'pretty';
77
+ /** Clock injection for tests. Default: `Date.now`. */
78
+ now?: () => number;
79
+ /** Base fields merged into every record (used by `child()`). */
80
+ base?: Partial<LogFields>;
81
+ /**
82
+ * Optional pre-emit redactor applied to every string-valued field.
83
+ * See {@link FieldRedactor}. When omitted, no log-field redaction runs
84
+ * (0.3.x behavior). `rea serve` wires this up to the same patterns the
85
+ * redact middleware uses so downstream error messages can't leak creds
86
+ * to stderr.
87
+ */
88
+ redactField?: FieldRedactor;
89
+ }
90
+ /**
91
+ * Parse `REA_LOG_LEVEL`. Unknown values fall back to 'info' — we never want
92
+ * startup to fail because an operator typo'd an env var.
93
+ */
94
+ export declare function resolveLogLevel(raw: string | undefined): LogLevel;
95
+ /**
96
+ * Build a {@link FieldRedactor} from a list of plain regexes. Each regex
97
+ * is applied in order with `String.prototype.replace`, producing
98
+ * `[REDACTED]` where a match is found.
99
+ *
100
+ * Why not reuse the middleware's `SafeRegex` wrappers?
101
+ * - Log fields are short operator-supplied strings (typically downstream
102
+ * error messages + event names), not arbitrary MCP payloads. The
103
+ * ReDoS surface that motivated `SafeRegex` for the redact middleware
104
+ * does not apply here — the patterns we ship are anchored and
105
+ * bounded.
106
+ * - Running regex in a worker thread on every log line would add
107
+ * measurable latency to every event and pull in the worker pool on
108
+ * startup. For a sync logger that needs to be near-free, a
109
+ * sync-regex path is the right tradeoff.
110
+ *
111
+ * Exported so callers can pass their own compiled pattern list without
112
+ * touching the internal shape.
113
+ */
114
+ export declare function buildRegexRedactor(patterns: ReadonlyArray<{
115
+ name: string;
116
+ pattern: RegExp;
117
+ }>): FieldRedactor;
118
+ /**
119
+ * Create a logger. Defaults are appropriate for `rea serve` at runtime;
120
+ * tests inject `stream`, `mode`, and `now` directly.
121
+ */
122
+ export declare function createLogger(opts?: LoggerOptions): Logger;
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Structured gateway logger (G5).
3
+ *
4
+ * Minimal JSON-lines logger for `rea serve` and its collaborators. The existing
5
+ * codebase had `console.error`/`console.warn` scattered across the gateway;
6
+ * that worked for humans tailing stderr but made logs impossible to parse for
7
+ * future tooling (shipping to syslog, aggregating in a dashboard, greping
8
+ * per-session).
9
+ *
10
+ * Design constraints (from the task spec):
11
+ *
12
+ * 1. NO dependency — pino would be overkill, and dep weight matters for an
13
+ * install-anywhere CLI. ~30 lines of hand-rolled code is enough.
14
+ * 2. Respect `REA_LOG_LEVEL` (info default; debug for future verbose hooks).
15
+ * 3. Pretty-print on a TTY stderr; emit JSON lines on non-TTY (CI, redirected
16
+ * stderr, process-supervised runs).
17
+ * 4. Preserve the `[rea-serve]` / `[rea]` prefix convention — the helix smoke
18
+ * test greps for these. Pretty-mode prints them explicitly; JSON mode
19
+ * carries them as structured fields.
20
+ * 5. Never throw. A logger that can crash the gateway is a bad trade.
21
+ *
22
+ * ## What this is NOT
23
+ *
24
+ * - Not an OpenTelemetry integration — see {@link ./observability/metrics.ts}
25
+ * for counters/gauges. Logs and metrics are separate concerns.
26
+ * - Not the audit log — `.rea/audit.jsonl` is a hash-chained, tamper-evident
27
+ * record of tool calls. This module is free-form operator observability.
28
+ * - Not a file logger. Records go to stderr only. If you need durable logs,
29
+ * redirect the process's stderr.
30
+ */
31
+ /** Ordered lowest → highest. A record is emitted iff its level ≥ current. */
32
+ const LEVEL_ORDER = {
33
+ debug: 10,
34
+ info: 20,
35
+ warn: 30,
36
+ error: 40,
37
+ };
38
+ /**
39
+ * Hard byte-cap for string-valued log fields. Applied before any regex runs
40
+ * so that an attacker-influenced error message (e.g. a downstream MCP error
41
+ * that echoes request content) cannot cause catastrophic backtracking against
42
+ * the redaction pattern set, even if a badly-written pattern is loaded.
43
+ *
44
+ * 4 KiB is generous for any legitimate structured-log value and tiny compared
45
+ * to the multi-MB payloads that would be needed to trigger backtracking on
46
+ * a pathological pattern.
47
+ */
48
+ const MAX_LOG_FIELD_BYTES = 4096;
49
+ /**
50
+ * Parse `REA_LOG_LEVEL`. Unknown values fall back to 'info' — we never want
51
+ * startup to fail because an operator typo'd an env var.
52
+ */
53
+ export function resolveLogLevel(raw) {
54
+ if (raw === undefined)
55
+ return 'info';
56
+ const normalized = raw.trim().toLowerCase();
57
+ if (normalized === 'debug' ||
58
+ normalized === 'info' ||
59
+ normalized === 'warn' ||
60
+ normalized === 'error') {
61
+ return normalized;
62
+ }
63
+ return 'info';
64
+ }
65
+ /**
66
+ * ANSI color helpers. Kept tiny — we don't pull chalk to color four words.
67
+ * A non-TTY writer never hits these because pretty mode only runs on a TTY.
68
+ */
69
+ const COLOR = {
70
+ reset: '\x1b[0m',
71
+ dim: '\x1b[2m',
72
+ red: '\x1b[31m',
73
+ yellow: '\x1b[33m',
74
+ green: '\x1b[32m',
75
+ cyan: '\x1b[36m',
76
+ };
77
+ function levelColor(level) {
78
+ switch (level) {
79
+ case 'error':
80
+ return COLOR.red;
81
+ case 'warn':
82
+ return COLOR.yellow;
83
+ case 'info':
84
+ return COLOR.green;
85
+ case 'debug':
86
+ return COLOR.cyan;
87
+ }
88
+ }
89
+ /**
90
+ * Resolve `mode` from options and stream. A non-TTY stream always gets JSON —
91
+ * that is the well-defined contract for supervisors like systemd or Claude
92
+ * Code's MCP runner which redirect our stderr.
93
+ */
94
+ function resolveMode(stream, explicit) {
95
+ if (explicit !== undefined)
96
+ return explicit;
97
+ const isTTY = stream.isTTY === true;
98
+ return isTTY ? 'pretty' : 'json';
99
+ }
100
+ /**
101
+ * Stringify a record body. We do NOT use JSON.stringify's replacer argument
102
+ * for ordering — JSON.stringify preserves insertion order for string keys,
103
+ * so composing the record in a stable order is enough.
104
+ */
105
+ function serialize(record) {
106
+ try {
107
+ return JSON.stringify(record);
108
+ }
109
+ catch {
110
+ // Last-ditch: drop unserializable values. A logger that throws on a
111
+ // circular ref in a user-supplied field would be a denial-of-service
112
+ // surface on its own daemon.
113
+ return JSON.stringify({
114
+ timestamp: record['timestamp'],
115
+ level: record['level'],
116
+ event: record['event'],
117
+ message: '[unserializable log record]',
118
+ });
119
+ }
120
+ }
121
+ /**
122
+ * Safe stringify for pretty-mode extras. JSON mode has its own serialize()
123
+ * fallback; pretty mode historically called `JSON.stringify(v)` raw, which
124
+ * throws on cyclic references. The outer emit() wrapped that in an empty
125
+ * try/catch so the entire record was dropped silently — a logger that
126
+ * drops records on operator-supplied extras is a DoS surface.
127
+ *
128
+ * Here we catch the throw, substitute a stable placeholder, and keep
129
+ * emitting. The record reaches the operator even if one field is
130
+ * unserializable.
131
+ */
132
+ function safeStringifyExtra(v) {
133
+ try {
134
+ return JSON.stringify(v);
135
+ }
136
+ catch {
137
+ return '[unserializable]';
138
+ }
139
+ }
140
+ /**
141
+ * Pretty-format for a human reader. Keeps the `[rea-serve]` prefix convention
142
+ * so the helix smoke test's grep still matches.
143
+ */
144
+ function formatPretty(level, timestamp, fields) {
145
+ const color = levelColor(level);
146
+ const levelTag = level.toUpperCase().padEnd(5);
147
+ // Strip C0 controls and DEL from disk-sourced strings before terminal output
148
+ // to prevent ANSI/OSC escape injection from compromised MCP error messages.
149
+ const strip = (s) => s.replace(/[\x00-\x1f\x7f\u200b-\u200f\u202a-\u202e\u2028\u2029\u2066-\u2069]/g, '?');
150
+ const event = strip(fields.event);
151
+ const message = strip(fields.message);
152
+ // Extract the well-known fields we already rendered.
153
+ const { event: _e, message: _m, session_id, server_name, ...rest } = fields;
154
+ void _e;
155
+ void _m;
156
+ const extras = [];
157
+ if (server_name !== undefined)
158
+ extras.push(`server=${strip(String(server_name))}`);
159
+ if (session_id !== undefined)
160
+ extras.push(`session=${strip(String(session_id)).slice(0, 8)}`);
161
+ for (const [k, v] of Object.entries(rest)) {
162
+ if (k === 'timestamp' || k === 'level')
163
+ continue; // trusted-internal: toISOString + enum-derived
164
+ extras.push(`${k}=${typeof v === 'string' ? strip(v) : safeStringifyExtra(v)}`);
165
+ }
166
+ const extrasStr = extras.length > 0 ? ` ${COLOR.dim}${extras.join(' ')}${COLOR.reset}` : '';
167
+ return `${COLOR.dim}${timestamp}${COLOR.reset} ${color}${levelTag}${COLOR.reset} [rea-serve] ${event}: ${message}${extrasStr}\n`;
168
+ }
169
+ class BasicLogger {
170
+ minLevel;
171
+ stream;
172
+ mode;
173
+ now;
174
+ base;
175
+ redactField;
176
+ constructor(opts) {
177
+ const level = opts.level ?? resolveLogLevel(process.env['REA_LOG_LEVEL']);
178
+ this.minLevel = LEVEL_ORDER[level];
179
+ this.stream = opts.stream ?? process.stderr;
180
+ this.mode = resolveMode(this.stream, opts.mode);
181
+ this.now = opts.now ?? Date.now;
182
+ this.base = opts.base ?? {};
183
+ this.redactField = opts.redactField;
184
+ }
185
+ debug(fields) {
186
+ this.emit('debug', fields);
187
+ }
188
+ info(fields) {
189
+ this.emit('info', fields);
190
+ }
191
+ warn(fields) {
192
+ this.emit('warn', fields);
193
+ }
194
+ error(fields) {
195
+ this.emit('error', fields);
196
+ }
197
+ child(base) {
198
+ return new BasicLogger({
199
+ level: inverseLevel(this.minLevel),
200
+ stream: this.stream,
201
+ mode: this.mode,
202
+ now: this.now,
203
+ base: { ...this.base, ...base },
204
+ ...(this.redactField !== undefined ? { redactField: this.redactField } : {}),
205
+ });
206
+ }
207
+ /**
208
+ * Apply the configured field redactor (if any) to every string-valued
209
+ * entry in the merged record. Non-string values pass through
210
+ * unchanged — a JSON-serializable object with credentials inside would
211
+ * need its own redactor upstream; the typical leak path for downstream
212
+ * MCP errors is a plain-string `error` or `message` field.
213
+ *
214
+ * SECURITY: downstream error messages are attacker-influenced and can
215
+ * be arbitrarily long. A badly-backtracking pattern applied to a large
216
+ * string would stall the event loop. Hard-cap each string field at
217
+ * MAX_LOG_FIELD_BYTES BEFORE applying any regex. The cap is applied
218
+ * regardless of whether a redactor is configured so the logger never
219
+ * writes runaway-length values to stderr even without patterns loaded.
220
+ */
221
+ applyRedactor(merged) {
222
+ const redactor = this.redactField;
223
+ const out = {};
224
+ for (const [k, v] of Object.entries(merged)) {
225
+ if (typeof v === 'string') {
226
+ // Truncate before regex — prevents catastrophic backtracking on
227
+ // attacker-controlled strings regardless of which patterns are loaded.
228
+ const capped = v.length > MAX_LOG_FIELD_BYTES
229
+ ? v.slice(0, MAX_LOG_FIELD_BYTES) + '\u2026[truncated]'
230
+ : v;
231
+ if (redactor === undefined) {
232
+ out[k] = capped;
233
+ }
234
+ else {
235
+ try {
236
+ out[k] = redactor(capped);
237
+ }
238
+ catch {
239
+ // A crashing redactor must not drop the record. Fall back to
240
+ // a stable sentinel so the record still reaches the operator
241
+ // without the raw (possibly sensitive) value.
242
+ out[k] = '[redactor-error]';
243
+ }
244
+ }
245
+ }
246
+ else {
247
+ out[k] = v;
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+ emit(level, raw) {
253
+ if (LEVEL_ORDER[level] < this.minLevel)
254
+ return;
255
+ // Merge in base fields; explicit fields win.
256
+ const merged0 = { ...this.base, ...raw };
257
+ // SECURITY: apply the field redactor BEFORE serialization so the line
258
+ // written to stderr never contains the raw string. No-op when no
259
+ // redactor was configured.
260
+ const merged = this.applyRedactor(merged0);
261
+ const timestamp = new Date(this.now()).toISOString();
262
+ try {
263
+ let line;
264
+ if (this.mode === 'json') {
265
+ const record = { timestamp, level, ...merged };
266
+ line = serialize(record) + '\n';
267
+ }
268
+ else {
269
+ line = formatPretty(level, timestamp, merged);
270
+ }
271
+ this.stream.write(line);
272
+ }
273
+ catch {
274
+ // Last resort — the stream rejected our write. Swallow; nothing we do
275
+ // here can improve the situation.
276
+ }
277
+ }
278
+ }
279
+ /**
280
+ * Recover the log level name from its numeric order. Only used when creating
281
+ * a child logger so the child inherits the parent's level without a second env
282
+ * lookup.
283
+ */
284
+ function inverseLevel(order) {
285
+ for (const [name, n] of Object.entries(LEVEL_ORDER)) {
286
+ if (n === order)
287
+ return name;
288
+ }
289
+ return 'info';
290
+ }
291
+ /**
292
+ * Build a {@link FieldRedactor} from a list of plain regexes. Each regex
293
+ * is applied in order with `String.prototype.replace`, producing
294
+ * `[REDACTED]` where a match is found.
295
+ *
296
+ * Why not reuse the middleware's `SafeRegex` wrappers?
297
+ * - Log fields are short operator-supplied strings (typically downstream
298
+ * error messages + event names), not arbitrary MCP payloads. The
299
+ * ReDoS surface that motivated `SafeRegex` for the redact middleware
300
+ * does not apply here — the patterns we ship are anchored and
301
+ * bounded.
302
+ * - Running regex in a worker thread on every log line would add
303
+ * measurable latency to every event and pull in the worker pool on
304
+ * startup. For a sync logger that needs to be near-free, a
305
+ * sync-regex path is the right tradeoff.
306
+ *
307
+ * Exported so callers can pass their own compiled pattern list without
308
+ * touching the internal shape.
309
+ */
310
+ export function buildRegexRedactor(patterns) {
311
+ // Capture patterns by reference; they are long-lived across logger use.
312
+ return (value) => {
313
+ let out = value;
314
+ for (const { pattern } of patterns) {
315
+ // Always create a fresh RegExp for global or sticky patterns — shared
316
+ // stateful RegExp objects carry `lastIndex` that concurrent callers (e.g.
317
+ // the SafeRegex worker in the redact middleware) can leave non-zero,
318
+ // causing String.replace to start mid-string and silently skip leading
319
+ // secrets. The `y` (sticky) flag carries the same hazard as `g`.
320
+ const re = (pattern.global || pattern.sticky)
321
+ ? new RegExp(pattern.source, pattern.flags)
322
+ : pattern;
323
+ out = out.replace(re, '[REDACTED]');
324
+ }
325
+ return out;
326
+ };
327
+ }
328
+ /**
329
+ * Create a logger. Defaults are appropriate for `rea serve` at runtime;
330
+ * tests inject `stream`, `mode`, and `now` directly.
331
+ */
332
+ export function createLogger(opts = {}) {
333
+ return new BasicLogger(opts);
334
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Policy } from '../../policy/types.js';
2
2
  import type { Middleware } from './chain.js';
3
+ import type { MetricsRegistry } from '../observability/metrics.js';
3
4
  /**
4
5
  * Post-execution middleware: appends a hash-chained JSONL audit record.
5
6
  *
@@ -8,5 +9,27 @@ import type { Middleware } from './chain.js';
8
9
  * SECURITY: Wraps next() in try/finally to ensure audit runs even on middleware exceptions.
9
10
  * SECURITY: Placed as outermost middleware so audit records ALL invocations, including denials.
10
11
  * PERFORMANCE: All fs operations are async to avoid blocking the event loop.
12
+ *
13
+ * CONCURRENCY (G1):
14
+ * - Per-process: the writeQueue below serializes writes within the Node process.
15
+ * - Cross-process: each write acquires a `proper-lockfile` lock on `.rea/`.
16
+ * Stale locks are reclaimed after 10s. Lock-acquisition failure falls back
17
+ * to the current best-effort behavior — the tool call proceeds and the
18
+ * failure is logged. Breaking the invocation because the auditor failed
19
+ * would let an audit outage take down the gateway.
20
+ *
21
+ * ROTATION (G1):
22
+ * - `maybeRotate` runs before each write's lock acquisition. Rotation writes
23
+ * a marker record whose `prev_hash` preserves hash-chain continuity across
24
+ * the rotation boundary. When no `audit.rotation` block is set in policy,
25
+ * rotation is a no-op — 0.2.x behavior is preserved.
26
+ */
27
+ export declare function createAuditMiddleware(baseDir: string, policy?: Policy,
28
+ /**
29
+ * Optional metrics registry. When supplied, the
30
+ * `rea_audit_lines_appended_total` counter is incremented on every
31
+ * successful append (post-fsync). When omitted, no metrics are emitted —
32
+ * keeps the middleware usable in unit tests that don't exercise the
33
+ * observability surface.
11
34
  */
12
- export declare function createAuditMiddleware(baseDir: string, policy?: Policy): Middleware;
35
+ metrics?: MetricsRegistry): Middleware;