@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.
- package/.husky/pre-push +15 -18
- package/README.md +41 -1
- package/THREAT_MODEL.md +100 -29
- package/dist/audit/append.d.ts +21 -8
- package/dist/audit/append.js +48 -83
- package/dist/audit/fs.d.ts +68 -0
- package/dist/audit/fs.js +171 -0
- package/dist/cli/audit.d.ts +40 -0
- package/dist/cli/audit.js +205 -0
- package/dist/cli/doctor.d.ts +19 -4
- package/dist/cli/doctor.js +172 -5
- package/dist/cli/index.js +26 -1
- package/dist/cli/init.js +93 -7
- package/dist/cli/install/pre-push.d.ts +335 -0
- package/dist/cli/install/pre-push.js +2818 -0
- package/dist/cli/serve.d.ts +64 -0
- package/dist/cli/serve.js +270 -2
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.js +399 -0
- package/dist/cli/utils.d.ts +4 -0
- package/dist/cli/utils.js +4 -0
- package/dist/gateway/audit/rotator.d.ts +116 -0
- package/dist/gateway/audit/rotator.js +289 -0
- package/dist/gateway/circuit-breaker.d.ts +17 -0
- package/dist/gateway/circuit-breaker.js +32 -3
- package/dist/gateway/downstream-pool.d.ts +2 -1
- package/dist/gateway/downstream-pool.js +2 -2
- package/dist/gateway/downstream.d.ts +39 -3
- package/dist/gateway/downstream.js +73 -14
- package/dist/gateway/log.d.ts +122 -0
- package/dist/gateway/log.js +334 -0
- package/dist/gateway/middleware/audit.d.ts +24 -1
- package/dist/gateway/middleware/audit.js +103 -58
- package/dist/gateway/middleware/blocked-paths.d.ts +0 -9
- package/dist/gateway/middleware/blocked-paths.js +439 -67
- package/dist/gateway/middleware/injection.d.ts +218 -13
- package/dist/gateway/middleware/injection.js +433 -51
- package/dist/gateway/middleware/kill-switch.d.ts +10 -1
- package/dist/gateway/middleware/kill-switch.js +20 -1
- package/dist/gateway/observability/metrics.d.ts +125 -0
- package/dist/gateway/observability/metrics.js +321 -0
- package/dist/gateway/server.d.ts +19 -0
- package/dist/gateway/server.js +99 -15
- package/dist/policy/loader.d.ts +47 -0
- package/dist/policy/loader.js +47 -0
- package/dist/policy/profiles.d.ts +13 -0
- package/dist/policy/profiles.js +12 -0
- package/dist/policy/types.d.ts +52 -0
- package/dist/registry/fingerprint.d.ts +73 -0
- package/dist/registry/fingerprint.js +81 -0
- package/dist/registry/fingerprints-store.d.ts +62 -0
- package/dist/registry/fingerprints-store.js +111 -0
- package/dist/registry/interpolate.d.ts +58 -0
- package/dist/registry/interpolate.js +121 -0
- package/dist/registry/loader.d.ts +2 -2
- package/dist/registry/loader.js +22 -1
- package/dist/registry/tofu-gate.d.ts +41 -0
- package/dist/registry/tofu-gate.js +189 -0
- package/dist/registry/tofu.d.ts +111 -0
- package/dist/registry/tofu.js +173 -0
- package/dist/registry/types.d.ts +9 -1
- package/package.json +3 -1
- package/profiles/bst-internal-no-codex.yaml +5 -0
- package/profiles/bst-internal.yaml +7 -0
- package/scripts/tarball-smoke.sh +197 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
3
|
import { Tier, InvocationStatus } from '../../policy/types.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
8
|
-
}
|
|
4
|
+
import { computeHash, fsyncFile, readLastRecord, withAuditLock, } from '../../audit/fs.js';
|
|
5
|
+
import { maybeRotate } from '../audit/rotator.js';
|
|
9
6
|
/**
|
|
10
7
|
* Post-execution middleware: appends a hash-chained JSONL audit record.
|
|
11
8
|
*
|
|
@@ -14,15 +11,42 @@ function computeHash(record) {
|
|
|
14
11
|
* SECURITY: Wraps next() in try/finally to ensure audit runs even on middleware exceptions.
|
|
15
12
|
* SECURITY: Placed as outermost middleware so audit records ALL invocations, including denials.
|
|
16
13
|
* PERFORMANCE: All fs operations are async to avoid blocking the event loop.
|
|
14
|
+
*
|
|
15
|
+
* CONCURRENCY (G1):
|
|
16
|
+
* - Per-process: the writeQueue below serializes writes within the Node process.
|
|
17
|
+
* - Cross-process: each write acquires a `proper-lockfile` lock on `.rea/`.
|
|
18
|
+
* Stale locks are reclaimed after 10s. Lock-acquisition failure falls back
|
|
19
|
+
* to the current best-effort behavior — the tool call proceeds and the
|
|
20
|
+
* failure is logged. Breaking the invocation because the auditor failed
|
|
21
|
+
* would let an audit outage take down the gateway.
|
|
22
|
+
*
|
|
23
|
+
* ROTATION (G1):
|
|
24
|
+
* - `maybeRotate` runs before each write's lock acquisition. Rotation writes
|
|
25
|
+
* a marker record whose `prev_hash` preserves hash-chain continuity across
|
|
26
|
+
* the rotation boundary. When no `audit.rotation` block is set in policy,
|
|
27
|
+
* rotation is a no-op — 0.2.x behavior is preserved.
|
|
17
28
|
*/
|
|
18
|
-
export function createAuditMiddleware(baseDir, policy
|
|
29
|
+
export function createAuditMiddleware(baseDir, policy,
|
|
30
|
+
/**
|
|
31
|
+
* Optional metrics registry. When supplied, the
|
|
32
|
+
* `rea_audit_lines_appended_total` counter is incremented on every
|
|
33
|
+
* successful append (post-fsync). When omitted, no metrics are emitted —
|
|
34
|
+
* keeps the middleware usable in unit tests that don't exercise the
|
|
35
|
+
* observability surface.
|
|
36
|
+
*/
|
|
37
|
+
metrics) {
|
|
19
38
|
// REA writes to a single .rea/audit.jsonl file (not dated per-day files).
|
|
20
39
|
const reaDir = path.join(baseDir, '.rea');
|
|
21
40
|
const auditFile = path.join(reaDir, 'audit.jsonl');
|
|
22
|
-
let prevHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
23
41
|
let dirEnsured = false;
|
|
24
42
|
// SECURITY: Use a write queue to serialize audit writes, ensuring the hash chain is linear.
|
|
25
43
|
let writeQueue = Promise.resolve();
|
|
44
|
+
async function ensureDir() {
|
|
45
|
+
if (!dirEnsured) {
|
|
46
|
+
await fs.mkdir(reaDir, { recursive: true });
|
|
47
|
+
dirEnsured = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
26
50
|
return async (ctx, next) => {
|
|
27
51
|
let nextError;
|
|
28
52
|
try {
|
|
@@ -40,64 +64,85 @@ export function createAuditMiddleware(baseDir, policy) {
|
|
|
40
64
|
// kill-switch denied before policy middleware ran).
|
|
41
65
|
const duration_ms = Date.now() - ctx.start_time;
|
|
42
66
|
const autonomyLevel = ctx.metadata.autonomy_level ?? policy?.autonomy_level ?? 'unknown';
|
|
67
|
+
// Cap ctx.error before writing the audit record. A downstream MCP server
|
|
68
|
+
// can produce arbitrarily long error strings; if the audit record grows
|
|
69
|
+
// beyond ~64 KiB, `rea status` misreports it as corrupt because the tail
|
|
70
|
+
// window in summarizeAudit cannot contain the full record. 4096 bytes is
|
|
71
|
+
// generous for any legitimate error description.
|
|
72
|
+
const MAX_AUDIT_ERROR_BYTES = 4096;
|
|
73
|
+
if (ctx.error && ctx.error.length > MAX_AUDIT_ERROR_BYTES) {
|
|
74
|
+
ctx.error = ctx.error.slice(0, MAX_AUDIT_ERROR_BYTES) + '\u2026[truncated]';
|
|
75
|
+
}
|
|
43
76
|
// Serialize audit writes via a queue to maintain hash chain linearity under concurrency.
|
|
44
|
-
// Each write awaits the previous one before
|
|
77
|
+
// Each write awaits the previous one before running its lock-scoped append.
|
|
45
78
|
const writePromise = writeQueue.then(async () => {
|
|
46
79
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
await ensureDir();
|
|
81
|
+
// G1: Attempt rotation BEFORE acquiring the append lock. No-op when the
|
|
82
|
+
// policy's audit.rotation block is absent. Errors are swallowed inside
|
|
83
|
+
// maybeRotate so rotation can never take down the gateway.
|
|
84
|
+
await maybeRotate(auditFile, policy);
|
|
85
|
+
await withAuditLock(auditFile, async () => {
|
|
86
|
+
const { hash: prevHash } = await readLastRecord(auditFile);
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const recordBase = {
|
|
89
|
+
timestamp: now,
|
|
90
|
+
session_id: ctx.session_id,
|
|
91
|
+
tool_name: ctx.tool_name,
|
|
92
|
+
server_name: ctx.server_name,
|
|
93
|
+
tier: ctx.tier ?? Tier.Write,
|
|
94
|
+
status: ctx.status,
|
|
95
|
+
autonomy_level: autonomyLevel,
|
|
96
|
+
duration_ms,
|
|
97
|
+
prev_hash: prevHash,
|
|
98
|
+
};
|
|
99
|
+
if (ctx.error) {
|
|
100
|
+
recordBase.error = ctx.error;
|
|
101
|
+
}
|
|
102
|
+
if (ctx.redacted_fields?.length) {
|
|
103
|
+
recordBase.redacted_fields = ctx.redacted_fields;
|
|
104
|
+
}
|
|
105
|
+
// Attach caller-supplied metadata when the middleware context carries any.
|
|
106
|
+
// The `autonomy_level` key is reserved for internal bookkeeping (see above)
|
|
107
|
+
// and is excluded from the exported metadata payload.
|
|
108
|
+
if (ctx.metadata !== undefined) {
|
|
109
|
+
const exported = {};
|
|
110
|
+
for (const [k, v] of Object.entries(ctx.metadata)) {
|
|
111
|
+
if (k === 'autonomy_level')
|
|
112
|
+
continue;
|
|
113
|
+
exported[k] = v;
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(exported).length > 0) {
|
|
116
|
+
recordBase.metadata = exported;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const hash = computeHash(recordBase);
|
|
120
|
+
const record = { ...recordBase, hash };
|
|
121
|
+
const line = JSON.stringify(record) + '\n';
|
|
122
|
+
try {
|
|
123
|
+
await fs.appendFile(auditFile, line);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Directory may have been deleted externally — retry once with mkdir
|
|
127
|
+
dirEnsured = false;
|
|
128
|
+
await ensureDir();
|
|
129
|
+
await fs.appendFile(auditFile, line);
|
|
130
|
+
}
|
|
131
|
+
await fsyncFile(auditFile);
|
|
132
|
+
// Only increment after fsync — a counter advance for a line that
|
|
133
|
+
// was never durable on disk would be a lie.
|
|
134
|
+
try {
|
|
135
|
+
metrics?.incAuditLines(1);
|
|
74
136
|
}
|
|
75
|
-
|
|
76
|
-
|
|
137
|
+
catch {
|
|
138
|
+
// Metrics failures must never crash the gateway.
|
|
77
139
|
}
|
|
78
|
-
}
|
|
79
|
-
const hash = computeHash(recordBase);
|
|
80
|
-
const record = { ...recordBase, hash };
|
|
81
|
-
prevHash = hash;
|
|
82
|
-
const line = JSON.stringify(record) + '\n';
|
|
83
|
-
// Ensure .rea dir exists (cached, with retry on failure)
|
|
84
|
-
if (!dirEnsured) {
|
|
85
|
-
await fs.mkdir(reaDir, { recursive: true });
|
|
86
|
-
dirEnsured = true;
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
await fs.appendFile(auditFile, line);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Directory may have been deleted externally — retry once with mkdir
|
|
93
|
-
dirEnsured = false;
|
|
94
|
-
await fs.mkdir(reaDir, { recursive: true });
|
|
95
|
-
dirEnsured = true;
|
|
96
|
-
await fs.appendFile(auditFile, line);
|
|
97
|
-
}
|
|
140
|
+
});
|
|
98
141
|
}
|
|
99
142
|
catch (auditErr) {
|
|
100
|
-
// SECURITY: Never crash the gateway on audit failure — log to stderr
|
|
143
|
+
// SECURITY: Never crash the gateway on audit failure — log to stderr.
|
|
144
|
+
// This catches lock-acquisition failures, EEXIST-without-stale, and
|
|
145
|
+
// any other I/O failure. The tool call itself continues.
|
|
101
146
|
dirEnsured = false;
|
|
102
147
|
console.error('[rea] AUDIT WRITE FAILED:', auditErr instanceof Error ? auditErr.message : auditErr);
|
|
103
148
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
1
|
import type { Policy } from '../../policy/types.js';
|
|
2
2
|
import type { Middleware } from './chain.js';
|
|
3
|
-
/**
|
|
4
|
-
* Pre-execution middleware: denies tool invocations whose arguments
|
|
5
|
-
* reference paths that are in the policy's blocked_paths list.
|
|
6
|
-
*
|
|
7
|
-
* SECURITY: Inspects all string values in arguments (including nested objects/arrays).
|
|
8
|
-
* SECURITY: Always blocks .rea/ regardless of policy configuration.
|
|
9
|
-
* SECURITY: Normalizes URL-encoded characters, path separators, and case before comparison.
|
|
10
|
-
* SECURITY: Re-reads blocked_paths from policy.yaml when baseDir is provided (hot-reload).
|
|
11
|
-
*/
|
|
12
3
|
export declare function createBlockedPathsMiddleware(initialPolicy: Policy, baseDir?: string): Middleware;
|