@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
package/.husky/pre-push
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
+
# rea:husky-pre-push-gate v1
|
|
3
|
+
# rea:gate-body-v1
|
|
2
4
|
# .husky/pre-push — rea governance gate for terminal-initiated pushes.
|
|
3
5
|
#
|
|
4
6
|
# Mirrors the logic of `.claude/hooks/push-review-gate.sh` but consumes the
|
|
@@ -20,8 +22,10 @@
|
|
|
20
22
|
# which ran the loop in a subshell — `exit 1` inside the loop aborted the
|
|
21
23
|
# subshell only, and the script then ran `exit 0` and allowed the push. We
|
|
22
24
|
# now feed the loop with a here-doc so it runs in the main shell, and we
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
+
# abort immediately (`exit 1`) on the first blocking refspec. The accumulator
|
|
26
|
+
# pattern (`block_push=1; continue`) was dropped so the text-level detector
|
|
27
|
+
# in `src/cli/install/pre-push.ts` can verify the miss-path is truly blocking
|
|
28
|
+
# without modeling loop-carried flags and post-loop exit blocks.
|
|
25
29
|
|
|
26
30
|
set -eu
|
|
27
31
|
|
|
@@ -63,13 +67,11 @@ if [ -f "$READ_FIELD_JS" ]; then
|
|
|
63
67
|
fi
|
|
64
68
|
fi
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# inside it would only abort that subshell — NOT the push — which was a
|
|
72
|
-
# real governance defect in the pre-review version of this file.
|
|
70
|
+
# Here-doc feeds the loop without creating a subshell, so an `exit 1`
|
|
71
|
+
# inside the loop terminates the hook and blocks the push. A pipeline
|
|
72
|
+
# would run the loop in a subshell and `exit 1` inside it would only
|
|
73
|
+
# abort that subshell — NOT the push — which was a real governance
|
|
74
|
+
# defect in the pre-review version of this file.
|
|
73
75
|
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
74
76
|
[ -z "${local_sha:-}" ] && continue
|
|
75
77
|
# Branch deletion: local_sha is 40 zeros. Skip protected-path check.
|
|
@@ -103,26 +105,21 @@ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
103
105
|
if [ ! -f "$AUDIT_LOG" ]; then
|
|
104
106
|
printf 'PUSH BLOCKED: protected paths changed but no audit log found at %s\n' "$AUDIT_LOG" >&2
|
|
105
107
|
printf ' Run /codex-review on HEAD %s before pushing.\n' "$local_sha" >&2
|
|
106
|
-
|
|
107
|
-
continue
|
|
108
|
+
exit 1
|
|
108
109
|
fi
|
|
109
110
|
# Require both (a) a `codex.review` tool_name and (b) the exact head_sha
|
|
110
111
|
# on the same JSONL line. The `codex.review` pattern ends with a closing
|
|
111
|
-
# quote, so `codex.review.skipped` never satisfies the gate.
|
|
112
|
+
# quote, so `codex.review.skipped` never satisfies the gate. The first
|
|
113
|
+
# refspec that fails this check aborts the hook — no accumulator needed.
|
|
112
114
|
if ! grep -E '"tool_name":"codex\.review"' "$AUDIT_LOG" 2>/dev/null | \
|
|
113
115
|
grep -qF "\"head_sha\":\"$local_sha\""; then
|
|
114
116
|
printf 'PUSH BLOCKED: protected paths changed — /codex-review required for HEAD %s\n' "$local_sha" >&2
|
|
115
117
|
printf ' Run /codex-review, or set REA_SKIP_CODEX_REVIEW=<reason> to bypass.\n' >&2
|
|
116
|
-
|
|
117
|
-
continue
|
|
118
|
+
exit 1
|
|
118
119
|
fi
|
|
119
120
|
fi
|
|
120
121
|
done <<HOOK_INPUT_EOF
|
|
121
122
|
$INPUT
|
|
122
123
|
HOOK_INPUT_EOF
|
|
123
124
|
|
|
124
|
-
if [ "$block_push" -ne 0 ]; then
|
|
125
|
-
exit 1
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
125
|
exit 0
|
package/README.md
CHANGED
|
@@ -66,7 +66,10 @@ to build a separate package that composes with REA.
|
|
|
66
66
|
`policy.yaml` is the maximum surface area — one outbound POST, opt-in.
|
|
67
67
|
- **Not a daemon supervisor.** `rea serve` is started by Claude Code via
|
|
68
68
|
`.mcp.json`. Claude Code owns the lifecycle. There is no `rea start`,
|
|
69
|
-
no `rea stop`, no
|
|
69
|
+
no `rea stop`, no systemd unit. A short-lived `.rea/serve.pid`
|
|
70
|
+
breadcrumb is written at startup so `rea status` can detect a live
|
|
71
|
+
gateway — it is removed on graceful shutdown and never used for
|
|
72
|
+
locking or lifecycle management.
|
|
70
73
|
- **Not a hosted service.** There is no REA Cloud, no SaaS tier, no
|
|
71
74
|
multi-token workstreams, no workload isolation platform.
|
|
72
75
|
- **Not a 70-agent roster.** 10 curated agents ship in the package. Four
|
|
@@ -132,6 +135,43 @@ install, `.mcp.json` gateway wiring, Codex plugin availability, and the
|
|
|
132
135
|
integrity of the audit hash chain. It returns a pass/fail summary with
|
|
133
136
|
specific remediation hints.
|
|
134
137
|
|
|
138
|
+
### 4. Watch the running gateway
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
rea status # human-readable summary
|
|
142
|
+
rea status --json # JSON — pipe to jq
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`rea status` is the live-process view. It reads the pidfile written by
|
|
146
|
+
`rea serve`, verifies the pid is alive, and surfaces the session id,
|
|
147
|
+
policy summary (profile, autonomy, HALT state), and audit stats (lines,
|
|
148
|
+
last timestamp, whether the tail record's hash looks well-formed). Use
|
|
149
|
+
`rea check` when you want the pure on-disk view without probing for a
|
|
150
|
+
live process.
|
|
151
|
+
|
|
152
|
+
### 5. Optional Prometheus `/metrics` endpoint
|
|
153
|
+
|
|
154
|
+
`rea serve` can expose a loopback-only Prometheus endpoint when the
|
|
155
|
+
`REA_METRICS_PORT` environment variable is set:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
REA_METRICS_PORT=9464 rea serve
|
|
159
|
+
# in another shell
|
|
160
|
+
curl http://127.0.0.1:9464/metrics
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Metrics exposed: per-downstream call and error counters, in-flight
|
|
164
|
+
gauge, audit-lines-appended counter, circuit-breaker state gauge, and a
|
|
165
|
+
seconds-since-last-HALT-check gauge. The listener binds to `127.0.0.1`
|
|
166
|
+
only, serves only `GET /metrics` (everything else is a fixed-body 404),
|
|
167
|
+
and never binds by default — "no silent listeners" is a design rule.
|
|
168
|
+
There is no TLS; scrape through SSH/a reverse proxy if you need
|
|
169
|
+
cross-host access.
|
|
170
|
+
|
|
171
|
+
Set `REA_LOG_LEVEL=debug` for verbose gateway logs; the default is
|
|
172
|
+
`info`. Records are JSON lines on a non-TTY stderr and pretty-printed
|
|
173
|
+
on an interactive terminal.
|
|
174
|
+
|
|
135
175
|
## Architecture
|
|
136
176
|
|
|
137
177
|
### Middleware chain
|
package/THREAT_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Threat Model — REA Gateway and Hook Layer
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.x | Last updated: 2026-04-18
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -19,11 +19,14 @@ This document describes the security threat model for REA (`@bookedsolid/rea`),
|
|
|
19
19
|
| `.rea/policy.yaml` | Autonomy level, max autonomy ceiling, blocked paths, attribution policy | Critical — controls all tool access |
|
|
20
20
|
| `.rea/audit.jsonl` | Hash-chained audit log of every tool invocation | High — integrity evidence |
|
|
21
21
|
| `.rea/HALT` | Kill-switch file; presence blocks all tool calls | High — single point of emergency stop |
|
|
22
|
+
| `.rea/install-manifest.json` | SHA-256 baseline of shipped artifacts; drives `rea upgrade` drift reports | Medium — upgrade trust signal |
|
|
22
23
|
| Hook scripts (`hooks/*.sh`) | Bash scripts that enforce security at tool invocation time | High — bypass = loss of control plane |
|
|
23
24
|
| Agent definitions (`agents/*`) | Role definitions and behavioral constraints for specialist agents | Medium |
|
|
24
25
|
| Secrets in scope | Credentials, API keys, tokens visible in tool arguments or results | Critical |
|
|
25
26
|
| Gateway process memory | In-flight tool arguments, results, session state | Medium |
|
|
26
|
-
| Codex invocation audit entries | Record of `/codex
|
|
27
|
+
| Codex invocation audit entries | Record of `/codex-review` / `/codex:adversarial-review` outcomes | Medium — pre-merge gate evidence |
|
|
28
|
+
| Escape-hatch audit entries | `codex.review.skipped` records naming the bypass reason and operator | Medium — governance-weakening signal |
|
|
29
|
+
| `.rea/metrics.jsonl` | Reviewer telemetry (counts, latency, rate-limit signals; NO payloads) | Low — operational observability |
|
|
27
30
|
|
|
28
31
|
---
|
|
29
32
|
|
|
@@ -32,7 +35,7 @@ This document describes the security threat model for REA (`@bookedsolid/rea`),
|
|
|
32
35
|
```
|
|
33
36
|
┌─────────────────────────────────────────────────────────────────┐
|
|
34
37
|
│ TRUSTED │
|
|
35
|
-
│ Human operator (operates via Claude Code UI)
|
|
38
|
+
│ Human operator (operates via Claude Code UI or terminal) │
|
|
36
39
|
│ Claude Code / agent process │
|
|
37
40
|
│ Codex plugin (running under the same Claude Code process) │
|
|
38
41
|
│ │ │
|
|
@@ -46,13 +49,14 @@ This document describes the security threat model for REA (`@bookedsolid/rea`),
|
|
|
46
49
|
│ │ │
|
|
47
50
|
│ ▼ │
|
|
48
51
|
│ UNTRUSTED │
|
|
49
|
-
│ Downstream MCP servers (tool descriptions, results)
|
|
52
|
+
│ Downstream MCP servers (tool descriptions, results, binaries) │
|
|
50
53
|
│ External network (responses, fetched content) │
|
|
51
54
|
│ Codex plugin RESPONSES (treated as untrusted input) │
|
|
55
|
+
│ Downstream subprocess environment (env vars we export to them) │
|
|
52
56
|
└─────────────────────────────────────────────────────────────────┘
|
|
53
57
|
```
|
|
54
58
|
|
|
55
|
-
Downstream MCP servers are treated as untrusted by default. Codex plugin *invocations* are trusted (same process), but Codex *responses* are treated as untrusted input and flow through the injection and redaction middleware just like any other tool result. The `.rea/` directory is always protected — no agent or MCP server can write to it through the gateway.
|
|
59
|
+
Downstream MCP servers are treated as untrusted by default. Codex plugin *invocations* are trusted (same process), but Codex *responses* are treated as untrusted input and flow through the injection and redaction middleware just like any other tool result. The `.rea/` directory is always protected — no agent or MCP server can write to it through the gateway. Environment variables exported to downstream MCP subprocesses are a deliberate data flow outward from the trusted process to an untrusted child — see §5.11.
|
|
56
60
|
|
|
57
61
|
---
|
|
58
62
|
|
|
@@ -66,6 +70,7 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
66
70
|
| Poisoned Codex response | A compromised or adversarial Codex review | Induce Claude to take unsafe action under "review" cover |
|
|
67
71
|
| Local user escalation | Direct filesystem access on the same machine | Modify policy.yaml, tamper with audit log, remove hooks |
|
|
68
72
|
| Supply chain attacker | npm package substitution or dependency confusion | Install malicious code that executes during build/run |
|
|
73
|
+
| Catalog-drift attacker | Compromised downstream starts advertising new tools | Extend attack surface silently after install review |
|
|
69
74
|
|
|
70
75
|
---
|
|
71
76
|
|
|
@@ -78,10 +83,11 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
78
83
|
**Mitigations:**
|
|
79
84
|
|
|
80
85
|
- `injection` middleware scans tool arguments and results for instruction-like patterns.
|
|
86
|
+
- All injection regexes run under a per-call worker-thread timeout (`wrapRegex`, default 100ms) with a hard kill on timeout — catastrophic backtracking cannot hang the gateway. See §5.12.
|
|
81
87
|
- Gateway middleware chain re-derives tier from tool_name independently on every invocation — a poisoned description cannot change the tier classification.
|
|
82
88
|
- `.rea/policy.yaml` is re-read on every invocation; a "set autonomy to L3" instruction cannot be persisted by any middleware path.
|
|
83
89
|
|
|
84
|
-
**Residual risk:** Semantic injection in natural-language descriptions is not fully mitigated. No regex can reliably detect all instruction-injection patterns.
|
|
90
|
+
**Residual risk:** Semantic injection in natural-language descriptions is not fully mitigated. No regex can reliably detect all instruction-injection patterns. A three-valued escalation (`clean` / `suspicious` / `likely-injection`) is planned for 0.3.0 so write/destructive tiers require human-loop approval on suspicion rather than accepting a binary pass/fail.
|
|
85
91
|
|
|
86
92
|
---
|
|
87
93
|
|
|
@@ -91,10 +97,11 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
91
97
|
|
|
92
98
|
**Mitigations:**
|
|
93
99
|
|
|
94
|
-
- `settings-protection.sh` blocks agent writes to `.claude/settings.json`.
|
|
100
|
+
- `settings-protection.sh` blocks agent writes to `.claude/settings.json`, `.claude/hooks/*`, `.husky/*`, `.rea/policy.yaml`, and `.rea/HALT`.
|
|
95
101
|
- The attribution-advisory hook and commit-msg hook both reject AI-attributed commits, so bypassing one does not disable the other.
|
|
96
102
|
- `CLAUDE.md` template bundled with REA enforces a non-negotiable rule: `--no-verify` is never permitted.
|
|
97
103
|
- The hook layer and gateway layer are independent — bypassing hooks does not disable the gateway middleware chain.
|
|
104
|
+
- `dangerous-bash-interceptor.sh` explicitly pattern-matches `--no-verify`, `--no-gpg-sign`, `git reset --hard`, `rm -rf /`, and other destructive forms.
|
|
98
105
|
|
|
99
106
|
**Residual risk:** A sufficiently privileged local process (same user) can modify git config directly without going through the hook. There is no runtime detection for this outside of audit log review.
|
|
100
107
|
|
|
@@ -110,6 +117,8 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
110
117
|
- `policy` middleware enforces the `max_autonomy_level` ceiling at runtime: autonomy_level is clamped even if policy.yaml is edited to exceed the maximum.
|
|
111
118
|
- Policy file is re-read on every tool invocation, so a stricter policy takes effect immediately. A more permissive edit never bypasses the ceiling.
|
|
112
119
|
- `promotion_requires_human_approval: true` is the default; CLI flag `--i-understand-the-risks` is required to disable it.
|
|
120
|
+
- Policy schema is zod `.strict()` — unknown fields fail loudly at load, not silently ignored. Typos cannot silently defang a policy.
|
|
121
|
+
- User-supplied redaction patterns are validated against `safe-regex` at policy load time; a flagged pattern rejects the entire policy load.
|
|
113
122
|
|
|
114
123
|
**Residual risk:** If the local user (not an agent) directly edits policy.yaml and also raises `max_autonomy_level`, there is no enforcement beyond the honor system. This is by design — the human operator is a trusted actor.
|
|
115
124
|
|
|
@@ -125,8 +134,9 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
125
134
|
- Audit write failures are logged to stderr but do not crash the gateway — the tool call proceeds. This avoids DoS via audit failure while preserving the event.
|
|
126
135
|
- `.rea/` is always blocked from agent writes, so agents cannot delete the audit file through the gateway.
|
|
127
136
|
- `rea check audit` verifies the hash chain on demand; intended for pre-merge gates and compliance reviews.
|
|
137
|
+
- Public `@bookedsolid/rea/audit` helper (`src/audit/append.ts`) is the only supported write path for consumers — it computes the hash and appends atomically with fsync so helix.* events and rea.* events share the chain.
|
|
128
138
|
|
|
129
|
-
**Residual risk:** A local user with filesystem access can delete or truncate audit files outside the gateway. Hash-chain verification is a detection control, not a prevention control.
|
|
139
|
+
**Residual risk:** A local user with filesystem access can delete or truncate audit files outside the gateway. Hash-chain verification is a detection control, not a prevention control. Concurrent-writer safety (proper-lockfile) and rotation semantics are planned for 0.3.0 (G1). Until then, concurrent `append()` calls from distinct processes can race at the fsync step.
|
|
130
140
|
|
|
131
141
|
---
|
|
132
142
|
|
|
@@ -137,9 +147,10 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
137
147
|
**Mitigations:**
|
|
138
148
|
|
|
139
149
|
- `redact` middleware scans both tool arguments (pre-execution) and tool results (post-execution) using secret patterns covering AWS keys, GitHub tokens, generic API keys, bearer tokens, PEM private keys, Discord tokens, Anthropic/OpenAI keys, and base64-encoded variants.
|
|
140
|
-
- Redaction patterns are validated at load time for catastrophic-backtracking safety.
|
|
150
|
+
- Redaction patterns are validated at load time for catastrophic-backtracking safety (`safe-regex`), AND bounded at runtime via a per-call worker-thread timeout with hard-kill on budget exhaustion. See §5.12.
|
|
151
|
+
- On regex timeout, the offending value is replaced with the sentinel `[REDACTED: pattern timeout]` — a scanner that cannot complete never lets the untouched value through. The byte length of the offending input is recorded in audit metadata; the input text is NEVER written to the log.
|
|
141
152
|
- `secret-scanner.sh` hook scans file writes for credential patterns.
|
|
142
|
-
- `env-file-protection.sh` blocks reads of `.env
|
|
153
|
+
- `env-file-protection.sh` blocks reads of `.env*` files.
|
|
143
154
|
|
|
144
155
|
**Residual risk:** Secret patterns not in the catalog (custom token formats, hex-encoded credentials) will not be redacted. Encoding-based bypasses (double-URL-encoding, Unicode normalization) are partially mitigated but not comprehensively tested.
|
|
145
156
|
|
|
@@ -165,18 +176,20 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
165
176
|
|
|
166
177
|
**Mitigations:**
|
|
167
178
|
|
|
168
|
-
- `kill-switch` middleware
|
|
169
|
-
-
|
|
170
|
-
-
|
|
179
|
+
- `kill-switch` middleware issues exactly **one syscall per invocation** against HALT: `fs.open(path, O_RDONLY)`. The previous `stat → lstat → open` sequence had a TOCTOU window between the check and the read; the new implementation has none (shipped in 0.2.0, G4).
|
|
180
|
+
- **Semantic guarantee:** HALT is evaluated once per invocation at chain entry. A call that passes the check runs to completion; a call that fails it is denied. Creating `.rea/HALT` mid-flight does **not** cancel in-flight invocations — it blocks *subsequent* invocations only. This matches standard kill-switch semantics (SIGTERM after acceptance: the process continues).
|
|
181
|
+
- **Fail-closed on unknown state:** `ENOENT` → proceed; any other errno (`EACCES`, `EPERM`, `EISDIR`, `EIO`, …) → deny.
|
|
182
|
+
- Read size is bounded at `MAX_HALT_READ_BYTES = 1024`; a large HALT file cannot DoS a scan.
|
|
183
|
+
- **Observability:** `ctx.metadata.halt_decision` (`absent` / `present` / `unknown`) is recorded on every audit row, plus `ctx.metadata.halt_at_invocation` (ISO-8601 timestamp when HALT was present, else `null`).
|
|
171
184
|
- The middleware **never clears HALT**. Unfreezing is an explicit `rea unfreeze --reason "..."` CLI action.
|
|
172
185
|
|
|
173
|
-
**Residual risk:**
|
|
186
|
+
**Residual risk:** On shared filesystems with eventual consistency, readers on different hosts could observe HALT at different times. `.rea/` is a project-local directory by design, so this is theoretical for the intended deployment.
|
|
174
187
|
|
|
175
188
|
---
|
|
176
189
|
|
|
177
|
-
### 5.8 Codex Plugin Abuse
|
|
190
|
+
### 5.8 Codex Plugin Abuse and Codex-Review Gate Weakening
|
|
178
191
|
|
|
179
|
-
**Threat:** A poisoned Codex adversarial-review response contains prompt-injection content designed to make Claude take an unsafe action "per the reviewer's recommendation." A malicious actor uses `/codex-review` to launder an attack past the policy layer.
|
|
192
|
+
**Threat:** A poisoned Codex adversarial-review response contains prompt-injection content designed to make Claude take an unsafe action "per the reviewer's recommendation." A malicious actor uses `/codex-review` to launder an attack past the policy layer. Separately: a rogue agent or operator disables the Codex-review requirement to push protected-path changes without adversarial review.
|
|
180
193
|
|
|
181
194
|
**Mitigations:**
|
|
182
195
|
|
|
@@ -184,8 +197,13 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
184
197
|
- Every Codex invocation produces an audit entry with request summary, response summary, and pass/fail signal — tamper-evident via the hash chain.
|
|
185
198
|
- Codex never receives `.rea/policy.yaml` content in its prompt; Codex reviews diffs, not policy.
|
|
186
199
|
- The `codex-adversarial` agent cannot by itself modify policy, trigger writes, or bypass blocked paths — it is a review tool, not an actor.
|
|
200
|
+
- **Pluggable reviewer** (0.2.0, G11.2): when Codex is unreachable, `ClaudeSelfReviewer` is the fallback. Claude-on-Claude review is explicitly tagged `degraded: true` in the audit record so self-review is visible and countable.
|
|
201
|
+
- **Audited escape hatch** (0.2.0, G11.1): `REA_SKIP_CODEX_REVIEW=<reason>` bypasses the protected-path Codex requirement but writes a `codex.review.skipped` audit record carrying the verbatim reason, the operator's git identity, the head_sha, and the files-changed count. Fail-closed on missing `dist/audit/append.js` or missing git identity — the gate never silently disables. Skip records use `tool_name: "codex.review.skipped"` so a skip cannot satisfy a future Codex-review requirement on the same HEAD.
|
|
202
|
+
- **First-class no-Codex mode** (0.2.0, G11.4): `policy.review.codex_required: false` skips the protected-path Codex requirement entirely. In that mode `REA_SKIP_CODEX_REVIEW` becomes a no-op (skipping a review that isn't required has no meaning), and no skip record is emitted. Both `.claude/hooks/push-review-gate.sh` (Claude Code path) and `.husky/pre-push` (terminal path) honor this knob.
|
|
203
|
+
- **Availability probe** (0.2.0, G11.3): `rea serve` runs an initial `codex --version` probe on startup when `codex_required` ≠ false. A failed probe emits a single stderr warn — startup never fail-closes on a Codex miss.
|
|
204
|
+
- **Reviewer telemetry** (0.2.0, G11.5): `ClaudeSelfReviewer.review()` writes a row to `.rea/metrics.jsonl` with invocation counts, estimated tokens (chars/4), latency, and a `rate_limited` signal parsed from stderr. Payloads are NEVER stored; a unit test asserts that marker strings in inputs never appear in the metrics file.
|
|
187
205
|
|
|
188
|
-
**Residual risk:** Semantic injection in Codex responses (e.g., reviewer recommends a specific code change that is itself malicious) cannot be fully detected. Mitigation is defense-in-depth: the middleware still runs on any subsequent write that Claude attempts based on the review.
|
|
206
|
+
**Residual risk:** Semantic injection in Codex responses (e.g., reviewer recommends a specific code change that is itself malicious) cannot be fully detected. Mitigation is defense-in-depth: the middleware still runs on any subsequent write that Claude attempts based on the review. A `rea doctor` abuse signal on escape-hatch frequency (≥3 invocations per rolling 7 days) is proposed for 0.3.0.
|
|
189
207
|
|
|
190
208
|
---
|
|
191
209
|
|
|
@@ -198,10 +216,10 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
198
216
|
- `dependency-audit-gate.sh` runs `npm audit` before commits and blocks on high/critical vulnerabilities.
|
|
199
217
|
- Dependabot weekly scans for npm and github-actions.
|
|
200
218
|
- CI publish pipeline includes gitleaks secret scanning and npm publish payload validation.
|
|
201
|
-
- **npm publish uses OIDC provenance** — package identity is cryptographically bound to the GitHub Actions workflow that built it.
|
|
202
|
-
- REA's runtime dependencies are minimal: `@modelcontextprotocol/sdk`, `yaml`, `zod`.
|
|
219
|
+
- **npm publish uses OIDC provenance** — package identity is cryptographically bound to the GitHub Actions workflow that built it. Migration to OIDC trusted-publisher (retiring `NODE_AUTH_TOKEN`) is planned for 0.3.0 (G8).
|
|
220
|
+
- REA's runtime dependencies are minimal: `@anthropic-ai/sdk`, `@clack/prompts`, `@modelcontextprotocol/sdk`, `commander`, `safe-regex`, `yaml`, `zod`.
|
|
203
221
|
|
|
204
|
-
**Residual risk:** Zero-day vulnerabilities in direct or transitive dependencies. SBOM generation is planned but not yet automated.
|
|
222
|
+
**Residual risk:** Zero-day vulnerabilities in direct or transitive dependencies. SBOM generation is planned but not yet automated. The `pnpm test` suite does not exercise "package works when a consumer installs it" — a dev-only dep that's mis-imported at runtime is not caught by CI (this was the 0.2.0 → 0.2.1 issue). A post-publish tarball smoke (install tarball into scratch dir, run CLI) is proposed for 0.3.0.
|
|
205
223
|
|
|
206
224
|
---
|
|
207
225
|
|
|
@@ -214,22 +232,75 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
214
232
|
- **REA source code never uses `eval`, `Function()`, or dynamic `require`/`import()` on policy-driven input.** ESLint rules enforce this.
|
|
215
233
|
- Policy parsing is strict zod schema — unknown fields rejected, not ignored.
|
|
216
234
|
- Profile composition is a static key-merge, not a code evaluation.
|
|
235
|
+
- User-supplied redaction regex patterns are compiled via `new RegExp(...)` with `safe-regex` vetting at load and per-call worker-thread timeout enforcement at runtime — regex compilation is the only policy-driven code path, and it's bounded.
|
|
217
236
|
|
|
218
237
|
**Residual risk:** A malicious third-party middleware plugin (not currently supported) could reintroduce this risk. Plugins are out of scope for v1 by design.
|
|
219
238
|
|
|
220
239
|
---
|
|
221
240
|
|
|
241
|
+
### 5.11 Downstream Subprocess Environment Inheritance
|
|
242
|
+
|
|
243
|
+
**Threat:** `rea serve` spawns downstream MCP servers as child processes over stdio. Environment variables from the gateway process leak into the child by default (Node's `child_process.spawn` inherits `process.env` unless overridden). A malicious or compromised downstream can read anything the gateway can — `AWS_*`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GITHUB_TOKEN`, database URLs, session cookies.
|
|
244
|
+
|
|
245
|
+
**Mitigations:**
|
|
246
|
+
|
|
247
|
+
- Downstream subprocesses are launched with an explicit env object built from `registry.yaml#servers[].env` — the gateway does not pass `process.env` through wholesale.
|
|
248
|
+
- Registry schema is zod `.strict()` — typos in the env list fail at load.
|
|
249
|
+
- Operator intent to forward a specific variable (e.g., `HOME`, `PATH`) is expressed explicitly in `registry.yaml`; no "allow list by default."
|
|
250
|
+
- The `redact` middleware also scrubs values that match secret patterns — if a downstream inadvertently emits a credential in its response, it is redacted before reaching the agent.
|
|
251
|
+
|
|
252
|
+
**Residual risk:** If an operator explicitly forwards a credential-bearing variable into a downstream, that is a conscious trust decision — REA does not override it. A `rea doctor` lint that flags likely-credential variable names (`*_TOKEN`, `*_KEY`, `*_SECRET`) in registry.yaml is a candidate for 0.3.0 (G7 adjacent).
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### 5.12 Regex Denial-of-Service (ReDoS)
|
|
257
|
+
|
|
258
|
+
**Threat:** A malicious MCP payload carefully crafted to trigger catastrophic backtracking in a redaction or injection regex — hanging the gateway's event loop and denying service to all downstream tools.
|
|
259
|
+
|
|
260
|
+
**Mitigations (shipped 0.2.0, G3):**
|
|
261
|
+
|
|
262
|
+
- Every built-in pattern in `redact.ts` and `injection.ts` is statically linted via `safe-regex` at build time (`pnpm lint:regex` — chained into `pnpm lint` BEFORE eslint so a bad regex short-circuits the pipeline).
|
|
263
|
+
- Every user-supplied `redact.patterns[]` is re-vetted via `safe-regex` at policy load time; a flagged pattern rejects the entire policy load, naming the offender.
|
|
264
|
+
- Every regex call at runtime flows through `wrapRegex(pattern, {timeoutMs})` — a worker-thread timeout wrapper that blocks the parent on `Atomics.wait` over a SharedArrayBuffer and hard-`terminate()`s the worker on budget exhaustion. Default budget is 100ms; configurable via `policy.redact.match_timeout_ms`.
|
|
265
|
+
- On timeout the redaction middleware substitutes the sentinel `[REDACTED: pattern timeout]` and records `{event: "redact.regex_timeout", pattern_source, pattern_id, input_bytes, timeout_ms}` on `ctx.metadata`. The input text is NEVER written — only its byte length.
|
|
266
|
+
- The "Private Key" PEM armor pattern, flagged by `safe-regex` in the original form, was tightened to a bounded alternation that `safe-regex` accepts.
|
|
267
|
+
|
|
268
|
+
**Residual risk:** A pattern that `safe-regex` approves but that is nevertheless slow on pathological inputs could still time out frequently, effectively denying redaction for that class of input. The sentinel-replacement behavior is a fail-secure outcome (the value is redacted), but a downstream that can trigger mass timeouts can effectively delete content from reaching the agent. Detection via `.rea/metrics.jsonl` rate-limit signals is the current observability story.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### 5.13 Installer Path Trust
|
|
273
|
+
|
|
274
|
+
**Threat:** `rea init` and `rea upgrade` copy shipped artifacts from the npm package into a consumer project (`.claude/hooks/*`, `.claude/agents/*`, `.claude/commands/*`, `.husky/*`, a managed CLAUDE.md fragment). A compromised npm tarball could carry a subverted hook that runs in the consumer's context with the consumer's privileges.
|
|
275
|
+
|
|
276
|
+
**Mitigations (shipped 0.2.0, G12):**
|
|
277
|
+
|
|
278
|
+
- Shipped artifacts are listed explicitly in `package.json#files[]`; nothing outside `dist/`, `hooks/`, `agents/`, `commands/`, `.husky/`, `scripts/`, `profiles/` is in the tarball.
|
|
279
|
+
- npm publish uses `--provenance` — each published version's tarball is cryptographically bound to the exact GitHub Actions workflow run that built it (commit SHA, workflow file, runner image). A consumer can verify provenance via `npm audit signatures`.
|
|
280
|
+
- `rea init` writes `.rea/install-manifest.json` recording the SHA-256 of every shipped file on first install. Subsequent `rea upgrade` runs compare canonical (what this rea version ships) against on-disk content via that manifest, and against the consumer's previous baseline — drifted files are flagged, not silently replaced.
|
|
281
|
+
- `rea upgrade` conflict policy: `unmodified` files auto-update silently; `drifted` files prompt (`keep | overwrite | diff`); `--yes` defaults to `keep` (safe). `--force` required for overwrite.
|
|
282
|
+
- Hook scripts are chmodded `0o755` during copy; the manifest records the hash of the content, not the mode, so a tampered mode is caught by `rea doctor` (which separately checks `hook executable`).
|
|
283
|
+
- The `postinstall` hook prints a one-line stderr nudge when the installed rea version disagrees with the manifest version — silent inside CI (`CI=true`), silent when no manifest exists, silent inside the rea repo itself. It never fails the install.
|
|
284
|
+
|
|
285
|
+
**Residual risk:** A consumer that blindly accepts `rea upgrade` prompts without reviewing diff output is trusting the current rea version's maintainers transitively through npm. Mitigation depends on the provenance ecosystem maturing — the `npm audit signatures` verification is a manual step today, not a default gate.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
222
289
|
## 6. Residual Risks and Open Issues
|
|
223
290
|
|
|
224
291
|
| Risk | Severity | Tracking |
|
|
225
292
|
| ------------------------------------------------------------- | -------- | ------------------------------ |
|
|
226
|
-
| Semantic prompt injection via tool descriptions | High |
|
|
227
|
-
| Semantic injection via Codex adversarial-review responses | High | No issue filed
|
|
228
|
-
| Double-URL-encoding bypass for blocked paths | Medium | Planned fix
|
|
229
|
-
| No real-time alert on audit hash chain break | Medium |
|
|
230
|
-
|
|
|
293
|
+
| Semantic prompt injection via tool descriptions | High | 0.3.0 G9 (tier escalation) |
|
|
294
|
+
| Semantic injection via Codex adversarial-review responses | High | No issue filed (defense in depth via middleware) |
|
|
295
|
+
| Double-URL-encoding bypass for blocked paths | Medium | Planned fix |
|
|
296
|
+
| No real-time alert on audit hash chain break | Medium | 0.3.0 G1 + G5 |
|
|
297
|
+
| Concurrent audit writers can race at fsync | Medium | 0.3.0 G1 (proper-lockfile) |
|
|
298
|
+
| SBOM not automated in publish pipeline | Medium | Planned |
|
|
231
299
|
| Secret pattern gaps (custom token formats, encoding variants) | Medium | No issue filed |
|
|
232
|
-
|
|
|
300
|
+
| Post-publish tarball smoke not in CI | Medium | 0.3.0 CI hardening |
|
|
301
|
+
| Escape-hatch abuse signal not surfaced in `rea doctor` | Low | 0.3.0 (threshold: ≥3 / 7d) |
|
|
302
|
+
| Catalog drift by downstream not detected on reconnect | Medium | 0.3.0 G7 (fingerprint + drift) |
|
|
303
|
+
| OIDC trusted publisher not yet migrated (`NODE_AUTH_TOKEN` still in use) | Medium | 0.3.0 G8 |
|
|
233
304
|
| Local user can escalate policy.yaml outside gateway | Low | By design (trusted actor) |
|
|
234
305
|
|
|
235
306
|
---
|
|
@@ -238,8 +309,8 @@ Downstream MCP servers are treated as untrusted by default. Codex plugin *invoca
|
|
|
238
309
|
|
|
239
310
|
REA operates two independent layers. Bypassing one does not disable the other.
|
|
240
311
|
|
|
241
|
-
**Hook layer** (development-time):
|
|
312
|
+
**Hook layer** (development-time): 13 Claude Code hooks intercept tool calls before execution at the Claude Code level. Hooks enforce: secret scanning, dangerous command interception, blocked path enforcement, settings protection, attribution advisory, dependency audit, commit/push review gates, PR issue linking, architecture review, env file protection, changeset security gates, and security-disclosure gates.
|
|
242
313
|
|
|
243
|
-
**Gateway layer** (runtime, `rea serve`): A middleware chain processes every proxied MCP tool call. Middleware enforces: kill switch, policy/autonomy level,
|
|
314
|
+
**Gateway layer** (runtime, `rea serve`): A middleware chain processes every proxied MCP tool call. Middleware enforces: audit, kill switch, policy/autonomy level, tier classification, blocked paths, rate limit, circuit breaker, prompt injection detection, secret redaction (pre and post), and result size cap.
|
|
244
315
|
|
|
245
|
-
Both layers fail closed: on read failure, parse error, or unexpected condition, the default action is deny.
|
|
316
|
+
Both layers fail closed: on read failure, parse error, unknown errno on HALT, regex timeout, or any unexpected condition, the default action is deny (or for redaction specifically: replace with a sentinel — the content never escapes unscanned).
|
package/dist/audit/append.d.ts
CHANGED
|
@@ -15,19 +15,25 @@
|
|
|
15
15
|
* - Never throws on stat/missing-file conditions; only throws on write failure
|
|
16
16
|
* (the caller decides how to react).
|
|
17
17
|
*
|
|
18
|
-
* ## Concurrency
|
|
18
|
+
* ## Concurrency (G1)
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* Writes are serialized two ways:
|
|
21
|
+
*
|
|
22
|
+
* 1. Per-process: a module-scoped queue keyed by the canonical path
|
|
23
|
+
* preserves linear ordering within a single Node process.
|
|
24
|
+
* 2. Cross-process: each `doAppend` call is wrapped in a `proper-lockfile`
|
|
25
|
+
* lock on `.rea/`. Stale locks are reclaimed after 10s. Two processes
|
|
26
|
+
* appending concurrently serialize cleanly; the hash chain stays linear.
|
|
27
|
+
*
|
|
28
|
+
* Rotation (`maybeRotate`) runs BEFORE the append lock is taken, so a full
|
|
29
|
+
* audit file is rotated out of the way transparently. The rotation marker
|
|
30
|
+
* record preserves hash-chain continuity across the boundary.
|
|
27
31
|
*
|
|
28
32
|
* @see {@link file://./codex-event.ts} for the canonical `codex.review` shape.
|
|
33
|
+
* @see {@link file://../gateway/audit/rotator.ts} for rotation semantics.
|
|
29
34
|
*/
|
|
30
35
|
import { Tier, InvocationStatus } from '../policy/types.js';
|
|
36
|
+
import type { Policy } from '../policy/types.js';
|
|
31
37
|
import type { AuditRecord } from '../gateway/middleware/audit-types.js';
|
|
32
38
|
/**
|
|
33
39
|
* Input shape for {@link appendAuditRecord}. All fields except `tool_name`
|
|
@@ -47,6 +53,13 @@ export interface AppendAuditInput {
|
|
|
47
53
|
metadata?: Record<string, unknown>;
|
|
48
54
|
/** ISO-8601 timestamp; defaults to `new Date().toISOString()` */
|
|
49
55
|
timestamp?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Optional policy for rotation decisions. When absent, rotation is
|
|
58
|
+
* disabled (back-compat). Callers that want rotation pass the already-
|
|
59
|
+
* loaded policy; the helper does not re-read `.rea/policy.yaml` on every
|
|
60
|
+
* append — that would be a surprise cost for consumers.
|
|
61
|
+
*/
|
|
62
|
+
policy?: Policy;
|
|
50
63
|
}
|
|
51
64
|
/**
|
|
52
65
|
* Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
|
package/dist/audit/append.js
CHANGED
|
@@ -15,23 +15,28 @@
|
|
|
15
15
|
* - Never throws on stat/missing-file conditions; only throws on write failure
|
|
16
16
|
* (the caller decides how to react).
|
|
17
17
|
*
|
|
18
|
-
* ## Concurrency
|
|
18
|
+
* ## Concurrency (G1)
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
* Writes are serialized two ways:
|
|
21
|
+
*
|
|
22
|
+
* 1. Per-process: a module-scoped queue keyed by the canonical path
|
|
23
|
+
* preserves linear ordering within a single Node process.
|
|
24
|
+
* 2. Cross-process: each `doAppend` call is wrapped in a `proper-lockfile`
|
|
25
|
+
* lock on `.rea/`. Stale locks are reclaimed after 10s. Two processes
|
|
26
|
+
* appending concurrently serialize cleanly; the hash chain stays linear.
|
|
27
|
+
*
|
|
28
|
+
* Rotation (`maybeRotate`) runs BEFORE the append lock is taken, so a full
|
|
29
|
+
* audit file is rotated out of the way transparently. The rotation marker
|
|
30
|
+
* record preserves hash-chain continuity across the boundary.
|
|
27
31
|
*
|
|
28
32
|
* @see {@link file://./codex-event.ts} for the canonical `codex.review` shape.
|
|
33
|
+
* @see {@link file://../gateway/audit/rotator.ts} for rotation semantics.
|
|
29
34
|
*/
|
|
30
35
|
import fs from 'node:fs/promises';
|
|
31
36
|
import path from 'node:path';
|
|
32
|
-
import crypto from 'node:crypto';
|
|
33
37
|
import { Tier, InvocationStatus } from '../policy/types.js';
|
|
34
|
-
|
|
38
|
+
import { GENESIS_HASH, computeHash, fsyncFile, readLastRecord, withAuditLock, } from './fs.js';
|
|
39
|
+
import { maybeRotate } from '../gateway/audit/rotator.js';
|
|
35
40
|
const REA_DIR = '.rea';
|
|
36
41
|
const AUDIT_FILE = 'audit.jsonl';
|
|
37
42
|
/** Per-file write queue to preserve linear hash-chain order within a process. */
|
|
@@ -73,83 +78,43 @@ async function resolveBaseDir(baseDir) {
|
|
|
73
78
|
return absolute;
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
|
-
function computeHash(record) {
|
|
77
|
-
return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
|
|
78
|
-
}
|
|
79
|
-
async function readLastHash(auditFile) {
|
|
80
|
-
let data;
|
|
81
|
-
try {
|
|
82
|
-
data = await fs.readFile(auditFile, 'utf8');
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
if (err.code === 'ENOENT')
|
|
86
|
-
return GENESIS_HASH;
|
|
87
|
-
throw err;
|
|
88
|
-
}
|
|
89
|
-
// Walk the file backwards by newline — the last non-empty line is the tail.
|
|
90
|
-
const trimmed = data.replace(/\n+$/, '');
|
|
91
|
-
if (trimmed.length === 0)
|
|
92
|
-
return GENESIS_HASH;
|
|
93
|
-
const lastNewline = trimmed.lastIndexOf('\n');
|
|
94
|
-
const lastLine = lastNewline === -1 ? trimmed : trimmed.slice(lastNewline + 1);
|
|
95
|
-
try {
|
|
96
|
-
const parsed = JSON.parse(lastLine);
|
|
97
|
-
if (typeof parsed.hash === 'string' && parsed.hash.length === 64) {
|
|
98
|
-
return parsed.hash;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
// Corrupt tail line — fall through to genesis. The operator will see this
|
|
103
|
-
// because the chain verify tool (future) will flag the break point. We do
|
|
104
|
-
// not throw: refusing to append would mask every subsequent event.
|
|
105
|
-
}
|
|
106
|
-
return GENESIS_HASH;
|
|
107
|
-
}
|
|
108
|
-
async function fsyncFile(filePath) {
|
|
109
|
-
let fh;
|
|
110
|
-
try {
|
|
111
|
-
fh = await fs.open(filePath, 'r');
|
|
112
|
-
await fh.sync();
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// fsync failure is not fatal — durability is best-effort here; the write
|
|
116
|
-
// itself already succeeded.
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
if (fh)
|
|
120
|
-
await fh.close();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
81
|
async function doAppend(resolvedBase, input) {
|
|
124
82
|
const reaDir = path.join(resolvedBase, REA_DIR);
|
|
125
83
|
const auditFile = path.join(reaDir, AUDIT_FILE);
|
|
126
84
|
await fs.mkdir(reaDir, { recursive: true });
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
85
|
+
// Rotate BEFORE acquiring our append lock. maybeRotate takes its own lock
|
|
86
|
+
// internally and is idempotent; callers that race simply observe a fresh
|
|
87
|
+
// file with the rotation marker as their chain anchor.
|
|
88
|
+
await maybeRotate(auditFile, input.policy);
|
|
89
|
+
return withAuditLock(auditFile, async () => {
|
|
90
|
+
const { hash: prevHash } = await readLastRecord(auditFile);
|
|
91
|
+
const effectivePrev = prevHash || GENESIS_HASH;
|
|
92
|
+
const now = input.timestamp ?? new Date().toISOString();
|
|
93
|
+
const recordBase = {
|
|
94
|
+
timestamp: now,
|
|
95
|
+
session_id: input.session_id ?? 'external',
|
|
96
|
+
tool_name: input.tool_name,
|
|
97
|
+
server_name: input.server_name,
|
|
98
|
+
tier: input.tier ?? Tier.Read,
|
|
99
|
+
status: input.status ?? InvocationStatus.Allowed,
|
|
100
|
+
autonomy_level: input.autonomy_level ?? 'unknown',
|
|
101
|
+
duration_ms: input.duration_ms ?? 0,
|
|
102
|
+
prev_hash: effectivePrev,
|
|
103
|
+
};
|
|
104
|
+
if (input.error)
|
|
105
|
+
recordBase.error = input.error;
|
|
106
|
+
if (input.redacted_fields?.length)
|
|
107
|
+
recordBase.redacted_fields = input.redacted_fields;
|
|
108
|
+
if (input.metadata && Object.keys(input.metadata).length > 0) {
|
|
109
|
+
recordBase.metadata = input.metadata;
|
|
110
|
+
}
|
|
111
|
+
const hash = computeHash(recordBase);
|
|
112
|
+
const record = { ...recordBase, hash };
|
|
113
|
+
const line = JSON.stringify(record) + '\n';
|
|
114
|
+
await fs.appendFile(auditFile, line);
|
|
115
|
+
await fsyncFile(auditFile);
|
|
116
|
+
return record;
|
|
117
|
+
});
|
|
153
118
|
}
|
|
154
119
|
/**
|
|
155
120
|
* Append a structured audit record to `${baseDir}/.rea/audit.jsonl` with a
|