---
title: Security Hardening Wave — Root-Cause Remediation
date: 2026-07-02
type: implementation-plan
status: draft
source_audit: wiki/Research/Audit-Reports-2026-07-01/security-assessment.md
target_release: v0.14 "Trustworthy Execution" (pre-Show-HN gate)
owner: main-thread + package wardens
---

# Security Hardening Wave — Root-Cause Remediation

## 1. Context & goal

The 2026-07-01 root-cause security assessment scored the framework **3/10** and found a **trust-boundary inversion**: external systems (SQL, npm, forks) are treated as hostile — correctly — but the framework's *own inputs* (model output, tool results, recalled memory, request bodies, config metadata) flow into privileged sinks (host shell, system prompt, agent execution, disk) with **no runtime enforcement point**. Security metadata the API advertises (`requiresApproval`, `riskLevel`, `.withVerification()`, guardrails, AgentCard `securitySchemes`, the memory `verified` column) is **decorative** — declared and ignored at runtime, which is worse than absent because it manufactures false assurance.

**All 28 findings were re-verified against current `main` (2026-07-02)** before this plan. Line refs and status below reflect the live code, not the draft audit.

**Goal:** take the framework from *insecure-by-default with confirmed Critical RCE* to *secure-by-default with least-privilege execution* **without a ground-up rewrite** — the hardened primitives already exist (Docker sandbox, parameterized SQL, constant-time HMAC). This must land **before Show-HN**: an HN audience will find `0.0.0.0` binds, unsigned-cert-auth, and `sh -c` denylist-bypass within hours, and "the honest harness" shipping dishonest security metadata is a direct brand contradiction.

**Framing (fits `feedback_substance_over_artifacts`):** this is not a marketing artifact — it is harness trustworthiness, which *is* the product. Every fix is a real enforcement point, verified by a red exploit test.

## 2. Verification summary (against current main)

| ID | Finding | Severity | Verified status | Exposure |
|---|---|---|---|---|
| F1 | shell-execute `sh -c` RCE + arbitrary FS (denylist bypass: `<(…)`, quoted/relative paths, `awk print\|"cmd"`, `tee`) | Critical | **CONFIRMED** `shell-execution.ts:857` | **Active — default path** |
| F2 | `requiresApproval`/`riskLevel` inert; `ToolAuthorizationError` never thrown | High (multiplier) | **CONFIRMED** `tool-service.ts:319-409`, comment `builder/types.ts:406` | **Active — default path** |
| F3 | tool output → memory (`verified:false`) → replayed into system prompt (no `WHERE verified=1` on read) | Critical | **CONFIRMED** write `act/tool-execution.ts:119-121`, read `semantic-memory.ts:185/207`, `search.ts:59-71`, inject `reactive.ts:169`/`direct.ts:114` | **Active — default when memory on** |
| F4 | A2A / `rax serve` / judge bind `0.0.0.0`, no auth, run attacker prompts on operator creds | Critical | **CONFIRMED** `runtime-shim/serve.ts:19`; callers omit hostname | **Active — when a server is started** |
| F5 | cert-auth accepts unsigned certs + self-included key (no trust anchor) | Critical | **CONFIRMED** `certificate-auth.ts:95`; `types.ts:28` optional | **Dormant** — identity pkg not wired to any live request path |
| F6 | `http-get` model-controlled SSRF, no egress guard | High | CONFIRMED (per audit) `http-client.ts:53-56` | **Active — default tool** |
| F7 | `code-execute` (`bun run` on host) + code-action `new Function` in worker mislabeled "sandbox"; Docker sandbox opt-in | High | **CONFIRMED** `code-execution.ts:99-111`, `sandbox-worker.ts:62-65`, `builtin.ts:165` | **Active — default path** |
| F8 | full prompts / completions / tool args persisted to disk **unredacted, tracing on by default** | High (Crit if synced) | **CONFIRMED** `helpers.ts:48-54`, `trace/events.ts:242-255`; trace imports no redactor | **Active — default on** |
| F9 | skill XML breakout (`</skill_content>` in body) + skill-evolution promotes injected text to `trusted` | High | **CONFIRMED** `activate-skill.ts:37-39`; evolution `memory/…/skill-evolution.ts:166-207` | **Active** |
| F10 | verification/guardrails compute verdicts, never block; `checkOutput` dead; input-only; fail-open | High | **PARTIAL** — `checkOutput` dead-confirmed; `checkToolCall`/`checkIteration` **live** (audit overstated "all dead") | **Active — false assurance** |
| F11 | webhook secret optional (secret-less accepts any POST); `senderId` from body JSON | High | **CONFIRMED** `webhook-service.ts:79`, `channels/…/webhook.ts:68,147` | **Dormant** — nothing serves `handleRequest` over HTTP |
| F12 | MCP subprocess inherits full `process.env` (all provider keys) | Medium | **CONFIRMED** `mcp-client.ts:304/386/414` | **Active — when MCP used** |
| F13–F28 | (rate-limit no-op exports, body caps, config-SSRF, health leak, CI token scope, unpinned actions, tracked `.env`, `&` timeout escape, etc.) | Med/Low | per-audit, not re-traced here | mixed |

**Operational (do first, independent of code):** `apps/advocate/.env` holds live-format `TAVILY_API_KEY` + a GitHub PAT that crossed the audit trust boundary — **rotate both, move to a secret manager.**

## 3. Design principles (bind every fix to these)

1. **Fix root causes, not findings.** The audit's own math: four root causes collapse the majority of findings. This plan is organized by root cause, not by F-number.
2. **Fail-closed, not fail-open.** Every new enforcement point denies on missing-policy / detector-error / absent-signature. No "proceeding anyway."
3. **Secure-by-default with an explicit unsafe escape hatch.** Default path is safe; power users opt into risk via a named, greppable flag (`.withUnsafeHostExecution()`, `hostname:"0.0.0.0"` explicit, `.withTraceContent()`). Never the reverse.
4. **Exploit-first TDD.** The audit ships live exploit strings. Each becomes a **failing test** (red) before the fix, kept as a regression guard. Follow `agent-tdd` + `superpowers:test-driven-development`.
5. **Delete dead security theater, don't maintain it.** `checkOutput`, `ToolAuthorizationError` (unthrown), the ignored `verified` column, unenforced `securitySchemes` — either wire to an enforcement point or delete. No maintained-but-non-functional controls.
6. **Clean types, no `any`.** Model trust in the type system where it eliminates a class (`Secret<T>`, `Untrusted<T>`) per `feedback_clean_types`.
7. **Package-isolated bundles → wardens.** Each workstream is scoped to one package and dispatched to its warden with a MissionBrief; cross-package helpers land in the lowest shared package.
8. **Ablation only where behavior/perf changes on the hot path.** Security default-flips are correctness, not lift — they bypass the `≥3pp lift` gate, but any that touch prompt assembly (F3 fence) or add per-call work (egress guard, redaction) get a token-overhead measurement so we don't silently regress the cost story.

## 4. The four root causes → four helpers

The entire wave reduces to introducing four secure-by-default primitives and routing existing sinks through them:

| Root cause | Helper / primitive | Eliminates |
|---|---|---|
| **RC1** — untrusted code/commands reach the host | `Executor` interface; Docker sandbox as the only production impl; `execFileInSandbox(argv, sandboxDir)` interim | F1, F7, F25 — neutralizes F2 blast radius |
| **RC2** — advertised security metadata has no enforcement point | Fail-closed gates: approval in `ToolService.execute`, `verified` on memory read + trust-fence envelope, verification/guardrail `onReject`, cert signature+trust-anchor | F2, F3, F5, F9, F10, F20 |
| **RC3** — no ingress auth / no egress guard | `secureServe()` (loopback default + token + body cap) and `assertPublicUrl()` (SSRF guard) middleware | F4, F6, F11, F14, F15, F16 |
| **RC4** — no secret-redaction boundary | `redactSensitive()` at trace/OTel/Cortex boundaries; `Secret<T>` wrapper; content-capture opt-in | F8, F12 (secret leg), F27 |

## 5. Workstreams & bundles

Sequenced **active-default-path first**, dormant last. Each bundle: scope → approach → red tests → breaking-change/escape-hatch → warden.

### Phase 0 — Immediate, no code (do today)
- **B0.1** Rotate `apps/advocate` Tavily key + GitHub PAT; move to a secret manager / untracked `.env` with `.env.example`. Verify old creds revoked.
- **B0.2** Untrack `apps/stackblitz/*/.env` (F28); add `gitleaks` pre-commit + CI secret scan (F28 defense).

### Phase 1 — RC1: mandatory execution isolation (F1, F7, F25) — `tools-warden` + `kernel-warden`
- **B1.1 `Executor` interface + Docker-as-default.** Define one `Executor` interface in `packages/tools/src/execution/`; production impl = the existing hardened `docker-sandbox.ts` (`cap_drop ALL`, `--network none`, read-only rootfs, seccomp, uid 65534). `shell-execute`, `code-execute`, and code-action's worker all depend on `Executor`, never on `sh -c`/`bun run`/`new Function` directly.
- **B1.2 Interim `execFileInSandbox(argv, sandboxDir)`** for environments without Docker: parse to `execFile(binary, argv)` with **no shell** (reuse the pattern already in `skills/cli/cli-runner.ts`), canonicalize every path arg with `realpath` and assert `startsWith(sandboxDir)`, reject quotes/substitution/`..` **structurally**. **Delete `DEFAULT_BLOCKED_RULES` denylist entirely** — an enumerated blocklist over a full shell cannot be complete (denylist debt).
- **B1.3** code-action worker: stop calling `new Function` the "isolation boundary" — route through `Executor`; if Docker unavailable and no opt-in, **refuse** (fail-closed). Gate `DockerSandboxConfig.network:"host"` behind an explicit `.withUnsafeSandboxNetwork()` flag.
- **Red tests:** every F1 exploit string (`cat <(id)`, `cat "/etc/passwd"`, `cat ../../../../etc/passwd`, `awk 'BEGIN{print "id"|"/bin/sh"}'`, `tee ../../.bashrc`, `$GH_CONFIG_DIR` expansion) must be **denied**; a benign `ls`/`git status` must still pass.
- **Breaking change:** users on the default host `sh -c` path who lack Docker now get `execFile` (no shell features) or a clear "enable Docker or `.withUnsafeHostExecution()`" error. Document in CHANGELOG as a **security-breaking** default flip. Escape hatch: `.withUnsafeHostExecution()` (named, greppable, warns loudly).

### Phase 2 — RC2: fail-closed enforcement points
- **B2.1 Approval in `ToolService.execute` (F2)** — `tools-warden`. Insert an authorization step: if `definition.requiresApproval` and no approval token/policy present → **throw `ToolAuthorizationError`** (finally give the declared error a throw site). Auto-feed per-tool `requiresApproval` flags into the kernel approval policy at config assembly (delete the `builder/types.ts:406` "fast-follow" debt). Assign MCP tool risk from declared capability, not the hardcoded `requiresApproval:false` constant. **Red test:** `shell-execute`/`code-execute`/`file-write` invoked with no approval policy → `ToolAuthorizationError`; with `.withApprovalPolicy` → gated as today.
- **B2.2 Trust-fenced memory (F3, F9, F20)** — `memory-warden` + `kernel-warden`. (a) Enforce `verified`/provenance on read — add the predicate to `semantic-memory.ts` (`listByAgent` :185, `generateMarkdown` :207) and `search.ts` (:59-71). (b) Render **all** recalled/tool-derived content inside a fenced untrusted-data envelope placed in a **user/context turn, never system** — fix the wrap at `reactive.ts:169` / `direct.ts:114` (audit's `prompt-sections-default.ts` ref was stale; the wrap lives in the strategy files). (c) XML/attribute-escape all fields in `buildSkillContentXml` (`activate-skill.ts:37-39`); exclude externally-derived text from `skill-evolution.ts` input; gate `tentative→trusted` promotion behind review. **Ablation note:** the fence changes prompt assembly — measure token overhead + a small quality check across ≥2 tiers so we don't regress the cost/quality story. **Red test:** a stored `</skill_content>Ignore prior instructions` or `verified:false` poison string does not appear in a system turn on the next run.
- **B2.3 Enforcement-point verification/guardrails (F10)** — `runtime-warden`. Add `onReject: block | annotate | proceed` (default `block` when `.withVerification`/guardrails explicitly enabled); wire `checkOutput` into the output path (or **delete it** if we choose not to enforce output-side — no dead theater); fail-**closed** on detector error (`guardrail.ts:32-40` currently catches to `passed:true`); pass tool/retrieved content through detectors, not input-only. Keep `checkToolCall`/`checkIteration` (live). **Red test:** a rejected response with `onReject:block` does not return the answer.
- **B2.4 Cert-auth trust anchor (F5)** — `provider-warden`/identity. Reject unsigned certs (remove the `if (cert.signature)` fall-through at `certificate-auth.ts:95`; make `signature` required in `types.ts:28`); verify against a CA/trust-anchor key, not `cert.publicKey`; validate `issuer` + `fingerprint == SHA-256(publicKey)`; remove the `development` escape hatch from the verify path. **Dormant** (identity not wired to live path) → lower urgency, but cheap and eliminates a false boundary; fix now so it's safe *when* wired. **Red test:** unsigned cert and self-signed `agentId:"orchestrator"` both → `{authenticated:false}`.

### Phase 3 — RC3: ingress auth + egress guard
- **B3.1 `secureServe()` (F4, F14, F16)** — new shared helper in `packages/runtime-shim/`. Default `hostname:"127.0.0.1"`; mandatory bearer/token gate; body-size cap enforced **before** parse/HMAC; per-IP throttle; LRU/TTL task eviction. Route A2A (`a2a/…/http-server.ts`), `rax serve` (`apps/cli/…/serve.ts`), judge (`judge-server/src/index.ts`), health (`health/src/service.ts`) through it. **Breaking:** servers no longer reachable off-host without explicit `hostname:"0.0.0.0"` + a configured token. **Red test:** unauthenticated POST to each server → 401; oversized body → 413 before any agent runs.
- **B3.2 `assertPublicUrl()` (F6, F15)** — shared egress guard. Scheme allowlist; block loopback / link-local / RFC-1918 / `metadata.google.internal` / `169.254.169.254`; re-validate each redirect hop via `redirect:"manual"`. Apply at **every** `fetch` site: `http-get` (`http-client.ts:53-56`), remote-agent tools, A2A discovery, MCP health probe, custom pricing URL. Default `http-get` to approval or host-allowlist. (Managed providers Anthropic/OpenAI/Gemini take no config `baseURL` — **not** SSRF-exposed, verified; don't touch them.) **Red test:** `http-get http://169.254.169.254/…` and `http://10.0.0.1/admin` → refused; a public URL → allowed.
- **B3.3 Webhook fail-closed (F11)** — `channels`/`gateway`. `requireSignature:true` default for network-exposed adapters; derive identity from transport-verified fields / per-sender tokens, not body `senderId`; add replay protection (timestamp window + delivery-id/nonce TTL). **Dormant** → fix alongside B3.1 since it's the same threat model; ship the guard before anything serves `handleRequest`.

### Phase 4 — RC4: redaction boundary (F8, F12, F27) — `runtime-warden` + provider/observe
- **B4.1 `redactSensitive()`** — one redactor (key regexes + PII + `Authorization`-context scrub) invoked in `trace/normalize`, `observe/tracer` `setAttribute`, and `cortex-reporter`. The existing `observability/…/default-patterns.ts` is wired only to the structured logger — back-propagate it to the three content surfaces.
- **B4.2 Flip trace content-capture to opt-in** (`helpers.ts:48-54`): default records run *metadata* (timings, token counts, tool names) but **not** prompt/completion/arg *content*; `.withTraceContent()` opts into full capture. Preserves the "record once, debug forever" flight-recorder value for its intended single-user case while removing the default at-rest liability. **Ablation/DX note:** confirm rax-diagnose still root-causes with metadata-only traces; if it needs content, scope content to a local-only opt-in default rather than fully off.
- **B4.3 `Secret<T>` wrapper** — non-enumerable `toString/toJSON → [redacted]` so key material can't be serialized by accident; wrap provider keys. **B4.4 MCP env allowlist (F12):** default MCP subprocess env to `PATH` + declared vars; explicit opt-in to forward specific secrets; treat MCP `notifications/message` fields as untrusted. **Red test:** a trace file after a run with a key in the prompt contains `[redacted]`, not the key; an MCP subprocess `env` has no `ANTHROPIC_API_KEY` unless forwarded.

### Phase 5 — Defense-in-depth / CI supply chain (F17–F24, F26)
Lower priority, parallelizable, no default-path exploit: bind CI `version` input to `env:` before regex use (F17); scope `NPM_TOKEN` to the publish step (F18); add `--provenance` or drop `id-token:write` (F19); SHA-pin third-party actions + `permissions: contents: read` (F21–F22); digest-pin Docker base images + verify build-time downloads + non-root (F23–F24); durable audit log actually invoked on tool-exec/auth/approval (F26). Route through `release-warden` for the CI items.

## 6. Sequencing rationale

- **Phase 1 + 2.1 + 2.2 + 3.1 are the pre-Show-HN gate** — they close every *active default-path* Critical (F1, F3, F4, F7) and the multiplier (F2). Ship these before the launch post.
- **Dormant items (F5, F11)** are real code but unreachable from the network today; fixed within their phase because they're cheap and prevent shipping a false boundary, but they don't block launch.
- **Phase 4** removes the at-rest liability that grows the longer default-on tracing runs in the field — high value, medium urgency.
- **Phase 5** is hygiene; parallel to everything, gated only by `release-warden` availability.

## 7. Backward-compat & versioning

Secure-by-default flips are **breaking**. Recommend cutting this as **v0.14 "Trustworthy Execution"** (or a `0.13.x` security series if we want it out before the Compounding work) with a clear migration section:
- Host exec now requires Docker or `.withUnsafeHostExecution()`.
- Servers bind loopback + require a token; set `hostname` + token to restore `0.0.0.0`.
- Trace content capture is opt-in via `.withTraceContent()`.
- `requiresApproval:true` tools now actually block without an approval policy.

Each flip gets a named, greppable escape hatch and a CHANGELOG **BREAKING (security)** entry. This is the honest move and a launch asset: *"v0.14 makes every advertised security control an enforcement point."*

## 8. Test & verification strategy

- **Exploit-corpus regression suite** — one `packages/tools/test/security/` (and per-package equivalents) file per finding, seeded with the audit's live exploit strings, red-first per `agent-tdd`.
- **Enforcement-point unit tests** — each gate tested for the fail-closed path (missing policy, detector error, absent signature, oversized body).
- **`bun test` + build + typecheck** green before any commit; CI has no keys and no Ollama (`feedback_ci_parity_no_keys_no_ollama`) — structural gate tests use the `test` provider; live-exec tests `skipIf` Docker/Ollama unreachable.
- **Ablation** for B2.2 (prompt fence) and B3.2/B4 (per-call redaction/egress) — token-overhead + small cross-tier quality check so security doesn't silently regress the cost story.
- **Post-fix re-verification** — re-run the three domain verifier agents against the exploit corpus; every CONFIRMED must flip to DENIED.

## 9. Do-not-break / non-goals

- **Don't** delete the flight recorder value — metadata traces stay on; only *content* capture flips to opt-in.
- **Don't** touch managed-provider SDKs for SSRF (verified not exposed).
- **Don't** build the heavy Phase-3-architecture items now (capability-grant type system, `Untrusted<T>` phantom types, single ingress/egress middleware framework, tamper-evident audit spine) — those are the *long-term* refactor (audit Phase 3); this wave lands the four helpers + enforcement points, which is the 80% at 20% cost. Revisit typed-trust after the wave ships and proves the boundaries.
- **Hold** the framework's existing do-not-build lines (orchestration substrate, Memory v2 CAS, LATS/GoT) — unaffected by this wave.
- **Keep** `checkToolCall`/`checkIteration` (live behavioral contracts) — only `checkOutput` is dead and up for wire-or-delete.

## 10. Open decisions (recommend, don't block)

1. **Release vehicle** — dedicated `0.13.x` security patch (fastest to Show-HN) vs fold into `v0.14`. **Recommend:** cut `v0.14 "Trustworthy Execution"` because the default flips are breaking and deserve a minor bump + migration notes; slot it *before* the Compounding work since launch depends on it.
2. **`checkOutput`** — wire as an enforcement point vs delete. **Recommend:** wire it (output-side PII/secret scan is real value once redaction exists), fail-closed.
3. **Docker-hard-requirement vs execFile-interim as the floor** — **Recommend:** ship both; `execFile`-no-shell is the portable floor, Docker is the default when present, host `sh -c` is deleted.

---

**Next action:** Phase 0 credential rotation (independent of code) + start Phase 1 B1.1/B1.2 as a `tools-warden` bundle with an exploit-first red suite. Dispatch B2.x and B3.x in parallel once B1 lands the `Executor` seam.

---

## Progress log

### 2026-07-02 — first execution session (branch `security-hardening-wave`, 6 commits)

**Shipped (exploit-first TDD, all green):**
- **Phase 0** — untracked placeholder `apps/stackblitz/*/.env` (F28). ⚠️ **Still owed (manual, user):** rotate the `apps/advocate` Tavily key + GitHub PAT — code cannot do this.
- **F1a** (`fix(tools)` `13110108`) — structural shell-execute input hardening: quote-aware rejection of *all* shell expansion/substitution (`$VAR`, `${}`, `$(...)`, backticks, `<(...)`/`>(...)`) + uniform per-token path canonicalization vs the sandbox (closes quoted-absolute + bare-relative traversal + prefix-match bug) + broadened awk getline/print-pipe rules. Exploit corpus of every confirmed bypass now denied; legit commands (globs, redirects, jq pipes) preserved.
- **F9 + F12** (`fix(tools)` `0a00308b`) — `buildSkillContentXml` attribute-escape + wrapper-tag neutralization (skill breakout closed); `buildMcpSubprocessEnv` allowlist so MCP subprocesses no longer inherit provider keys (opt-in via explicit `env`).
- **F4** (`feat(security)` `282bec3a`) — `secureServe()` in runtime-shim: loopback default, fail-closed refusal to bind non-loopback without a token, bearer auth (constant-time), body-size cap before handler. Wired into A2A, `rax serve`, judge, health; host/token via `RA_{A2A,SERVE,JUDGE,HEALTH}_{HOST,TOKEN}`.
- **F6** (`feat(security)` `3baef618`) — `assertPublicUrl()` SSRF egress guard (blocks loopback/link-local/private/CGNAT/metadata by IP, hostname, and DNS resolution incl. rebinding; injectable resolver). Wired into `http-get` with per-redirect-hop re-validation; `RA_HTTP_ALLOW_PRIVATE=1` opt-in. **RC3 (ingress+egress middleware) now complete.**

**Deviations from plan (flagged):**
- **F1a is the input-policy layer, not "delete the denylist."** Deleting the denylist + execFile-only breaks legitimate shell features (globs, pipes, redirects) that have no execFile equivalent; the true root containment is **F1b Docker-mandatory substrate** (keep `sh -c` inside a hardened container) which needs Docker and cannot be verified in this CI env. F1a closes every *confirmed* live vector as defense-in-depth; F1b remains the architectural follow-up.
- awk/interpreter-internal escapes are handled by denylist rules (defense-in-depth), not structurally — F1b containment is the real fix.

### 2026-07-02 — second execution session (3 more commits, 9 total)

- **F8a** (`fix(trace)` `redact secrets at the trace disk-write boundary`) — `applyRedactors(defaultRedactors)` applied to serialized JSONL in the recorder flush before `appendFile`; in-memory snapshot (rax-diagnose/replay) keeps full fidelity. Added a bearer-token pattern. Follow-up: OTel/cortex-reporter boundaries, content-capture opt-in, `Secret<T>`.
- **F5** (`fix(identity)` cert-auth) — reject unsigned certs; anchor trust in the issued-cert store (foreign/self-signed rejected); fingerprint binding + signature verify against the trusted key. (Dormant finding, now safe when wired.)
- **F3** (`fix(reasoning)` memory fence) — `fenceRecalledMemory()` wraps recalled memory in a `<retrieved_memory>` untrusted-data envelope with a guard note + breakout neutralization; wired into reactive + direct strategies. Follow-up: verified-aware retrieval + move recalled content out of the system turn (needs ablation).

**Session tally:** 9 bundles green (Phase 0, F1a, F9, F12, F4, F6, F8a, F5, F3). Full-repo typecheck 69/69. Two Criticals (F1 vectors, F4) + the memory-injection Critical (F3, fence layer) + F5/F6/F8/F9/F12 closed.

### 2026-07-02 — third execution session (2 more commits, 12 total)

- **F2** (`fix(runtime)` approval auto-feed) — folds per-tool `requiresApproval` flags into the approval policy at config assembly (`foldApprovalRequiredTools` in `approval-autofeed.ts`, wired at `runtime-construction.ts`). Registered `requiresApproval:true` tools (built-ins always; terminal `shell-execute` when enabled; custom) are now gated without hand-listing. **Non-breaking scope:** runs only when `.withApprovalPolicy` is set. Updated the stale `builder/types.ts` doc. Full runtime 1116 pass.
- **F11** (`fix(gateway)` webhook fail-closed) — `requireSignature` (default true): a secretless route is refused (401) unless explicitly opted out. Threaded through `registerAdapter` + `WebhookConfig`. (Dormant finding; false boundary removed.) Gateway 123 pass.

**Session tally: 11 security bundles shipped, all green** (Phase 0, F1a, F9, F12, F4, F6, F8a, F5, F3, F2, F11). Full-repo typecheck 69/69. Every **active** Critical (F1 vectors, F3 memory-injection, F4 unauth servers) + the approval **multiplier** (F2) + Highs (F6, F8, F9, F10-partial-n/a, F12) + the two dormant Criticals (F5, F11) closed.

### 2026-07-02 — fourth execution session (1 commit, 14 total)

- **F8-remainder** (RC4 complete) — sync `redactSecrets()` added; applied at the OTel tracer (`observe/tracer.ts` tool params/output) and the Cortex WS reporter (`cortex-reporter.ts` payload). Secrets no longer leave the machine via telemetry. `observe` now depends on `observability` (acyclic).
- **F15** — `assertPublicUrl` wired into the remaining config/peer fetch sites: remote-agent A2A calls + A2A discovery. Refined the guard so **cloud-metadata/link-local is always blocked even under `allowPrivate`** (`isMetadataOrLinkLocalIp`); operator-configured agent peers allow loopback/RFC-1918 by default (`RA_AGENT_STRICT_EGRESS=1` locks down).
- **F25** — quote-aware scanner rejects bare `&` backgrounding (mid-string + trailing) while preserving `&&` chaining and literal `&` in single quotes.

**Session tally: 12 security bundles shipped** (adds F8-remainder, F15, F25 to the 11). Full runtime 1120 pass, tools 882, typecheck clean.

### 2026-07-02 — fifth execution session (1 commit, 15 total)

- **F1b — opt-in Docker isolation** (reframed per user: Docker is NOT a hard dep — host path stays default; Docker offered for added isolation). `docker-sandbox.executeShell()` added (shared hardened run-args builder); shell commands run in a fully-isolated container with a writable ephemeral **tmpfs `/work`** (mode 1777, **no host bind mount** — works on every Docker setup incl. Docker Desktop path-sharing), `--network none`, read-only rootfs, cap-drop ALL, non-root, seccomp. Opt in via `shell-execute` `sandbox: "docker"` (`.withTools({ terminal: { sandbox: "docker" } })`) or `RA_SANDBOX=docker` (global, also flips `code-execute`). F1a filters run first (defense-in-depth); fails closed if Docker unavailable. **Verified against a real Docker daemon** (runs, tmpfs writable, network blocked, no host FS, filters fire, code returns 42). Files ephemeral (isolation over continuity); shell image overridable for git/jq/gh.

**Session tally: 13 security bundles shipped.** Full tools 889 pass, real-container tests green.

### 2026-07-02 — sixth execution session (1 commit, 16 total) — F10 + branch merge

- **F10 (verification half)** — `.withVerification()` was pure telemetry ("proceeding anyway"). Added an `onReject` policy (`"proceed"` default | `"annotate"` | `"block"`) wired to a **real enforcement point**: the quality gate sets a `ctx.metadata` flag; result assembly reads it via `applyVerificationOutcome` (pure, tested) → `block` withholds the answer + `success:false`; `annotate` prepends a warning; `proceed` unchanged. Threaded `VerificationOptions.onReject` → `RuntimeOptions` → `ReactiveAgentsConfig`. **Verified end-to-end** through the real engine (mock verifier always rejects): block/annotate/proceed all behave correctly. Full runtime 1126 pass. Non-breaking (default proceed).

**Session tally: 14 security bundles shipped.** Branch `security-hardening-wave` → merged to local `main` (per merge-to-local-main-then-tag workflow; unpushed).

**Remaining (each has a real blocker):**
1. **F10 guardrail half** — wire `GuardrailService.checkOutput`, fail-closed detectors, scan tool/recalled content. Guardrails off-by-default → lower urgency; separate follow-up from the verification half (shipped).
2. **Phase 5** — CI supply-chain (F17–F24). Verifiable only by running CI; `publish.yml` release-sensitive. Safe additive parts (SHA-pin actions, `permissions:`, `--provenance`) when a CI dry-run is available.
3. **F8 tail** — trace content-capture opt-in, `Secret<T>` wrapper (redaction boundaries all done).
4. **F3 remainder** — verified-aware retrieval + move recalled content out of the system turn (needs cross-tier ablation).
5. **Smaller:** F13, F14-tail, F26, F27.

**Still owed (manual, user):** rotate the `apps/advocate` Tavily key + GitHub PAT.
