security: redact audit logs with env policy

Jaime Fournier <jaimef@linbsd.org>

520417d9556c19e62d40ea350d3403a35ded38a1

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 2d32502..585b2bc 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -772,6 +772,12 @@ they are build-time. Close the runtime loop.
   shipped artifact for `$HOME` path leakage.
 - **Accept:** tests: crafted failing request yields opaque ref + logged
   detail; scanner catches a synthetic leak; artifact grep clean.
+- **Status:** partially complete. `(std security audit-log)` now exposes
+  `audit-log-use-env-policy!`, workers bind their env policy to per-run audit
+  logs before recording events, and tests assert registered secret values are
+  redacted from both JSONL and human summaries. Remaining work: protocol
+  surfaces need opaque client error refs, the scanner needs a condition-message
+  HTTP-response rule, and release artifact path-leak checks still need a gate.
 
 ### K3-P1-11 — Parameterized-only SQL in the safe surface
 **Serves:** G1. **Effort:** 2–3 days.
diff --git a/docs/security-reference.md b/docs/security-reference.md
index 91a85fb..e206983 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -815,6 +815,7 @@ These modules are implemented but not covered in depth above.
 | `(std security sanitize)` | Context-aware sanitization: `sanitize-html`, `sanitize-html-attribute`, `sanitize-url-attribute`, `sql-escape`, `sanitize-path`, `safe-path-join`, `sanitize-header-value`, `sanitize-url`. Raises `&path-traversal`, `&header-injection`, `&url-scheme-violation`. |
 | `(std security errors)` | Error classification (internal vs client-safe). Generates opaque error references for correlation. Prevents leaking internal details in error responses. |
 | `(std security audit)` | Append-only JSONL audit log with SHA-256 hash chain. `audit-log!`, `verify-audit-chain`, `check-capability!/audit`. |
+| `(std security audit-log)` | Per-run structured audit records with JSONL/summary rendering, per-log redactors, and `audit-log-use-env-policy!` to scrub `(std security env)` registered secret values before emission. |
 | `(std security auth)` | API key stores, session tokens with expiry, auth middleware pattern, rate limiting for auth attempts. |
 | `(std security flow)` | Information flow control. Security levels form a lattice: public < internal < secret < top-secret. Data flows up freely; downward flow requires explicit `declassify` which is logged. |
 | `(std security metrics)` | Security counters, gauges, histograms with alerting thresholds. |
diff --git a/lib/std/security/audit-log.ss b/lib/std/security/audit-log.ss
index 3e790b3..08b2cfd 100644
--- a/lib/std/security/audit-log.ss
+++ b/lib/std/security/audit-log.ss
@@ -36,6 +36,7 @@
     audit-log-summary
     audit-log-redactor-set!
     audit-log-redactor
+    audit-log-use-env-policy!
     audit-log-now
 
     audit-event
@@ -51,7 +52,8 @@
     audit-encode-jsonl-line)
 
   (import (chezscheme)
-          (only (jerboa core) def defstruct))
+          (only (jerboa core) def defstruct)
+          (only (std security env) env-policy? env-policy-redact))
 
   (defstruct audit-log
     (records-tail records-rev redactor))
@@ -62,6 +64,15 @@
   (def (audit-log)
     (make-audit-log #f '() #f))
 
+  (def (audit-log-use-env-policy! log pol)
+    (unless (audit-log? log)
+      (error 'audit-log-use-env-policy! "expected audit-log" log))
+    (unless (env-policy? pol)
+      (error 'audit-log-use-env-policy! "expected env-policy" pol))
+    (audit-log-redactor-set! log
+      (lambda (s) (env-policy-redact pol s)))
+    log)
+
   ;; defstruct already provides `audit-log-redactor` (reader) and
   ;; `audit-log-redactor-set!` (writer); both are re-exported above.
 
@@ -273,8 +284,8 @@
       [(symbol? v) (symbol->string v)]
       [(boolean? v) (if v "true" "false")]
       [(number? v) (number->string v)]
-      [(pair? v) (format-as-string v)]
-      [else (format-as-string v)]))
+      [(pair? v) (maybe-redact (format-as-string v) redactor)]
+      [else (maybe-redact (format-as-string v) redactor)]))
 
   ;; ---------- Event constructors ----------
   ;; Lightweight (type . fields) cons; not a defstruct so we keep audit
diff --git a/lib/std/security/worker.ss b/lib/std/security/worker.ss
index 71e5bdc..e9a019f 100644
--- a/lib/std/security/worker.ss
+++ b/lib/std/security/worker.ss
@@ -48,6 +48,7 @@
                 audit-log-now
                 audit-log-record!
                 audit-log-records
+                audit-log-use-env-policy!
                 audit-process-start
                 audit-process-exit)
           (only (std security env)
@@ -281,6 +282,7 @@
              [else
               (let ([log (audit-log)]
                     [start-ms (audit-log-now)])
+                (audit-log-use-env-policy! log (worker-policy-env-policy pol))
                 (audit-log-record!
                  log
                  (audit-process-start
diff --git a/tests/test-limits-primitives.ss b/tests/test-limits-primitives.ss
index ebad6bf..e8e5e4e 100644
--- a/tests/test-limits-primitives.ss
+++ b/tests/test-limits-primitives.ss
@@ -446,6 +446,29 @@
       jsonl
       (lambda (s) (string-contains? s "process_start")))))
 
+(let ([log (audit-log)]
+      [pol (env-policy-default)])
+  (env-policy-secret! pol "DEMO_SECRET" "hunter2")
+  (audit-log-use-env-policy! log pol)
+  (audit-log-record! log
+    (audit-process-start
+      'command: '("/bin/echo" "hunter2")
+      'tag: "token=hunter2"))
+  (let ([jsonl (audit-log-jsonl-string log)]
+        [summary (audit-log-summary log)])
+    (test-pred "audit log env-policy redactor scrubs JSONL"
+      jsonl
+      (lambda (s)
+        (and (string? s)
+             (not (string-contains? s "hunter2"))
+             (string-contains? s "<redacted>"))))
+    (test-pred "audit log env-policy redactor scrubs summary"
+      summary
+      (lambda (s)
+        (and (string? s)
+             (not (string-contains? s "hunter2"))
+             (string-contains? s "<redacted>"))))))
+
 (let ([log (audit-log)])
   (audit-log-record! log
     (audit-net-event 'host: "1.2.3.4" 'port: 443 'verdict: 'denied