# Security-gate — assembled-orchestrator design (2026-06-16)

audience: AI coding agents first. Optimize for activation, not prose. Do not prettify into narrative.

## BLUF — what the gate is
ONE thin orchestrator that runs TWO validated legs over a target + its pulled-in critical imports, then merges.
The legs already work (validated, n≥3, git-pinned canonicals — see `RESULT_coverage_status.md`). The orchestrator's
ONLY non-trivial logic is: (1) scope resolution (what imports to pull in), (2) cross-file escalation, (3) k-roll
union + merge. Everything else calls a proven leg. Keep it minimal — run the deletion test before adding any layer.

## The two legs — deploy EXACTLY what was validated, never an approximation
- **Oracle leg (deterministic AST).** `bun ~/.claude/security-gate/oracle/oracle2.ts <file>` → FLAGS section. Catches
  the complete-mediation / identity-collision class ONLY (C02 buyer≡vendorOwner, C09 sibling self-vendor). The ONLY
  leg that catches C02 — no unseeded LLM config ever did.
  - **NOT pure single-file syntax.** To recognize an identity guard it RESOLVES the imported predicate's *body* from
    sibling files on disk (relative imports only; tsconfig aliases are NOT resolved). Siblings present (a file in its
    real repo) → it fires; this is the validated condition. Siblings absent (extracted file / partial checkout /
    aliased import) → the predicate is invisible and a naive run would false-clean.
  - **Degraded-input guard — no silent false-clean.** When a guard-position callee's body cannot be resolved, the
    oracle lists it under `=== UNRESOLVED IMPORTS` and prints `unresolved-imports: N>0`. `gate.py:oracle_status`
    treats SILENT-with-unresolved as **NOT a clean pass** (same discipline as the LLM "unverified cross-file
    dependency" flag) — surfaced, never read as clean. **DO NOT** read a SILENT oracle as clean without confirming
    `unresolved-imports: 0`.
- **LLM leg (stochastic).** The VALIDATED triple = **(`prompt_v2.txt` GREEN 10-pass, model=sonnet, effort=medium)**.
  Spawn via `claude -p --model sonnet --effort medium` with the v2 prompt retargeted to the in-scope file.
  - **DO NOT** deploy the stock `security-guard` agent (opus/xhigh, different general prompt) as the LLM leg — it
    was NEVER run in the catch campaign, so its catch rate on the baseline-missed set is UNMEASURED. Shipping it
    would be shipping an unproven config. Use the measured triple.
  - **Run k≥3 independent rolls and UNION the findings.** F3 (the only variance-prone cell) catches ≈3/4 per single
    roll; a single-roll leg is NOT robust. Independent samples → miss needs all k to miss: (0.25)^3 ≈ 1.6%. The
    other 5 cells are 3/3 single-roll, but the union costs nothing extra and removes the F3 hole.

## Scope resolution — the load-bearing NEW logic (the cross-file principle, proven by G3c)
A bug whose fix lives in an imported function is INVISIBLE from the importer (G3 concurrent-claim: 7 lines in
`webhook.ts`, 50 in the imported `claimWebhookEvent`). The gate MUST pull imported critical functions into scope.
Two modes, run BOTH:

1. **Proactive (import-name heuristic).** Parse the target's import statements. An imported symbol is CRITICAL if
   its name matches: `claim|lock|guard|settle|payout|refund|charge|transfer|release|mediat|authoriz|verif|reserve|
   consume|debit|credit|ledger|escrow|hold|webhook|idempoten`. Resolve its module path to a file (apply the repo's
   `@/*` tsconfig alias), add to scope. (For G3: `claimWebhookEvent` matches `claim` → pulled in proactively.)
2. **Reactive (escalation on flag).** Scan unioned LLM findings for "unverified cross-file dependency",
   "cannot be confirmed from this file", or a named imported symbol the reviewer could not verify. Resolve that
   symbol → file, add to scope, RE-RUN the legs on it. Loop until no new cross-file flag or depth cap (default 2).
   **DO NOT** treat an "unverified cross-file dependency" flag as a clean pass — it is a REQUIRED escalation.

## Pipeline (per orchestrator run)
```
scope = {target}; queue = [target]; depth = 0
proactive-pull-in(target) -> add critical imports to scope+queue
while queue not empty and depth <= cap:
  f = queue.pop()
  oracle_flags[f]   = run oracle2.ts on f
  llm_union[f]      = union( k>=3 x  claude -p --model sonnet --effort medium  < v2_prompt(f) )
  for dep in cross_file_flags(llm_union[f]):           # reactive escalation
     if resolve(dep) not in scope: scope+=, queue+=, depth bookkeeping
merge: oracle_flags ∪ llm_union across scope -> dedupe -> rank by severity -> report
```

## Form decision — a SKILL backed by a thin orchestrator SCRIPT
- **Form = skill** (`security-gate`) that the user invokes on a target; it documents the procedure and calls the
  script. Matches the project goal verbatim: "reasons through a codebase + writes correct subagent spawn prompts."
- **Script = `gate.py`** does the deterministic mechanics: import parse + alias resolve, oracle subprocess, k-roll
  `claude -p` fan-out (foreground, gate-compliant `xargs -P`/pool), union/dedupe, escalation loop, merge/report.
- Rejected: a single LLM agent doing it all inline (scope-resolution + union are mechanical → script is
  deterministic + reproducible). Rejected: the Workflow tool (needs explicit user opt-in; the script is portable
  and reproduces the exact validated `claude -p` invocation).

## Acceptance test (the build's pass/fail — from the advisor)
End-to-end from the IMPORTER, not the file where the bug lives:
- Input: pre-fix `webhook.ts` (importer). It imports `claimWebhookEvent` from `stripe-webhook-events.ts`.
- PASS iff: scope resolution pulls in `stripe-webhook-events.ts` AND the merged report contains the TOCTOU
  concurrent-claim finding (INSERT-then-SELECT-`processedAt` two-step → double-process).
- This validates the HALF that G3c did not: G3c proved "catchable when the file is in scope"; this proves "the
  orchestrator PULLS IN the right file given only the importer." Both halves are required for the deployed catch.
- Blind discipline: copy ONLY the in-scope pre-fix files into an isolated mini-repo (import resolves between them);
  run LLM-leg agents with `CLAUDE_CONFIG_DIR=/tmp/sg_cfg` (no memory/skills/ground-truth leak); subjects contain no
  answer. The orchestrator (me) may read git/ground-truth for JUDGING; the catch-test agents may not.

## ACCEPTANCE TEST — PASSED (2026-06-17, `gate_acc_report.md`)
Ran `gate.py` on ONLY the importer (`webhook.ts`), blind mini-repo `/tmp/gate_acc`, clean config `/tmp/sg_cfg`.
- **Scope resolution fired:** proactively pulled in `stripe-webhook-events.ts` (depth 1) — critical import
  `claimWebhookEvent`. (stderr: `[scope] …stripe-webhook-events.ts (depth 1; proactive: critical import claimWebhookEvent)`.)
- **Canonical surfaced in merged report** under the pulled-in file, ranked CRITICAL:
  `[critical] Race condition: two concurrent workers both receive shouldProcess: true (2/3 rolls)` +
  `[critical] Concurrent double-processing: TOCTOU race between claimWebhookEvent insert and processedAt check (1/3 rolls)`.
- **Both pull-in paths confirmed:** webhook.ts's own review ALSO emitted the reactive signal
  (`Atomicity of claimWebhookEvent is unverifiable…`, 1/3) — proactive heuristic got there first; reactive is backup.
- **Result:** the half G3c could not test (does the orchestrator PULL IN the right file from the importer?) is now
  proven. Deployed end-to-end catch of concurrent-claim = CONFIRMED.

### Known precision limits (step-2, recorded — not silently ignored)
- Parser FP: numbered Fix sub-items mis-read as findings → FIXED (drop unrated imperative-mood items; `IMPERATIVE`
  guard in `gate.py`). PASS was unaffected (canonical was always surfaced) → not re-run for 2 cosmetic lines.
- Semantic dedup gap: two paraphrases of the SAME bug (report L11/L12) were not merged (word-overlap threshold).
  Real precision cost (~10 findings : ~1 canonical). Step-2 work: embed-similarity or an LLM merge pass. Recorded.

## Deletion test (every layer earns its place)
- scope-resolution: delete → G3 concurrent-claim missed (invisible from importer). KEEP.
- k-roll union: delete → F3 flaky ~3/4. KEEP.
- oracle leg: delete → C02 missed (no LLM config catches it). KEEP.
- escalation loop: delete → cross-file dep one hop past a non-heuristic-named import missed. KEEP.
- merge/dedupe: delete → no single ranked report; duplicate findings across rolls/files. KEEP.
All layers pass — minimal correct wiring, no decorative seams.

## Honest scope (carry from RESULT_coverage_status.md — do not overclaim)
- "Catches the baseline-missed set in FILE/DIFF review" (matches deployment mode) — NOT "in a full-repo audit".
- Validated leg is payments/financial (the v2 prompt is payments-specific + the corpus is multideal). A general
  ALL-projects gate needs the LLM leg's prompt generalized + RE-VALIDATED on non-payments code — that is step-2,
  explicitly not yet done. State this; do not imply general coverage from payments validation.
- Precision/noise (~10 findings : ~1 canonical per roll) is the standing-gate concern for step-2, not step-1.
