docs(spec) access-control-prover: abstract-interpretation amendment -- evidence algebra, stable symbols, label-blind acquisition, admission discipline

This commit is contained in:
Marius Ciepluch
2026-10-29 13:31:40 +01:00
parent 152f405bcf
commit 0d2b4cf15d
6 changed files with 1007 additions and 991 deletions
+360 -416
View File
@@ -1,474 +1,418 @@
# Design Document # Design Document
## Overview ## Overview
**Purpose**: a deterministic prover for the access-control family: given a change checked out at head, find The access-control prover remains a deterministic rule arm over the existing obligation checker. The amendment
every protected operation reachable from an entry point without a check dominating the path, and say what is changes the semantic contract: resolution, resource matching, authority matching and dominance return abstract
unchecked. **Users**: the maintainer, scoring it as an arm on the training pool; the pre-push hook and the proof relations rather than booleans, and every examined path receives one of three judgments: `unguarded`,
arm, consuming its findings. **Impact**: adds Java and framework entry recognition to the mapper, receiver- and signature-precise call `discharged`, or `indeterminate`. A degraded instrument may reduce precision to `indeterminate`; it cannot
resolution in the obligations package `[2026-10-29]`, a path builder and enforcement witnesses in the manufacture either final verdict.
obligations package, a prover over the hunter's per-change context, and a runner that scores it with the
valid-benchmark metrics. Target knowledge remains outside the engine. Entry shapes live in registered recognition strategies, while
operations, dischargers, resource rules and authority implications live in promoted fact data. A label-blind
initialization job produces that knowledge, and evaluation freezes its digest before admission labels are opened.
### Goals ### Goals
- A finding names an entry point, the hops, the operation and the missing check; nothing else is a finding. - Produce an unguarded finding only from a complete, sufficiently resolved evidence path.
- Scored on the pool: recall on 23 fixes and 93 locations, false alarms per 100 ordinary pushes, seconds per change. - Make call, path, resource, authority and control-flow uncertainty explicit and monotone.
- Resolution precision `[2026-10-29]`: a guard discharges only the operation whose resource it binds, and a - Carry target knowledge as verified data or recogniser strategies, never engine branches.
call resolves to the class and signature it targets; an operation declared on a base class attaches only to - Attribute development gains across analyzer and fact changes before measuring an untouched admission population.
the classes that extend it. - Fail the runtime and instrument gates rather than summarize partial work as a result.
- Coverage stated per repository: what was recognised, reached, checked, and left unchecked by reason.
### Non-Goals ### Non-Goals
- Taint flow, the Joern engine, nesting-aware dominance, families other than access control. - Taint analysis, Joern integration, model calls inside the prover, or vulnerability families beyond access control.
- Model calls inside the prover; intent adjudication stays a separate arm. - Client-side presentation gating, proof execution, pre-push wiring, or admission-policy thresholds.
- The unseen pilot measurement and the pre-push wiring (later increments of other specs). - Replacing the existing order-based checker mode for consumers that do not opt into prover semantics.
## Boundary Commitments ## Boundary Commitments
### This Spec Owns ### This Spec Owns
- Entry recognisers for Stapler, Jetty handlers, Symfony attributes, Livewire and Laravel controllers, Remix - The abstract evidence domain and three-way pair/path judgment.
exports, Tornado handlers, and programmatic servlet/handler registration sites `[2026-10-29]` - Stable program identities, conservative call-target sets and bounded interprocedural summaries.
(`mapping.py` blocks, each with evidence). - Resource- and authority-sensitive operation/witness matching.
- `semantic/obligations/resolution.py`: the class table and receiver- and signature-qualified call - Prover-mode control-flow dominance without textual-order fallback.
resolution `[2026-10-29]`. - Acquired entry, operation, discharger and authority knowledge needed by this family.
- The optional `receiver` field of `semantic/ir.py`'s `CallSite` and its capture in `semantic/cst.py` - Deterministic traces, failure-visible coverage, development arms and the admission-run contract.
(additive, default `None`; every existing consumer unchanged) `[2026-10-29]`.
- `semantic/obligations/paths.py`: `PathRecord` and `compose_paths` over the caller index.
- `semantic/obligations/enforcement.py`: router, decorator, voter `[2026-10-29]` witnesses from route files
and attributes, each carrying the resource it binds `[2026-10-29]`.
- `hunt/prover.py`: `prove_change` and `Coverage`.
- `benchmarks/valid/prover_arm.py`: the runner and its summary.
- Operation and discharger rows added to `ruleset/obligations/{java,php,python,typescript}.toml`.
### Out of Boundary ### Out of Boundary
- The facts loader's existing fields, `OrderDominance`, and the checker's existing decision rules (owned by - The legacy checker behavior used by existing callers; default APIs remain backward compatible.
`authorization-obligations`). Three extensions are allowed and owned here: the handler-named operation fields - Target labels and benchmark curation, owned by `valid-detection-benchmark`.
(`handlers`, `framework`) in the facts; the `assume_entries_protected` keyword and the `unguarded_path` label - Runtime demonstrations, owned by `search-with-proof`.
(see the checker block below); and, `[2026-10-29]`, the optional `resource` field on `Discharge` with the - Hook admission and rollout, owned by `pre-push-safety-net`.
`resource_bound` keyword and the all-kind caller-hop re-judgement in `_on_caller_hop` (see the checker block - Continuous fact evolution after the one-time initialization pass.
below). Each is additive or default-off; every existing checker test unchanged.
- The feature table, the DSPy and hunter arms, the ablation (`hunter-learning-loop`).
- The proof arm's demonstration logic (`search-with-proof`); this spec only shapes the finding.
### Allowed Dependencies ### Allowed Dependencies
- `openultrasast.source` (callers_of, code_index, product_files, generated_reason), `openultrasast.mapping`, - `openultrasast.source`, `openultrasast.mapping`, the existing semantic IR and CFG, and the existing obligation
`openultrasast.semantic.cst` and `ir`, `openultrasast.semantic.obligations` (check, facts, dominance), facts/checker contracts.
`openultrasast.hunt.producers.Context`, `benchmarks.valid.metrics`, `benchmarks.unseen.score`. - The hunter `Context` caches, valid-benchmark metrics, replay record envelopes and reserved-population guard.
- No new third-party dependency. - No new third-party dependency.
### Revalidation Triggers ### Revalidation Triggers
- A change to `EntryPointRecord` fields or `Discharge` scopes. - A change to `EntryPointRecord`, `Operation`, `Discharge`, `Trace`, hunt-defect or replay-record contracts.
- A change to `CallSite` fields or the caller-index keying `[2026-10-29]`. - A change to call-site receiver/signature capture, target-knowledge schemas or authority implication semantics.
- A change to the replay record shape `benchmarks.valid.metrics` scores. - A change to path aggregation, judgment ordering, admission population isolation or the 120-second budget.
- A change to the hunt defect shape (`defect_from`).
## Architecture ## Architecture
### Existing Architecture Analysis ### Existing Architecture Analysis
The checker already judges reached operations by path and witnesses by scope; the hunter's `Context` already The delivered baseline already recognises entries, composes bounded paths, discovers operations and witnesses,
caches everything the prover reads. The missing pieces are the inputs: entries for Java and the pool's PHP, builds structural CFGs, emits traces and scores replay records. Its decision interfaces are boolean: unresolved
TypeScript and Python frameworks; paths; witnesses from outside the function; operation rows for the pool's receivers fall back to a bare name, unavailable CFGs fall back to textual order, and a witness either discharges or
protected calls. The prover is wiring plus those inputs. does not. The amendment preserves the existing checker as the single verdict owner while inserting typed abstract
values before that boolean boundary and adding an opt-in three-way judgment path.
### Architecture Pattern & Boundary Map ### Architecture Pattern & Boundary Map
```mermaid ~~~mermaid
flowchart LR flowchart LR
C[hunt.producers.Context\nproduct files, texts, IR, call index, entry points] --> P[hunt.prover.prove_change] TK[Target knowledge] --> MR[Mapping registry]
M[mapping.analyze_entry_points\n+ Stapler, Jetty, Symfony, Livewire, Remix, Tornado\n+ programmatic registration] --> C TK --> FI[Fact interpreter]
C --> RES[obligations.resolution\nclass table, receiver-qualified calls] MR --> CTX[Per change context]
RES --> P FI --> OP[Operations and witnesses]
P --> PA[obligations.paths.compose_paths] CTX --> SI[Symbol index]
P --> EN[obligations.enforcement.witnesses\nrouter, decorator, voter, resource-bound] SI --> CR[Call resolution]
PA --> CK[obligations.check.check_obligations] CR --> FS[Function summaries]
EN --> CK OP --> FS
RES --> CK FS --> PM[Concrete path materializer]
F[ruleset/obligations/*.toml\noperations, dischargers] --> CK CTX --> CFG[Control flow relations]
CK --> P PM --> J[Three way judgment]
P --> R[benchmarks/valid/prover_arm.py\nrecords, metrics, coverage, anchor refusal] CFG --> J
P --> H[pre-push admission / proof arm\nhunt defects] OP --> J
``` J --> TR[Canonical traces]
- Selected pattern: inputs assembled in the prover, decision in the existing checker. J --> PR[Prover findings and coverage]
- Existing patterns preserved: closed facts in TOML with checked headers; `_entry` recogniser blocks; per-change TR --> RUN[Development and admission runners]
`Context`; replay record shape; counts and digests in records. PR --> RUN
- Steering compliance: no model call, instrument gate (bytes read), plain English, standard metrics. ~~~
Dependency direction is:
`abstract domain -> fact and recogniser adapters -> symbol resolution -> summaries and CFG -> checker judgment
-> prover integration -> runners`. Engine modules never import target-named strategy modules. Target-aware
adapters may import engine contracts.
- **Strategy + Registry**: entry recognisers are registered per recognition concern.
- **Interpreter**: promoted fact rows describe operations, dischargers, resource rules and authority implications.
- **Adapter**: the prover converts repository IR, target knowledge and context caches into engine contracts.
- **Abstract interpretation**: finite domains and monotone joins propagate evidence and incompleteness.
### Technology Stack ### Technology Stack
| Layer | Choice | Role | Notes | | Layer | Choice | Role |
|---|---|---|---| |---|---|---|
| Analysis | Python 3.11, tree-sitter IR, regex caller index | entries, paths, operations | existing | | Analysis | Python 3.11, existing semantic IR and structural CFG | symbols, calls, summaries, dominance |
| Facts | TOML under `ruleset/obligations/` | operations, dischargers | existing loader | | Knowledge | Existing TOML fact files and mapping strategies | target-specific vocabulary |
| Scoring | `benchmarks.valid.metrics`, `benchmarks.unseen.score` | recall, false alarms per 100 | existing | | Scoring | Existing valid-benchmark metrics and replay records | development and admission results |
## File Structure Plan ## File Structure Plan
``` ~~~text
src/openultrasast/ src/openultrasast/
├── mapping/ # recogniser registry [2026-10-29]: one strategy module per ├── mapping/
│ │ # recognition concern, each registering itself in one table │ ├── registry.py # strategy registration and dispatch
│ ├── registry.py # the registry the language dispatch consults │ ├── registration_sites.py # generic call-site registration adapter
│ ├── registration_sites.py # call-site handler registration strategy (data-driven vocabulary) │ └── <target>.py # one target-aware strategy per migrated concern
│ └── stapler.py, jetty.py, symfony.py, livewire.py, remix.py, tornado.py
│ # strategy modules migrating from mapping.py's legacy blocks
├── mapping.py # dispatch consults the registry during migration; blocks retire
│ # family by family (breadth map tracks the order)
├── semantic/ ├── semantic/
│ ├── cst.py # + capture call receiver text in _call [2026-10-29] │ ├── ir.py # CallSite receiver and signature evidence
│ ├── ir.py # + CallSite.receiver: str | None [2026-10-29] │ ├── cst.py # receiver, arity and signature capture
│ └── obligations/ │ └── obligations/
│ ├── resolution.py # ClassRecord, class_table, qualify — receiver/signature resolution [2026-10-29] │ ├── abstract_domain.py # relations, completeness, resources, authorities, judgments
│ ├── paths.py # PathRecord, compose_paths(callers, entries, targets, depth, fan_out) │ ├── resolution.py # SymbolId, symbol index and CallTargetSet
│ └── enforcement.py # witnesses(root, texts, entries, facts) -> list[Discharge] │ ├── summaries.py # monotone FunctionSummary computation over call components
├── hunt/prover.py # prove_change(root, change, ctx) -> ProverResult(findings, coverage) │ ├── paths.py # concrete evidence paths and cutoff records
└── ruleset/obligations/{java,php,python,typescript}.toml # + operation and discharger rows, checked headers │ ├── operations.py # operation/witness abstract identities
src/openultrasast/tools/fact_init.py # the one-time initialization job: extract + propose -> verify -> promote [2026-10-29] │ ├── dominance.py # legacy order mode, unchanged by default
benchmarks/valid/prover_arm.py # run / summarize; records per change; coverage table; anchor refusal │ ├── cfg_dominance.py # prover-mode control-flow relation, no order fallback
tests/test_mapping_registry.py, tests/test_mapping_registration_sites.py, tests/test_obligation_resolution.py, │ ├── check.py # opt-in abstract matching and PairJudgment
tests/test_obligation_paths.py, tests/test_obligation_enforcement.py, tests/test_hunt_prover.py, │ ├── enforcement.py # target-aware enforcement adapter
tests/test_prover_arm.py, tests/test_fact_init.py │ └── trace.py # canonical trace with abstract relations and completeness
``` ├── hunt/
│ ├── producers.py # cached symbol index and summaries
│ └── prover.py # integration, path aggregation, findings and coverage
└── tools/
└── fact_init.py # label-blind extract/propose/verify/promote workflow
ruleset/obligations/*.toml # promoted operation, witness and authority knowledge
benchmarks/valid/
├── prover_arm.py # records, stage funnel, gates and four-arm comparison
└── prover_admission.py # frozen-manifest admission orchestration
tests/
├── test_obligation_abstract_domain.py
├── test_obligation_resolution.py
├── test_obligation_summaries.py
├── test_obligation_judgment.py
├── test_obligation_trace.py
├── test_mapping_registry.py
├── test_fact_init.py
├── test_hunt_prover.py
└── test_prover_arm.py
~~~
### Modified Files ### Modified Files
- `src/openultrasast/mapping.py`: dispatch consults the `mapping/` registry (Strategy pattern); legacy blocks - Existing facts, IR, checker, dominance, trace, prover and arm modules receive additive or opt-in contracts.
migrate into strategy modules family by family; `EntryPointRecord` unchanged. - Existing callers retain legacy defaults; only the prover selects three-way judgment and strict CFG semantics.
- `src/openultrasast/hunt/producers.py`: `Context` exposes `entry_points` and `ir(path)` already; add nothing - Existing completed benchmark records remain immutable development baselines.
unless a cache is missing (then a `cached_property`, same pattern).
- `src/openultrasast/ruleset/obligations/*.toml`: rows only, admitted through the fact gate.
## System Flows ## System Flows
```mermaid
sequenceDiagram ### Analysis
participant R as prover_arm ~~~mermaid
participant X as Context flowchart TD
participant P as prove_change A[Read cached source] --> B[Recognise entries and facts]
participant PA as compose_paths B --> C[Build symbol index]
participant EN as enforcement C --> D[Resolve call target sets]
participant CK as check_obligations D --> E[Compute function summaries]
R->>X: checkout head, Context(root, change) E --> F[Materialise evidence paths]
R->>P: prove_change(root, change, ctx) F --> G[Match witnesses to obligations]
P->>X: entry_points, call_index, texts, ir(path) per touched file G --> H[Evaluate control flow]
P->>PA: targets = touched functions + callees holding operations H --> I{Path state}
PA-->>P: PathRecords (hops entry..operation function), unreached list I -->|complete and missing| U[Unguarded]
P->>EN: route files, attributes, entries I -->|complete and governed| DCH[Discharged]
EN-->>P: router/decorator Discharges I -->|possible unknown or cut| IND[Indeterminate]
P->>CK: irs of touched + hop files, entries, facts, paths, OrderDominance(texts), extra_witnesses U --> FIND[Finding and trace]
CK-->>P: ObligationResult (findings, discharges, degradations) DCH --> TRACE[Trace only]
P-->>R: ProverResult(findings as hunt defects with entry, hops, operation, missing; coverage) IND --> TRACE
R->>R: record per change; metrics.summarize; coverage table ~~~
```
Gating: a file that fails to parse, a language without facts, or an unrecognised framework yields coverage ### Evaluation
counts and no finding. A declared-public route suppresses `path_guard` only. ~~~mermaid
flowchart LR
ACQ[Label blind acquisition] --> FREEZE[Freeze candidate bundle]
BASE[Materialize baseline bundle] --> DEV[Four arm development comparison]
FREEZE --> DEV
DEV --> AUDIT[Review ordinary labels]
AUDIT --> HOLD[Untouched paired admission run]
HOLD --> GATE{Instrument budget and quality gates}
GATE -->|pass| GO[G1 eligible]
GATE -->|fail| NOGO[No go]
~~~
## Requirements Traceability ## Requirements Traceability
| Requirement | Components | | Requirement | Components |
|---|---| |---|---|
| 1.1-1.8 | mapping recognisers | | 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 | mapping registry, registration adapter, fact initialization, coverage |
| 1.9 | mapping `_jetty_registration_entries` + `resolution.class_table` | | 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10 | fact interpreter, abstract domain, operations, enforcement, witness matcher |
| 2.1-2.8 | enforcement.witnesses, facts rows, checker (existing) | | 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10 | symbol resolution, summaries, path materializer |
| 2.9 | facts rows (handler-named webhook and data-exposure patterns), `handlers_of` | | 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7 | pair judgment, checker integration, prover findings |
| 2.10 | enforcement resource tokens and voter witnesses; checker `resource_bound` | | 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7 | prover arm, four-arm development comparison |
| 3.1-3.6 | paths.compose_paths | | 6.1, 6.2, 6.3 | context caches, deadline state, runner budget gate |
| 3.7 | hunt.prover target enumeration (down-call budget) | | 7.1, 7.2, 7.3, 7.4, 7.5, 7.6 | completeness, prover coverage, runner funnel |
| 3.8 | trace `guards_before_call` + checker `_on_caller_hop` all-kind re-judgement | | 8.1, 8.2, 8.3 | finding contract, trace references, proof-arm filter |
| 3.9 | resolution.qualify, class_table, `CallSite.receiver` | | 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7 | canonical trace and record provenance |
| 3.10 | checker `_on_caller_hop` + prover witness assembly | | 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7 | strict CFG dominance and legacy order compatibility |
| 4.1-4.6 | hunt.prover.prove_change | | 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8 | abstract domain, path and operation aggregation, transition report |
| 5.1-5.5 | prover_arm | | 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9 | acquisition record, freeze manifest, admission runner |
| 5.6 | prover_arm anchor refusal |
| 6.1-6.2 | prover_arm timing, Context caches |
| 7.1-7.2 | Coverage in prove_change, prover_arm summary |
| 7.3 | prove_change error reasons |
| 8.1-8.2 | finding shape in prove_change |
| 9.1-9.6 | trace serialisation in prove_change (obligations.trace) |
| 10.1-10.7 | cfg.FunctionCfg, CfgDominance |
## Pattern Application and Fact Acquisition `[2026-10-29]`
The implementation follows the steering pattern discipline (`code-standards.md`): the engine stays
target-agnostic; per-target vocabulary is data. Concretely for this spec:
- **Strategy + Registry** for entry recognisers: the recognisers this spec adds (registration sites, and the
migration of existing framework blocks) live as strategy modules in a `mapping/` registry -- one module per
recognition concern, each naming its target family in the filename, registering itself in one table. The
dispatch in `mapping.py` consults the registry; it does not name frameworks.
- **Interpreter** for obligation facts: operation rows, discharger rows, handler-name families and
registration-site vocabulary are TOML data interpreted by the engine; adding a target's vocabulary is a data
change reviewable in one place.
- **Adapter** at the engine seams: `hunt/prover.py`'s callers adapter binds the IR to the caller index
(receiver-qualified through `resolution`); `obligations/enforcement.py` adapts framework constructs into
`Discharge` witnesses and is target-aware territory -- engine modules never import it.
### Fact acquisition (the one-time initialization pass)
Per-target vocabulary is produced by a one-time initialization job, not by hand-specced enumerations. The job
has two modes and one gate:
1. **Extraction mode**: walks the repository's existing recogniser code and framework knowledge (the legacy
`mapping.py` blocks, the current TOML rows) and extracts the target vocabulary already embedded there into
the acquirable structures (row families, strategy registrations).
2. **Proposal mode**: an LLM pass (or the maintainer) proposes the vocabulary a new target needs -- entry
shapes, registration sites, handler-name families, policy-decision constructs -- against the target's
repository.
3. **The gate**: every extracted or proposed row passes the same header discipline the facts files already
enforce (source, version, region) and engine verification admits each row (the row must match what the
code says); only then is it promoted to a closed row. Nothing skips the gate.
The initialization job is a pipeline tool in this spec's scope (it produces the data the engine consumes);
the standing fact-proposal lane (LLM proposes continuously, engine verifies, repetition promotes) remains the
roadmap's growth path and is out of boundary here. First runs: extraction over the current embedded
vocabulary, proposal for the training pool's target families, with the pool repositories as evidence.
## Components and Interfaces ## Components and Interfaces
### mapping recognisers ### Abstract evidence domain
| Field | Detail | ~~~python
|---|---| class EvidenceRelation(Enum):
| Intent | name web-exposed methods of the pool's frameworks as `route` entries with evidence | MUST = "must"
| Requirements | 1.1-1.8 | MAY = "may"
NO = "no"
UNKNOWN = "unknown"
Each block follows the existing pattern: scan the file text (and the IR's function spans when a method body is class PairJudgment(Enum):
needed), call `_entry(target, start, end, handler, name, kind, access, trust_boundary, evidence, conditions)`. UNGUARDED = "unguarded"
Stapler: class text matches `extends|implements` one of the exposed types, or a method parameter is typed DISCHARGED = "discharged"
`StaplerRequest`, `StaplerRequest2`, `StaplerResponse` or annotated `@QueryParameter`/`@AncestorInPath`; method is INDETERMINATE = "indeterminate"
`public` and its name matches `^do[A-Z]`. Access `public` only for `UnprotectedRootAction`. Jetty: classes
extending `AbstractHandler`, `HttpServlet` or annotated `@Path`/`@GET`/`@POST`. Symfony: methods with
`#[Route(` or `@Route(`; classes extending `AbstractController`. Livewire: classes extending `Component`, public
methods not starting with `_`, plus `Route::` registrations naming a controller method. Remix: `export const|function
action|loader` under `app/routes`. Tornado: classes extending `RequestHandler` or `WebSocketHandler`, request
methods and `open`.
Registration sites `[2026-10-29]` (1.9): the call-site registration concept the Python route
registrations and the WordPress registration table already use, stated once, generically: when the
repository's own code registers a handler at a call site (a constructor or registration method whose argument
names the handler class), the registered class's request methods become `route` entries at the method's
declaration line in the registered class's file, with the registration site (`path:line`) and the registered
class name as evidence and `registered_by` set. The registration-site vocabulary -- which constructors and
methods count as registrations, and which methods of the registered class are request methods -- is acquirable
data under the facts discipline (R2 AC7), carried by a registration strategy module in the recogniser
registry, never hardcoded in the engine. Class-literal resolution goes through `resolution.class_table`. A
registration whose target class the repository does not define yields coverage (`registration_unresolved`) and
no entry. Jetty's programmatic servlet registration is the first data case (seatunnel evidence), not the
definition.
### obligations.paths
```python
@dataclass(frozen=True) @dataclass(frozen=True)
class PathRecord: class Completeness:
hops: tuple[tuple[str, int | None], ...] # ("path::function", line of the call site in the caller), entry first is_complete: bool
entry: str # "path::function" of the entry hop reasons: tuple[str, ...] = ()
complete: bool # True when hops[0] is an entry point
def compose_paths(
*, callers: Callable[[str, str], Sequence[tuple[str, int]]], # (path, function) -> caller sites
entries: set[str], targets: Sequence[tuple[str, str]], max_depth: int = 3, fan_out: int = 25,
) -> tuple[list[PathRecord], list[str]]: # complete paths, unreached "path::function" targets
```
- Preconditions: targets name functions with IR; `callers` resolves a function to its call sites through the
cached index and the enclosing function of each site (`source.enclosing_at` or the IR spans).
- Postconditions: every path's last hop is a target; no path implies nothing; unreached targets are listed.
### obligations.resolution `[2026-10-29]` (3.7, 3.9; feeds 1.9, 2.10, 3.10)
```python
@dataclass(frozen=True) @dataclass(frozen=True)
class ClassRecord: class ResourceRef:
path: str # file declaring the class kind: Literal["exact", "aliases", "request_global", "unknown"]
values: tuple[str, ...] = ()
@dataclass(frozen=True)
class AuthorityRef:
values: tuple[str, ...]
is_unknown: bool = False
~~~
The partial order places definite evidence below possible or unknown evidence. Join never converts `MAY` or
`UNKNOWN` into `MUST` or `NO`. The precision order is:
~~~text
MUST <= MAY <= UNKNOWN
NO <= MAY <= UNKNOWN
~~~
`MUST` and `NO` are incomparable. The alternative-path join is fixed by this table:
| join | MUST | NO | MAY | UNKNOWN |
|---|---|---|---|---|
| MUST | MUST | MAY | MAY | UNKNOWN |
| NO | MAY | NO | MAY | UNKNOWN |
| MAY | MAY | MAY | MAY | UNKNOWN |
| UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
Three separate operations prevent one table from being used for the wrong quantifier:
1. `join_alternatives` uses the table above when alternative call targets or control paths contribute evidence.
2. `conjoin_dimensions` combines kind, scope, resource, authority and dominance for one witness: any `NO`
produces `NO`; otherwise any `UNKNOWN` produces `UNKNOWN`; otherwise any `MAY` produces `MAY`; only
all-`MUST` produces `MUST`.
3. `aggregate_witnesses` asks whether any witness definitely governs one obligation: any `MUST` produces
`MUST`; otherwise `UNKNOWN` takes precedence over `MAY`, and `MAY` takes precedence over `NO`.
An empty witness set is `NO` only when discovery and facts are complete; otherwise it is `UNKNOWN`.
For one complete path, all required obligations at `MUST` yield `discharged`; any `MAY` or `UNKNOWN` yields
`indeterminate`; otherwise any `NO` yields `unguarded` and every `NO` obligation is reported missing.
Thus a mixed `[MUST, NO]` obligation vector is unguarded for the `NO` obligation. Resource
alias sets join by finite union; an unbounded or unresolvable value joins to `unknown`. Authority implication is
read from promoted facts and contributes its fact identifier to the match evidence.
### Symbol resolution
~~~python
@dataclass(frozen=True, order=True)
class SymbolId:
module: str
enclosing_type: str | None
name: str name: str
superclass: str | None signature: tuple[str, ...] | None
abstract: bool
methods: tuple[str, ...]
def class_table(texts: Mapping[str, str], language: str) -> Mapping[str, ClassRecord]
# bare class name -> record; one table per change, cached on the Context
def qualify(*, receiver: str | None, enclosing_class: str | None, name: str,
arity: int | None, table: Mapping[str, ClassRecord]) -> str
# the qualified key for a callee: "Class.name" when the receiver text names a class
# in the table (or `this`/`self`/bare call in a class's own file), "Class.name/arity"
# when the class declares same-name methods with distinct parameter lists and the
# call site's arity is known, and the bare "name" when the receiver is unresolvable
# (dynamic languages, framework objects the repository does not define).
```
- Preconditions: `receiver` is the receiver expression text captured on `CallSite` (new field; `None` for
bare calls in dynamic languages); `class_table` covers the repository's own classes only.
- Postconditions: same-name methods on different classes yield different keys; overloads with known distinct
arity yield different keys; unresolvable receivers fall back to the bare name and are counted
`receiver_unresolved` in coverage, never silently merged.
- Base-class attachment rule: an operation or handler-named entry declared on a class attaches to a path only
when the entry's (or call site's) class is that class or an in-repo subclass of it; `abstract` classes and
framework bases never project onto unrelated routes.
- Consumers: the prover's `callers` adapter returns, for a target key `Class.name`, only caller files whose
IR binds the callee to a receiver of that class (or subclass); the target enumeration joins callees by
qualified key instead of trailing name. `compose_paths` and `check_obligations` consume `path::function`
strings unchanged -- qualification happens before them, so `PathRecord` keeps its shape.
- Budget: the class table is one pass over texts (already read); resolution adds no tree walks. The target
enumeration extends down through calls within its own budget (`down_depth`, default 2, sharing the fan-out
bound), so an operation one call deeper than the handler is reached without unbounded descent.
### checker extension (opt-in)
`check_obligations(..., assume_entries_protected: bool = False)`. With path records, no declared clause and no
sibling norm, the checker today returns no finding (gating 3 of `authorization-obligations`): nothing justified
the obligation. The prover's premise justifies it: a recognised entry is protected unless declared public. With
the keyword on, `_finding` labels such a case `unguarded_path` when the path's entry hop is an entry record whose
access is not `public` and the route is not declared public; evidence carries the entry's kind and evidence
strings and the sentence "no declared policy clause and no sibling norm; the prover assumes a recognised entry is
protected". `findings_to_static` explains the label like the others. `LABELS` gains the value.
Resource-bound discharge `[2026-10-29]` (2.10): `check_obligations(..., resource_bound: bool = False)`. With the
keyword on, a witness whose `resource` field is set suppresses only operations whose resource token equals it
(a token computed the way `Operation.resource` already is: receiver first segment, table name, or the row's
`resource_arg`); a witness with `resource` unset keeps today's behaviour. Default off; existing tests unchanged.
Caller-hop re-judgement `[2026-10-29]` (3.8, 3.10): `_on_caller_hop` re-judges a witness on an earlier hop for
every discharger kind, not only `path_guard` and `ownership_check`: the witness is re-judged against the
operation moved to the hop's call line, by the selected dominance. A guard recorded on a hop before the call
(`guards_before_call`) participates in the same re-judgement, so a finding is never emitted when the pair's own
trace shows a dominating guard of a required kind. Router and decorator scopes keep their whole-handler
semantics.
### facts extension: handler-named operations (owned here, schema change)
`[[operation]]` rows gain two optional fields: `handlers` (a list of regular expressions over the function name)
and `framework` (a recogniser id the mapper's evidence names: `stapler`, `tornado`, `remix`, `jetty`,
`symfony`, `laravel`). A row with `handlers` and no `calls` is a handler-named operation: `find_operations`
yields it at the function's declaration line for every function whose name matches and that the caller lists as
a recognised entry of that framework (new keyword `handlers_of: Mapping[str, str] | None`, function name to
framework id; default None keeps today's behaviour). The smoke run of delivery A showed why: 15 of the 17
Jenkins fixes are form-validation and list-filling web methods whose body calls no listed operation; the
method is the operation. Jenkins documents the rule (form validation methods must check a permission).
Handler-named rows for the webhook, build-trigger and data-exposure shapes `[2026-10-29]` (2.9): the
`handlers` patterns extend to verb families whose handler name itself carries the obligation -- endpoints
that act with repository or job privileges when called (webhook and build-trigger shapes) and endpoints that
expose protected data (data-exposure shapes) -- as operation rows on framework-recognised entries, at the
declaration line. The families are rows in the facts files, not code: a new verb family is a row addition
acquired through the one-time initialization pass for the target (or proposed by the fact-proposal lane), and
a row without `handlers` never matches a declaration name. The gate-1 misses (Stapler webhook and
chart-data endpoints) are the first evidence set, not the definition.
### trace: the ordered serialisation (owned here)
```python
@dataclass(frozen=True)
class Guard: kind: str; scope: str; line: int | None; provenance: str; fact_id: str
@dataclass(frozen=True)
class Hop: function: str; call_line: int | None; guards_before_call: tuple[Guard, ...]; forwarded: tuple[str, ...]
@dataclass(frozen=True)
class Trace:
entry_kind: str; boundary: str; access: str; parameters: tuple[str, ...]
hops: tuple[Hop, ...] # entry first; the last hop holds the operation
operation_kind: str; fact_id: str; operation_line: int
arguments: tuple[tuple[str, str], ...] # (argument text, provenance) in call order
guards_after: tuple[Guard, ...]
requires: tuple[str, ...]; dominating: tuple[str, ...]; missing: str | None
def text(self) -> str: ... # one component per line, in order
def record(self) -> dict: ... # canonical JSON
```
Argument provenance: within each hop's function, follow the IR's binds backwards from the operation's argument
identifiers to the function's parameters or to a request-source fact (`request_input`), to an identity source
(`authenticated_context`), or to a literal (`constant`); a parameter that the previous hop's call site fills
from its own request-derived values keeps `request_input`; anything the walk cannot follow is `unknown`. No
inter-procedural dataflow beyond the call sites on the path. The trace never carries language, framework,
repository, size or count fields.
### cfg and CfgDominance (owned here)
`src/openultrasast/semantic/cfg.py`: `FunctionCfg.from_cst(node, language)` builds basic blocks and edges from
the tree-sitter syntax tree of one function (blocks, if/else, switch arms, loops with back edges, try/catch/
finally, return, throw, break, continue, exit) and computes dominators (`dominates(a_line, b_line)`) and the
control context of a line (`context(line) -> ControlContext(conditions, in_loop, in_try, catches, on_exception_path)`).
`src/openultrasast/semantic/obligations/cfg_dominance.py::CfgDominance` implements the `Dominance` protocol:
a statement witness dominates when its block dominates the operation's block and the operation is unreachable
from the guard's failing arm: either that arm exits (`throws`, `returns`, `exits`), or the operation lies only
inside the passing branch (`if (can(x)) { op(); }`, effect `branches`); a bare boolean whose value no branch or
exit consumes before the operation is `value_only` and never discharges; router and decorator scopes
dominate as before; along a path record, a witness on an earlier hop dominates as before. Without a graph for
a function, `CfgDominance` falls back to `OrderDominance` and records `cfg_unavailable`.
The trace's `Guard` gains `relation` (`all_paths`, `some_paths`, `none`, `after`) and `effect`
(`throws`, `returns`, `exits`, `branches`, `value_only`; `constrains` for an identity constraint or a
non-permissive value on the operation's own line, which has no failing arm; router and decorator witnesses carry
`exits`); the trace gains `operation_context: ControlContext`. Coarse spots accepted for this increment: a
ternary or switch expression is one node, several statements on one line share it, a guard's result held in a
variable is followed by name without reassignment tracking.
### obligations.enforcement
```python
def witnesses(*, root: Path, texts: Mapping[str, str], entries: Sequence[EntryPointRecord], language: str) -> list[Discharge]
```
Reads Laravel route files (`routes/*.php`) for `->middleware([...])` and controller constructors for
`$this->middleware`/`authorizeResource`; Symfony attributes and `config/packages/security.yaml`
`access_control`; Tornado `@authenticated`; emits `Discharge(kind="path_guard", scope="router"|"decorator", path,
function, line=None, evidence=...)` for each covered handler. Names recognised live in the facts files' headers.
Resource binding and policy witnesses `[2026-10-29]` (2.10): every witness this module mints carries the
resource it binds (`Discharge` gains an optional `resource: str | None`, default `None`): the route path for
router middleware, the attribute or subject expression for a policy-layer guard, the middleware name for
`auth`-class guards (no resource -- `None`). A policy-layer witness covers an action's operations when a
policy decision (voter, capability check, or equivalent) reachable from the action -- in the action body or a
function it calls -- names the action's attribute or subject; the witness is scoped to that action's
operations with the attribute or subject as its resource. Which constructs count as policy decisions is
acquirable data in the facts files (the discharger rows already carry the vocabulary); the witness shapes are
engine behaviour. Discharge matching is the checker's `resource_bound` keyword (above); the resource token
computation is shared with `Operation.resource` so the two sides compare like with like. Symfony's voter
attributes are the first data case (kimai evidence), not the definition.
### hunt.prover
```python
@dataclass(frozen=True)
class Coverage: files_by_language, frameworks_recognised, entry_points, reached, unreached, operations_checked,
operations_discharged, unchecked_by_reason # all ints or dicts of ints
@dataclass(frozen=True) @dataclass(frozen=True)
class ProverResult: findings: tuple[dict, ...]; coverage: Coverage; degradations: tuple[dict, ...] class CallTargetSet:
targets: tuple[SymbolId, ...]
completeness: Completeness
~~~
def prove_change(root: Path, change: Mapping[str, Any], ctx: Context | None = None, *, max_depth: int = 3) -> ProverResult The symbol index covers repository-defined declarations and inheritance. Exact receiver and full signature produce
``` one complete target. Partial type or signature information produces a finite candidate set with an incompleteness
- Touched functions come from `_candidates`-style enumeration (touched head lines to function spans), build reason. No call falls back to a global bare-name key. Base declarations attach through explicit descendant edges.
output left out via `source.generated_reason`.
- Targets = touched functions plus callees that hold a protected operation, joined by qualified key
(`resolution.qualify`) and followed down through calls within the `down_depth` budget (default 2) sharing the
fan-out bound, so an operation one call deeper than the handler is reached `[2026-10-29]` (3.7).
- The `callers` adapter resolves a target to its call sites through the caller index and keeps only callers
whose receiver binds to the target's class (or subclass); unresolvable receivers fall back to the bare name
and are counted `receiver_unresolved` in coverage `[2026-10-29]` (3.9).
- Findings: for each `ObligationFinding`, a hunt defect dict with `path`, `line`, `family="access_control"`,
`function`, `evidence` (entry record summary, hops, operation id and calls, missing kind), `id` as the checker
emits.
- Degradations and every unchecked reason land in `Coverage`.
### prover_arm ### Function summaries and paths
`python -m benchmarks.valid.prover_arm run POOL --repos REPOS --out DIR [--slice 1] [--only all] [--max-depth 3]` ~~~python
writes `<sha256(change id)>.json` per change (replay record envelope copied from `hunter_arm`: id, case_id, @dataclass(frozen=True)
repo, kind, family, path, function_lines, files, head, instrument {changed_bytes, hook_bytes_read, head_verified}, class FunctionSummary:
findings, coverage, seconds, status), resumes by skipping done records, then `summarize` writes `summary.json` symbol: SymbolId
(metrics from `benchmarks.valid.metrics.summarize`, per-repository and per-framework coverage, changes over 120 s, operations: tuple[Operation, ...]
instrument failures) and refuses when any record read zero bytes of a file git holds. witnesses: tuple[Discharge, ...]
calls: tuple[CallTargetSet, ...]
completeness: Completeness
~~~
Anchor refusal `[2026-10-29]` (5.6): `_prove` validates every returned finding's `path` against the files it Summaries are computed by a monotone worklist over strongly connected call components. Their finite contents grow
actually read (`instrument.file_bytes`); a finding on an absent path marks the record by union until stable. A budget cutoff stops growth, records the cutoff and leaves affected summaries incomplete.
`status="instrument_failure"`, reason `stale_anchor`, findings cleared -- the same treatment as a zero-byte Concrete paths are then materialised from recognised entries through exact targets for evidence. Candidate edges
read. `summarize` refuses to summarise a run containing such records, listing them by record id. may contribute diagnostic traces but cannot complete a final path.
### Witness matching and judgment
~~~python
@dataclass(frozen=True)
class WitnessMatch:
relation: EvidenceRelation
resource_relation: EvidenceRelation
authority_relation: EvidenceRelation
evidence: tuple[str, ...]
@dataclass(frozen=True)
class PathJudgment:
judgment: PairJudgment
missing: tuple[str, ...]
completeness: Completeness
matches: tuple[WitnessMatch, ...]
~~~
A witness governs an obligation only when kind, scope, resource, authority and dominance are all `MUST`.
Any `MAY` or `UNKNOWN` required relation makes the path indeterminate. With complete evidence and no governing
witness for a required kind, the path is unguarded. Multiple paths aggregate as follows:
- any complete unguarded path -> `unguarded`;
- complete enumeration and every path discharged -> `discharged`;
- otherwise -> `indeterminate`.
The checker gains an opt-in abstract mode. Legacy callers keep the current boolean result and order-based default.
### Strict CFG dominance
`CfgDominance.relation` returns an evidence relation and a guard effect. Prover mode never calls
`OrderDominance` when a graph, line mapping or construct is unavailable; it returns `UNKNOWN` with
`cfg_unavailable` or `line_without_node`. Route and declaration scopes return `MUST` only after exact handler
attachment. The legacy dominance class remains unchanged.
### Target knowledge and acquisition
The mapping registry and fact interpreter are target-aware adapters. The initialization job has:
- extraction mode for existing strategies and fact rows;
- proposal mode for source- or documentation-derived candidate knowledge;
- verification that checks source, version, region and concrete matches;
- promotion that writes only verified rows or enabled strategies;
- an acquisition record containing input revision, evidence digest, accepted/rejected proposals and output digest.
Admission mode prevents the acquisition process from receiving benchmark labels or fix locations. A changed output
digest after labels are opened spends the population.
### Prover integration
`prove_change` orchestrates cached reads, recognition, resolution, summaries, paths, strict dominance and
judgment. It emits findings only for `unguarded` pairs and preserves all three judgments as traces. Coverage
contains stage counts, reason counts, pair judgments and exploration cutoffs. Exceptions are recorded at the stage
that raised them.
### Development and admission runners
Before comparison, the development runner materialises and validates two immutable bundles:
~~~python
@dataclass(frozen=True)
class AnalyzerBundle:
analyzer_revision: str
fact_set_digest: str
acquisition_record_digest: str
configuration_digest: str
~~~
The baseline bundle points to the analyzer revision and fact set that produced the immutable 2026-10-28 records;
the candidate bundle points to the amended equivalents. The runner refuses a four-arm comparison if either
revision or fact artifact cannot be materialised and digest-verified. It then registers baseline
analyzer/baseline facts, baseline analyzer/candidate facts, candidate analyzer/baseline facts and candidate
analyzer/candidate facts. This separates knowledge coverage from engine behavior.
The admission runner accepts only a frozen manifest naming analyzer, facts, acquisition, configuration and
population digests. The population manifest is produced and reviewed by `valid-detection-benchmark`; this spec
only consumes and validates it. The runner verifies its review and isolation fields, scores its paired
vulnerable/fixed cases and reviewed ordinary labels, refuses zero-byte or stale-anchor records, fails any change
over 120 seconds, and never converts indeterminate pairs to clean results.
## Data Models
### Canonical trace
Each trace carries:
- entry identity and target-knowledge evidence;
- exact hops and call-target completeness;
- operation kind, resource and authority;
- witnesses with kind, scope, provenance, resource, authority, dominance and match relations;
- control context and guard effects;
- completeness reasons and final path judgment;
- analyzer, fact-set and input digests.
Repository identity, repository size and function-size features remain outside decision input.
### Run manifest
The manifest contains population role (`development` or `admission`), immutable input digest, analyzer revision,
fact-set digest, acquisition-record digest, configuration digest, label-review state and spent-state marker.
The analyzer/fact bundle is a separate immutable input whose artifacts must be materialisable by revision and
digest; a manifest cannot redefine or synthesize the baseline.
## Error Handling ## Error Handling
- Exceptions inside `prove_change` per file are recorded under their own reason key - Parse, recognition, resolution, summary, CFG and judgment exceptions become stage-specific incompleteness reasons.
`error:<exception class name>` `[2026-10-29]` (7.3), never folded into `framework_unrecognised` or any other - Deadline, fan-out, depth and recursion limits preserve accumulated evidence and return indeterminate affected
quiet reason; the change is still proved for the other files. A provider never appears (no model call). A pairs.
checkout failure is an instrument failure on the record (`status="instrument_failure"`, reason type only). - Zero-byte reads and stale anchors are instrument failures that invalidate the run.
- Missing or mismatched manifest digests invalidate admission rather than falling back to development mode.
## Testing Strategy ## Testing Strategy
- Unit: each recogniser on a fixture file written in the test (not copied from clones) with positive and negative - Unit: the complete join table, dimension conjunction, witness aggregation and monotonicity laws; symbol
cases; `compose_paths` on a three-file fixture (depth 1, 2, 3, unreached, fan-out cut, same-name in two files); collisions and overload candidates; resource aliases and authority
`witnesses` for a Laravel route file, a Symfony attribute, a Tornado decorator. `[2026-10-29]`: implication; SCC termination; CFG unknown behavior; three-way aggregation.
`class_table`/`qualify` on a fixture with two same-name methods on different classes (distinct keys), two - Metamorphic: renaming a type preserves judgment; adding an unresolved overload can only preserve a judgment or
same-name methods with different arity (distinct keys), an unresolvable receiver (bare name + move it to indeterminate; removing semantic evidence cannot switch directly between discharged and unguarded.
`receiver_unresolved` counted), an abstract base method reached through a subclass (attaches) and through an - Integration: exact unguarded and discharged paths; a guard in a called helper; registration-site entries; base
unrelated class (does not); a programmatic `addServlet` registration fixture yielding entries at the attachment; resource mismatch; authority insufficiency; ambiguous calls; CFG unavailable; budget cutoff.
registered class's methods with registration evidence, and an unresolved registration yielding coverage only; - Runner: baseline/candidate bundle materialisation, four-arm attribution, paired vulnerable/fixed scoring,
a voter fixture (`isGranted` with attribute, `voteOnAttribute` implementation) yielding a `voter` witness with reviewed-manifest validation, freeze-digest guard, zero-byte and stale-anchor refusal, indeterminate rate, and
the attribute as resource; a resource-mismatch fixture (guard binds another object's resource -> operation hard 120-second gate.
stays undischarged).
- Integration: `prove_change` on a git fixture with a Stapler-style class: an unguarded `doTestConnection` that ## Migration Strategy
calls `lookupCredentials` yields one finding with hops and missing `path_guard`; the same with 1. Add abstract types and adapters without changing legacy checker defaults.
`Jenkins.get().checkPermission(...)` above the call yields none; a Laravel controller behind `auth` middleware 2. Add strict resolution, summaries and judgment behind the prover's opt-in path.
yields none; an unrecognised framework yields coverage only. `[2026-10-29]`: the guard sits one call deeper 3. Migrate traces and coverage, then enable the amended development arms.
than the handler (a helper checks permission before the operation) and discharges; a guard whose trace shows 4. Freeze facts and analyzer before running a separate admission population.
it dominating before the call produces no finding; an operation on a base handler class attaches to the
subclass route only; two classes with the same method name keep separate verdicts. Rollback is the previous prover arm and fact-set digest; baseline records remain immutable and comparable.
- Runner: a two-change fixture pool produces records the metrics summarise; a zero-byte read refuses;
a fixture finding anchored to an absent path refuses with `stale_anchor` `[2026-10-29]`; a fixture raising a
recognition exception counts it under `error:<class>` and not under `framework_unrecognised`
`[2026-10-29]`.
+236 -261
View File
@@ -1,304 +1,279 @@
# Requirements Document # Requirements Document
## Project Description (Input) ## Project Description (Input)
An access-control prover: a deterministic check that an operation needing authorization is reachable from an An access-control prover: a deterministic check that a protected operation is reachable from an attacker-facing
entry point without a check dominating the path, modelled per framework, built from the existing obligation facts, entry point without a required authorization check governing that path. The prover consumes acquired entry,
caller index and entry-point mapper, scored as a rule on the access-control training pool (recall on 23 fix operation and enforcement facts, resolves calls and resources conservatively, emits an ordered trace for every
changes, false alarms per 100 ordinary pushes) and feeding the proof arm. examined pair, and is measured separately on development and admission populations.
## Introduction ## Introduction
The maintainer's finding of 2026-10-14: the hunter's features (diff shape, function size, callers, entry-point The security object for access control is not the shape of a changed function. It is an ordered path from an
counts, name roles, textual obligation order) describe the shape of code around a change and cannot decide that attacker-facing entry to a protected operation, together with the authorization evidence that governs every
code is unsafe. Unsafety for access control is a property: an operation that needs authorization is reachable by a relevant control-flow and call-flow step. The 2026-10-28 development measurement established a useful baseline:
caller that no check has constrained. The access-control training pool (23 fix changes, 93 verified locations, 15 of 23 fix changes detected, 15.0 flagged ordinary pushes per 100, and no instrument failures. The subsequent
460 ordinary pushes, 23 application repositories) holds exactly that shape: a permission check added before a ablation showed that unresolved calls, over-broad resource matching, missing registration facts and silent
protected operation in a web-exposed method. Seventeen of the 23 fixes are Jenkins plugin web methods. fallbacks account for both missed detections and false alarms.
This spec delivers the prover for that one family, as a deterministic rule with no model call, measured on the This amendment makes uncertainty part of the result rather than a side counter. A pair is `unguarded`,
pool with the standard metrics, and emitting findings that carry the unguarded path so the proof arm can act on `discharged`, or `indeterminate`; degraded resolution, missing control-flow semantics, incomplete facts and
them. It is the increment that follows the annotation ablation in `hunter-learning-loop`. budget cutoffs cannot create a final security verdict. It also separates label-blind fact acquisition and
development regression from admission measurement on untouched repositories.
**Amendment 2026-10-29 (after the 2026-10-28 measurements).** Gates 1-3 measured the built prover on the pool: Concrete frameworks and repositories are benchmark evidence, not acceptance-criteria inventories. Target
recall 65.2% (15/23), 15.0 false alarms per 100 ordinary pushes, a zero-shot judge that costs no recall but vocabulary is acquired as reviewed data or recogniser strategies and is verified before promotion. The abstract
drops almost no false alarms. The taxonomy of the 8 misses and of 12 sampled kept false alarms points at one interpretation engine remains target-agnostic.
cross-cutting gap: the prover resolves too shallowly and too loosely. Operations and guards one call deeper
than a handler are invisible; same-name methods on different classes are conflated; operations declared on
abstract bases are projected onto routes that never implement them; coarse guards (a path guard, a generic
ownership check) discharge operations whose resource they do not bind. This amendment adds the resolution
requirements (Requirement 3 acceptance criteria 7-10, Requirement 2 acceptance criteria 9-10), the
programmatic-registration entry facts (Requirement 1 acceptance criterion 9), and the failure-visibility
criteria (Requirement 5 acceptance criterion 6, Requirement 7 acceptance criterion 3). Acceptance criteria
added or changed by this amendment carry the tag `[2026-10-29]`.
Per the specification-level steering rule, this amendment states **acquisition capabilities**, not per-target
inventories: the target vocabulary implied below (registration sites, handler-name families, policy-witness
shapes) is data the fact pipeline acquires per target -- a one-time initialization job per target can produce
it, and can extract the target knowledge already embedded in the repository's recogniser code into the same
structures -- with the engine's verification discipline admitting each row. The named repositories appear only
as benchmark evidence the acquired data must satisfy.
## Boundary Context ## Boundary Context
- **In scope**: entry-point recognition for the pool's frameworks; reachability paths from entry points to - **In scope**: acquired entry, operation and enforcement knowledge; exact and conservative call resolution;
touched functions through the existing caller index; framework enforcement points that discharge an obligation resource- and authority-sensitive witness matching; bounded interprocedural summaries and concrete evidence
outside the function (route middleware, controller attributes, declared public routes); resolution of calls on paths; control-flow-sensitive dominance; three-way pair judgments; failure-visible coverage; development and
those paths precise enough to place operations and guards in the function that holds them `[2026-10-29]`; admission runners.
the verdict per change; a runner that scores the verdict as an arm on the pool; a coverage statement per - **Out of scope**: taint flow; the Joern engine; model calls inside the prover; vulnerability families other than
repository. access control; client-side presentation gating; pre-push hook wiring; proof execution; hand-tuning vocabulary
- **Out of scope**: taint flow, the Joern engine, any model call inside the prover, new families beyond access from evaluation labels.
control, a project-owned declared policy file (exists in `authorization-obligations` and is consumed as is), - **Adjacent expectations**: `authorization-obligations` supplies the existing checker and fact contracts;
the unseen pilot measurement (G1 exit, run by the maintainer after this arm exists), the pre-push hook wiring `valid-detection-benchmark` supplies reviewed populations; `search-with-proof` consumes unguarded traces;
(`pre-push-safety-net`), client-side JavaScript permission gating (hiding controls in a browser frontend -- `pre-push-safety-net` may consume the arm only after admission passes.
the byobu miss of gate 3 is not a server-side security object) `[2026-10-29]`, tuning the zero-shot judge
prompt (gate 2 measured the judge inert on this pool; the lever is the trace, not the prompt)
`[2026-10-29]`, re-auditing the pool's negative pushes (owned by `valid-detection-benchmark`)
`[2026-10-29]`.
- **Adjacent expectations**: `authorization-obligations` owns the facts schema, the checker and the dominance
protocol; this spec extends facts and supplies paths and witnesses through the checker's existing parameters.
`hunter-learning-loop` owns the feature table and the hunter arms; the prover is scored beside them with the
same metric functions. `search-with-proof` consumes the finding's path and operation to build a demonstration.
`valid-detection-benchmark` owns the pool: 3 of 12 sampled "ordinary" pushes carry real missing-check bugs
(pre-2019 negatives), so false-alarm counts read against that pool overstate the prover's error until the
negatives are re-audited `[2026-10-29]`.
## Requirements ## Requirements
### Requirement 1: Entry points of the pool's frameworks ### Requirement 1: Acquired attacker-facing entry points
**Objective:** As a maintainer, I want the entry-point mapper to name the web-exposed methods of the frameworks **Objective:** As a maintainer, I want entry recognition to consume verified target knowledge, so that new targets
the pool uses, so that reachability starts where an attacker starts. can be covered without adding target vocabulary to the analysis engine.
#### Acceptance Criteria #### Acceptance Criteria
1. When a Java file declares a public method whose name starts with `do` in a class that extends or implements 1. When verified target knowledge describes a declarative entry shape, the mapper shall record every matching
a Jenkins or Stapler exposed type (Descriptor, Action, RootAction, Plugin, UnprotectedRootAction, repository-defined handler as an entry with its source range, trust boundary and recognition evidence.
AbstractDescribableImpl, or a parameter typed StaplerRequest, StaplerResponse or annotated QueryParameter), 2. When verified target knowledge describes a call-site registration, the mapper shall resolve the registered
the mapper shall record an entry point of kind `route` with trust boundary `http_request` and the method's repository-defined handler and record its request methods as entries with the registration site and resolved
line range, with access `public` when the class implements UnprotectedRootAction and `unknown` otherwise. handler identity as evidence.
2. When a PHP file declares a Symfony controller action (a method carrying a `Route` attribute or annotation, or 3. If a registration target cannot be resolved to repository-defined code, the mapper shall emit no entry for it
a class extending AbstractController with public methods named by a routes file), the mapper shall record it and shall record `registration_unresolved` coverage.
as a `route` entry with the attribute's path as evidence. 4. When acquired knowledge proves that an entry is intentionally public, the mapper shall record access `public`;
3. When a PHP Livewire component or Laravel controller method is registered by a route file, a `Route::` call, a otherwise, the mapper shall record access `unknown` rather than infer public access.
`wire:` binding or a public method on a class extending Livewire Component, the mapper shall record it as a 5. The mapper shall preserve the promoted fact or strategy identifier and the concrete source evidence on every
`route` entry, and shall carry a declared access of `public` only when the route file names no middleware. entry record.
4. When a TypeScript or JavaScript file under a Remix or React Router routes directory exports `action` or 6. If no promoted entry knowledge covers a file, the prover shall record `framework_unrecognised` and shall not
`loader`, the mapper shall record each export as a `route` entry with trust boundary `http_request`. interpret the absence of entries as a clean result.
5. When a Python class extends a Tornado RequestHandler or WebSocketHandler, the mapper shall record its `get`, 7. When target entry knowledge is added or updated, the system shall apply it through data or a registered
`post`, `put`, `delete`, `patch` and `open` methods as `route` entries. recognition strategy without changing target-agnostic analysis modules.
6. When a Java class extends a Servlet, a Jetty handler or a JAX-RS resource, the mapper shall record the
request methods as `route` entries (existing Servlet and JAX-RS rows are kept; Jetty handlers are added).
7. The mapper shall record every recogniser's evidence strings so a finding can name why a method is an entry.
8. If a file's framework is not recognised, then the mapper shall record nothing for it and the coverage
statement (Requirement 7) shall count the file as unchecked; silence is never a pass.
9. `[2026-10-29]` When the repository's own code registers a handler at a call site rather than declaring it
through annotations or configuration, the mapper shall record the registered class's request methods as
`route` entries with the registration site and the registered class as evidence, so that a server assembled
in code still yields entry points. The registration-site vocabulary for a target is acquirable data under
Requirement 2 acceptance criterion 7's discipline, not engine code; Jetty's programmatic servlet
registrations are the first case (the seatunnel miss of gate 3).
### Requirement 2: Enforcement points outside the function ### Requirement 2: Acquired protected operations and enforcement evidence
**Objective:** As a maintainer, I want checks that frameworks apply outside the handler to count as discharges, **Objective:** As a maintainer, I want operations and authorization evidence represented as verified knowledge,
so that a guarded route is not reported because its handler body shows no check. so that the prover compares the correct action, resource and authority.
#### Acceptance Criteria #### Acceptance Criteria
1. When a Laravel route registration attaches `auth`, `can:`, `role` or `permission` middleware, or a controller 1. When promoted operation knowledge matches a call or recognised handler, the prover shall record the protected
calls `authorizeResource` or `middleware` in its constructor, the prover shall supply a discharge witness of operation, its required authorization kinds, its resource identity and any required authority.
scope `router` for every handler the registration covers. 2. When promoted enforcement knowledge matches a statement, decorator, route, policy decision or equivalent
2. When a Symfony action carries `IsGranted`, `Security` or a `denyAccessUnlessGranted` call, or security.yaml's construct, the prover shall record a witness with its scope, resource identity, authority and provenance.
`access_control` covers its path with a role, the prover shall supply a `decorator` or `router` witness. 3. When a handler name itself denotes a protected action, the prover shall apply that operation only to an entry
3. When a Jenkins web method, or a method it calls on its own path, calls `checkPermission`, recognised for the same promoted target knowledge.
`checkAnyPermission` or guards on `hasPermission` with an exit, the existing statement witnesses shall apply; 4. When a route or declared policy marks an entry public, the prover shall waive only a request-level path guard
`RequirePOST`, `POST` and `Interceptor` annotations shall not count as authorization. and shall preserve resource, ownership and authority obligations.
4. When a Remix action or loader calls a session requirement (`requireUser`, `requireUserId`, 5. When a witness binds a different resource or an insufficient authority, the prover shall not use that witness
`requireAuthentication`, `authenticateApiRequest`, `authorize`) before the operation, the prover shall treat to discharge the operation.
that call as a path guard, and when the operation reads a record by an identifier from the request without an 6. When a witness is request-global, the prover shall use it without resource equality only when the operation
ownership constraint bound to the authenticated principal the prover shall report the missing fact explicitly declares that witness kind sufficient at request scope.
`ownership_check` (the pool's CWE-639 case). 7. If resource identity or authority compatibility cannot be decided, the prover shall treat the witness match as
5. When a Tornado handler is decorated with `authenticated` or `tornado.web.authenticated`, or the class unknown rather than matching it broadly or rejecting it as definitely unrelated.
overrides `get_current_user` and checks it before the operation, the prover shall treat that as a path guard. 8. When one authority implies another, the prover shall apply that implication only from promoted target
6. Where a declared policy marks a route public, the prover shall suppress `path_guard` findings for it and knowledge and shall record the implication used in the trace.
keep `ownership_check` findings, as the checker already does. 9. When target knowledge is extracted or proposed, the acquisition workflow shall verify its source, version,
8. Where a framework's security model makes the web method itself the protected operation (Jenkins form region and match evidence before promoting it to closed data or an enabled strategy.
validation and list-filling methods `doCheck*`, `doFill*Items`, `doTest*`, `doValidate*`, `doVerify*`, which 10. If proposed target knowledge contradicts the inspected source or lacks verification evidence, the acquisition
Jenkins requires to check a permission; Tornado handler methods; a Remix action), the facts shall be able to workflow shall reject it with a reason and shall not modify promoted knowledge.
name the obligation by handler name pattern and framework, and the prover shall apply such an operation only
to functions the mapper recognised as entries of that framework, at the method's declaration line.
7. The prover shall write every enforcement fact as closed data in the obligation facts files, with the header
naming the clones and references checked, and shall never add a name that does not occur in the checked code
or the framework's documentation. A proposed fact (from a maintainer or an LLM proposal pass) is promoted to
a closed row only after the same header discipline is satisfied `[2026-10-29]`; per-target vocabulary enters
as data through this gate, never as engine code.
9. `[2026-10-29]` The facts shall be able to name, by handler-name pattern and by the privileged calls the
handler makes, webhook and build-trigger obligations and protected-data exposure obligations, applied at the
method's declaration line to entries the mapper recognised. The name families for a target are acquirable
data under acceptance criterion 7's discipline -- a one-time initialization pass per target can produce them
(and can extract the vocabulary already embedded in the repository's recogniser code into those structures)
-- and the pool's Stapler webhook and data-exposure handlers are the first cases (the git-plugin, p4,
analysis-core and global-build-stats misses of gate 3 all reached the entry and checked no operation).
10. `[2026-10-29]` A guard shall discharge an operation only when it binds that operation's resource: when the
guard constrains a different path, a different object, or a permission level the operation does not require,
the prover shall treat the operation as undischarged (the crx and kimai misses of gate 3: a generic
`path_guard` or `ownership_check` dominated a credentials form-fill and invoice actions whose resource it
never touches). Where the target's policy layer decides access by an attribute or subject of the request --
a voter, policy, or capability decision reachable from the action or a function it calls -- the prover shall
supply a policy witness scoped to that action's operations with the attribute or subject as its resource;
Symfony's voter decisions are the first case.
### Requirement 3: Reachability paths through the caller index ### Requirement 3: Conservative interprocedural reachability
**Objective:** As a maintainer, I want a path from an entry point to each touched function, so that an **Objective:** As a maintainer, I want calls resolved to stable program identities and paths carried through
obligation inside a helper is judged by the checks on the way to it. helpers, so that unrelated same-name code neither creates nor suppresses findings.
#### Acceptance Criteria #### Acceptance Criteria
1. When a change is checked out at head, the prover shall build, for every touched function and every function 1. When a touched function or reachable helper contains a protected operation, the prover shall construct caller
that holds a protected operation it calls, the caller chains that end at an entry point, through the chains from recognised entries to that operation within the configured work budget.
per-change caller index, up to a configurable depth with default 3 and a configurable fan-out with default 25 2. When a call target can be identified by module, enclosing type, method and signature, the prover shall keep
callers per step. that identity distinct from every other same-name target.
2. The prover shall emit each chain as a path record whose hops are `path::function` strings from the entry to 3. When more than one call target remains feasible, the prover shall preserve the finite candidate set and shall
the operation's function, in the shape `check_obligations` and `OrderDominance` already consume. mark the call incomplete rather than merge the candidates under a bare name.
3. If a touched function is itself an entry point, then the prover shall emit the one-hop path. 4. If no bounded candidate set can be established, the prover shall mark the call unresolved and shall not use it
4. If no chain reaches an entry point within the depth, then the prover shall record the function as to create a final discharged or unguarded judgment.
`unreached` in the coverage statement and shall produce no finding for it. 5. When an operation or entry is declared on a base type, the prover shall attach it only to repository-defined
5. The prover shall deduplicate chains, shall stop at the first entry point on a chain, and shall bound the work concrete descendants whose resolved identities support that attachment.
so a change of the pool's largest repositories finishes within the budget of Requirement 6. 6. When a helper calls another helper that contains an operation or governing authorization evidence, the prover
6. When two functions share a name in different files, the prover shall keep the chains separate by file. shall continue the analysis through that call while budget remains.
7. `[2026-10-29]` When a function on a path calls another function of the repository that holds a protected 7. When recursion is present, the prover shall terminate and shall preserve the accumulated operations,
operation, the prover shall extend the path through that call (within the depth and fan-out budget) so the authorization effects and incompleteness reasons for the recursive component.
operation is checked even though no touched function holds it directly; an operation buried one call deeper 8. If depth, fan-out, time or another work bound truncates exploration, the prover shall record the exact cutoff
than the handler shall not be invisible (the gate 3 operation-fact misses). reason and shall treat affected pairs as incomplete.
8. `[2026-10-29]` When a guard sits in a called function on the path, before the call or before the operation, 9. When a complete chain is materialised, the prover shall emit its hops from entry to operation with the call
the trace shall carry it with its own function, line and kind, and dominance shall honour it as if it sat in site of every transition.
the hop's own function; a guard one call deeper than the handler shall still discharge (the dependency-track 10. When multiple complete chains reach the same operation, the prover shall evaluate each chain independently so
and shopper false alarms of gate 3). that a guarded chain cannot hide an unguarded chain.
9. `[2026-10-29]` The prover shall resolve calls by signature and receiver: same-name methods on different
classes (the vault and p4 false alarms) and same-name overloads with different parameter lists shall stay
distinct, and an operation or entry declared on an abstract or base class shall attach only to paths whose
concrete class the repository defines (the mage-ai false alarm: `BaseDetailHandler.get` projected onto an
unrelated internal route).
10. `[2026-10-29]` A guard already recorded on a hop before a call shall be carried into the pair's trace and
dominance verdict; the prover shall not emit a finding whose own trace shows a dominating guard of the
required kind (the trigger.dev false alarm of gate 3).
### Requirement 4: The verdict ### Requirement 4: Access-control verdicts
**Objective:** As a developer about to push, I want a finding only when the tool can show an unguarded path to a **Objective:** As a developer about to push, I want a finding only for a demonstrated unguarded path, so that every
protected operation, so that every alarm names what to add and where. alarm names a concrete security obligation and repair location.
#### Acceptance Criteria #### Acceptance Criteria
1. When a protected operation is reached by a path record and no witness of a required discharger kind 1. When a complete path reaches a protected operation, at least one required authorization kind is proven absent,
dominates it along that path or inside its function, the prover shall emit one finding per operation and and every other required kind is proven either governing or absent, the prover shall judge the pair
missing kind with family `access_control`, the operation's path and line, the entry point, the hops, the `unguarded` and shall report each absent kind.
missing kind and the evidence strings. 2. When every required authorization kind is proven to govern the operation on a complete path, the prover shall
1a. The prover's premise shall be explicit: a recognised entry point is protected unless the framework or a judge the pair `discharged`.
declared policy marks it public. The checker shall apply that premise only when the caller opts in, shall 3. If the evidence is insufficient for either final judgment, the prover shall judge the pair `indeterminate`.
label such findings `unguarded_path` (distinct from `declared_policy_violation` and 4. When a pair is `unguarded`, the prover shall emit one finding per operation and missing authorization kind with
`consistency_violation`), and shall say in the evidence that no policy clause and no sibling norm backed it. family `access_control`, exact source anchor, entry, hops and evidence.
The checker's default behaviour with path records (no clause, no sibling norm, no finding) is unchanged. 5. When a pair is `discharged` or `indeterminate`, the prover shall emit no vulnerability finding for that pair.
2. When every required discharger kind is dominated, the prover shall emit no finding and shall count the 6. The prover shall apply the recognised-entry-is-protected premise only in its explicit opt-in mode and shall
operation as `discharged` in the coverage statement. identify that premise in the evidence.
3. The prover shall never emit a finding for a function whose file failed to parse, whose language has no 7. The prover shall make no model call and shall return findings in the existing hunt-defect contract.
obligation facts, or whose framework is unrecognised; each such case shall be counted under its reason.
4. The prover shall make no model call.
5. The prover shall report findings in the hunt defect shape (`path`, `line`, `family`, `evidence`,
`function`), so the existing scoring, the proof arm and the pre-push admission consume them unchanged.
6. When a touched function holds no protected operation and reaches none, the prover shall emit nothing for it.
### Requirement 5: Scoring as a rule arm on the pool ### Requirement 5: Development measurement
**Objective:** As a maintainer, I want the prover scored on the training pool with the standard metrics, so that **Objective:** As a maintainer, I want repeatable development measurements, so that changes to facts and analysis
its number stands beside the hunter arms and decides whether it becomes a G1 arm. can be attributed without being mistaken for admission evidence.
#### Acceptance Criteria #### Acceptance Criteria
1. The runner shall run the prover over every change of a pool slice on the training clones, one checkout per 1. When a development population is run, the runner shall write one resumable record per change with analyzer,
change, and shall write one record per change in the replay record shape that `benchmarks.valid.metrics` fact-set, population and input digests.
scores (id, kind, repo, status, findings, bytes read, seconds). 2. When development records are summarised, the runner shall report recall on fix changes and verified locations,
2. The runner shall report recall on fix changes and on verified locations with Wilson intervals, false alarms flagged ordinary pushes per 100, precision, vulnerable/fixed pair correctness, instrument failures and runtime
per 100 ordinary pushes, precision, instrument failures, and seconds per change, and shall refuse to distributions with uncertainty intervals where defined.
summarise when any record read zero bytes of a tree git holds. 3. The runner shall report a stage funnel from entry recognition through call resolution, operation discovery,
3. The runner shall report a per-framework and per-repository coverage table: entry points found, touched control-flow evaluation, witness matching and pair judgment.
functions reached, operations checked, functions unchecked by reason. 4. The runner shall report per-target-family and per-repository coverage without disclosing reserved repository
4. The runner shall checkpoint records per change and resume by skipping finished ones. identities in shared records.
5. The record of a run shall carry counts, digests and framework names, never repository names of reserved 5. When comparing an amended analyzer and amended facts, the runner shall support the four registered arms formed
populations, as the reserved-repository guard requires. by old/new analyzer crossed with old/new fact set.
6. `[2026-10-29]` The runner shall refuse to summarise a run whose findings anchor to a path that is not 6. If any record reads zero bytes for a file present in the analysed tree or anchors a finding outside files read,
present in the change's analysed tree, the same way it refuses zero-byte reads: a finding on an absent file the runner shall mark an instrument failure and shall refuse the summary.
is an instrument defect, not a result (the crx stale anchor of gate 3). 7. The runner shall label results from populations used to choose facts or analyzer behavior as development or
regression evidence and shall not present them as admission evidence.
### Requirement 6: Budget ### Requirement 6: Pre-push budget
**Objective:** As a developer, I want the prover to finish inside the pre-push window, so that it can run on **Objective:** As a developer, I want the prover to finish inside the pre-push window, so that eligible detection
every push. can run on every push.
#### Acceptance Criteria #### Acceptance Criteria
1. The prover shall finish a change of the pool within 120 seconds on the development host, measured by the 1. When a measured change exceeds 120 seconds, the runner shall fail the budget gate and shall identify the
runner, and the runner shall list every change over that bound. change, stage and elapsed time.
2. The prover shall reuse the per-change context caches (product files, texts, call index, file IR, entry 2. The prover shall reuse per-change file, text, syntax, call, entry, symbol and summary caches and shall not read
points) and shall not walk or read the product tree more than once per change. the product tree more than once per change.
3. If analysis stops because of a deadline, the prover shall return incomplete coverage and indeterminate affected
pairs rather than a partial clean verdict.
### Requirement 7: Coverage statement ### Requirement 7: Failure-visible coverage
**Objective:** As a developer, I want the tool to say what it did not check, so that no finding count reads as a **Objective:** As a developer, I want the tool to state what it could not decide, so that silence never reads as a
clean bill. clean bill of health.
#### Acceptance Criteria #### Acceptance Criteria
1. The prover shall return, with its findings, the counts of files by language, files whose framework was 1. The prover shall report files by language, recognised entry families, entries, reached and unreached targets,
recognised, entry points found, touched functions reached and unreached, operations checked and discharged, operations, discharged pairs, unguarded pairs and indeterminate pairs.
and functions unchecked by reason (`parse_failed`, `language_unsupported`, `framework_unrecognised`, 2. The prover shall count unchecked or incomplete work by specific reason, including parse failure, unsupported
`unreached`, `no_facts`). language, unrecognised target, absent facts, unresolved registration, ambiguous or unresolved call, exploration
2. When the coverage statement shows zero recognised frameworks for a repository, the runner shall mark the cutoff, unavailable control flow, unknown resource, unknown authority and analysis exception.
repository `uncovered` in its summary, and recall for that repository shall be reported separately from the 3. If no target family is recognised for a repository, the runner shall mark the repository `uncovered` and shall
covered recall. report its recall separately from covered recall.
3. `[2026-10-29]` When a recognition or resolution pass over a file raises an exception, the coverage statement 4. When a recognition, resolution, summary or judgment pass raises an exception, the prover shall record the
shall count that file under a reason that names the exception class, and shall never fold it into exception class at that stage and shall not fold it into a quiet coverage reason.
`framework_unrecognised` or any other quiet reason (the byobu ValueErrors of gate 3 surfaced only by 5. When the same pair has multiple incompleteness reasons, the prover shall preserve every reason in the trace and
inspection of the record). aggregate each reason in coverage.
6. The runner shall report the indeterminate rate separately and shall not include indeterminate pairs in either
clean or discharged counts.
### Requirement 8: Proof-arm feed ### Requirement 8: Proof-arm feed
**Objective:** As the proof arm, I want each finding to carry what a demonstration needs, so that a finding **Objective:** As the proof arm, I want each unguarded finding to carry a complete analysis witness, so that a
can be proved or dismissed without re-deriving it. demonstration can be attempted without re-deriving the decision.
#### Acceptance Criteria #### Acceptance Criteria
1. Each finding shall carry the entry point record (kind, path, line, evidence), the hop list, the operation 1. When a finding is emitted, it shall carry the entry record, exact hop list, operation, missing authorization
(id, kind, calls matched, line) and the missing discharger kind. kind, resource and authority relations, pair judgment, trace identifier and completeness evidence.
2. The finding id shall be `obligation:<operation kind>:<path>:<line>:<missing>` as the checker already emits. 2. The finding identifier shall remain stable for the same operation, source anchor and missing authorization
kind across deterministic replays.
3. If the pair is indeterminate, the prover shall expose its trace only as diagnostic coverage and shall not send
it to the proof arm as a vulnerability finding.
### Requirement 9: The ordered trace is the prover's output for every examined pair ### Requirement 9: Ordered traces for every examined pair
**Objective:** As the maintainer, I want every (entry, operation) pair the prover examined serialised as an ordered **Objective:** As a maintainer, I want every examined pair represented by the same deterministic trace, so that
trace with its verdict, so that a decision model sees the security-relevant structure and nothing else. decisions and degradations can be inspected and compared.
#### Acceptance Criteria #### Acceptance Criteria
1. For every path record and every protected operation at its end, the prover shall emit one trace: the entry 1. When the prover examines an entry-operation path, it shall emit one trace containing the entry, ordered hops,
(kind, trust boundary, declared access, parameter names), each hop in order (function, the call line, the operation, authorization witnesses, resource and authority relations, control-flow relations, completeness
guards that dominate the call in that function with their kind, scope, line and provenance, the entry state and pair judgment.
parameters and request-derived values forwarded through the call), the operation (kind, fact id, line, the 2. The prover shall emit traces for unguarded, discharged and indeterminate pairs alike.
provenance of each argument: request_input, authenticated_context, constant, unknown), the guards after the 3. When an argument or resource is forwarded through a call, the trace shall preserve its origin through the
operation, and the verdict (required kinds, dominating kinds, missing kind or discharged). resolved hop; if origin cannot be followed, the trace shall record `unknown`.
2. The prover shall emit traces for discharged pairs and for unguarded pairs alike, and shall list unreached 4. The trace shall carry target knowledge identifiers and source evidence needed to reproduce the judgment but
targets with their reason, so that negatives carry the same structure as positives. shall omit repository identity, file-size and repository-size features from decision input.
3. The argument provenance shall be traced within each hop's function through the IR's binds from the entry's 5. The trace shall serialise to one deterministic canonical text form and one deterministic canonical JSON form.
parameters and the request-source facts, forwarded through the call sites on the path; a value the trace 6. When a work bound or fallback would have been required, the trace shall record the cutoff or unavailable
cannot follow shall be `unknown`, never `constant`. semantic fact rather than silently substituting a weaker analysis.
4. The trace shall carry no language, framework name, repository name, file size, line count, or any count over 7. The runner shall persist traces and their analyzer, fact-set and input digests with the corresponding record.
the function or the repository: only the ordered components above.
5. The prover-arm record shall carry the traces, and a trace shall serialise to one canonical text form (one
component per line, in order) and one canonical JSON form, both deterministic.
6. The hunt defect of an unguarded pair shall reference its trace, so the proof arm and the pre-push admission
can show the path.
### Requirement 10: Control flow decides dominance, and the trace says so ### Requirement 10: Control-flow-sensitive dominance
**Objective:** As the maintainer, I want a guard to count only when control flow makes the operation unreachable **Objective:** As a maintainer, I want authorization evidence to count only when control flow guarantees it governs
without it, so that a check in a branch the attacker can avoid, or one whose result is ignored, is not a discharge. the operation, so that avoidable or ignored checks do not discharge obligations.
#### Acceptance Criteria #### Acceptance Criteria
1. The prover shall build a structural control-flow graph per function on a path from the syntax tree it already 1. When authorization evidence appears before an operation, the prover shall determine whether every feasible
parses: statement blocks, if and else branches, switch arms, loops, try, catch and finally, return, throw, path to the operation passes that evidence.
break and continue, and the function's exit; with no grammar for a construct the function shall be marked 2. When a guard's failing branch terminates or the operation exists only in the passing branch, the prover shall
`cfg_unavailable` and judged by order with that degradation recorded. record the guard effect and may treat it as governing the operation when resource and authority also match.
2. A guard shall dominate an operation when every path from the function's entry to the operation passes the 3. When a predicate result is ignored or appears only on some paths, the prover shall not treat it as a governing
guard's statement (dominators over the graph); the trace shall record each guard's relation to the operation authorization check.
as `all_paths`, `some_paths`, `none` or `after`. 4. When an operation is inside a loop, exception region or conditional branch, the trace shall record that control
3. The trace shall record each guard's failure effect: `throws`, `returns`, `exits` (break, continue, exit call), context and any exception handling that can bypass the guard.
`branches` (the operation lies only inside the guard's passing branch), or `value_only` when the guard's 5. When route- or declaration-scoped authorization evidence covers the whole handler, the prover shall preserve
result is not consumed by a branch or an exit before the operation; a `value_only` guard shall not discharge. that declared scope without inventing a statement location.
4. The trace shall record the operation's control context: the branch conditions it sits under in order (the 6. If control-flow semantics or source-to-control-flow correspondence is unavailable, the prover shall mark the
guard's own condition when it is one), whether it is inside a loop, inside a try whose catch swallows the affected relation unknown and the pair indeterminate rather than decide it by textual order.
guard's exception type, or on an exception path. 7. The existing order-based checker mode shall remain available to callers that do not opt into prover semantics.
5. Router and decorator witnesses shall keep their scope semantics (they dominate the handler as a whole).
6. The parameter list of a Java entry shall carry parameter names only, never annotation names; the arguments
of a handler-named operation shall be the handler's own parameters with their provenance.
7. The existing order-based dominance shall remain the default of the checker; the prover shall select the
control-flow dominance, and the checker's tests shall stay unchanged.
### Requirement 11: Three-way abstract judgment
**Objective:** As a maintainer, I want uncertainty to propagate monotonically through the analysis, so that losing
precision cannot manufacture a vulnerability or a discharge.
#### Acceptance Criteria
1. The prover shall represent call resolution, path completeness, resource compatibility, authority compatibility
and dominance with values that distinguish definite, possible, absent and unknown evidence.
2. When all required evidence on a complete path is definite and at least one required obligation has no governing
witness, the prover shall produce `unguarded` and shall report every such obligation, including when other
required obligations are definitely governed.
3. When every required obligation has a definite governing witness on a complete path, the prover shall produce
`discharged` for that path.
4. If any required relation is possible or unknown, or path enumeration is incomplete, the prover shall produce
`indeterminate` unless another complete path already proves `unguarded`.
5. When multiple paths reach one operation, the prover shall aggregate them as `unguarded` if any complete path
is unguarded, `discharged` only if enumeration is complete and every path is discharged, and `indeterminate`
otherwise.
6. When analysis precision is reduced by adding an unresolved candidate or removing semantic information, a final
judgment shall remain unchanged or move to `indeterminate`; it shall not switch directly between `unguarded`
and `discharged`.
7. The prover shall keep abstract-domain operations deterministic and independent of target names.
8. The runner shall expose judgment counts and transitions between compared arms so that precision loss cannot be
hidden inside aggregate recall or false-alarm metrics.
### Requirement 12: Label-isolated admission workflow
**Objective:** As a maintainer, I want admission measured on untouched, reviewed evidence after the analyzer and
facts are frozen, so that a better development score is not mistaken for generalisation.
#### Acceptance Criteria
1. When an admission run is prepared, the workflow shall freeze the analyzer revision, promoted fact-set digest,
acquisition record, configuration and population manifest before opening admission labels.
2. While acquiring target knowledge for an admission repository, the workflow shall use repository source and
approved documentation without vulnerability, fix-location or ordinary-change labels.
3. If acquisition observes evaluation labels or is changed in response to an admission result, the workflow shall
mark that population spent and shall not use it for admission.
4. When ordinary changes are prepared for a false-alarm denominator, the benchmark workflow shall review and
remove or relabel changes that contain real missing-authorization defects before scoring.
5. When a vulnerable case has a fixed twin, the admission run shall require the expected unguarded trace on the
vulnerable side and quiet or discharged behavior at the corresponding fixed operation.
6. When an amended prover is considered for the G1 arm, the workflow shall measure it on untouched repositories
that were not used to choose facts, recognition strategies, abstract-domain behavior or thresholds.
7. The admission report shall include recall, verified-location recall, flagged ordinary pushes per 100,
precision, paired vulnerable/fixed correctness, indeterminate rate, coverage, runtime and instrument failures
with sample counts and uncertainty intervals.
8. If the budget gate, instrument gate or pre-registered quality criteria fail, the workflow shall return NO-GO
and shall not enable the arm in pre-push admission.
9. The workflow shall retain the current 23-repository population as development regression evidence and shall
require a separate untouched population for admission claims.
@@ -205,3 +205,72 @@ records the code survey behind the design amendment (same date tag).
lane (deferred injection spec) is the designated place to grow type facts, not this spec. lane (deferred injection spec) is the designated place to grow type facts, not this spec.
- `down_depth` widens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository - `down_depth` widens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository
seconds-per-change in the runner summary will show any cost. seconds-per-change in the runner summary will show any cost.
---
## Abstract judgment and admission amendment -- 2026-10-29
### Context
The design review after the capability-level lift found that the proposed amendment still allowed two degraded
instruments to participate in binary decisions: unresolved receivers fell back to a bare-name call key, and
unavailable CFGs fell back to source order. It also found that the fact-initialization task used known development
misses and then remeasured the same population, which is valid regression work but cannot qualify an admission arm.
### Synthesis
1. **Generalization**: call certainty, path completeness, resource compatibility, authority compatibility and
dominance are instances of one evidence-relation problem. They share a finite domain
(`MUST | MAY | NO | UNKNOWN`) and one incompleteness record instead of separate booleans and counters.
2. **Build vs adopt**: the repository already owns the semantic IR, structural CFG, obligation checker and path
records. A small abstract-domain layer and monotone function summaries extend those contracts; adopting another
graph or abstract-interpretation engine would duplicate the current architecture and violate steering.
3. **Simplification**: the prover keeps one checker and one concrete trace. Function summaries accelerate and
stabilize interprocedural discovery, but final evidence remains a materialised path. No second verdict engine is
introduced.
### Decisions
#### Decision: three-way judgment
- **Selected approach**: every path and aggregate pair is `unguarded`, `discharged`, or `indeterminate`.
- **Invariant**: less precise evidence may preserve a verdict or move it to `indeterminate`; it may not switch
directly between the two final verdicts.
- **Algebra**: `MUST` and `NO` are incomparable, both below `MAY`, which is below `UNKNOWN`. Alternative
evidence uses the least-upper-bound table; dimensions of one witness use conjunction; witnesses for one
obligation use existential aggregation. These operations are distinct so `MUST + NO` on alternative paths
becomes `MAY`, while one definite governing witness remains sufficient for the obligation it covers.
- **Consequence**: prover mode does not use textual order as a semantic fallback. Legacy checker callers retain it.
#### Decision: stable symbols and finite candidate sets
- **Selected approach**: identify repository declarations by module, enclosing type, method and available signature.
Partial resolution returns a finite candidate set plus incompleteness; unbounded resolution returns unresolved.
- **Rejected approach**: global bare-name fallback, because it conflates unrelated methods and can both invent paths
and borrow unrelated guards.
#### Decision: resource and authority are separate abstract values
- **Selected approach**: match witness kind, scope, resource and authority independently, joining each relation into
the pair judgment. Request-global guards are valid only when the operation fact explicitly allows that scope.
- **Rejected approach**: `None` as a resource wildcard. It cannot distinguish intentionally global authentication
from missing resource evidence.
#### Decision: summaries discover; concrete paths prove
- **Selected approach**: monotone summaries over strongly connected call components collect operations, witnesses,
call target sets and incompleteness. Concrete paths are materialised from exact edges for trace evidence.
- **Trade-off**: summary and path code add one engine boundary, but they remove recursion and depth handling from
ad-hoc traversal and make every cutoff explicit.
#### Decision: development attribution before admission
- **Selected approach**: compare old/new analyzer crossed with old/new facts on the current development population,
then freeze analyzer, facts, acquisition record, configuration and a separate untouched population for admission.
- **Consequence**: the 2026-10-28 numbers remain the immutable baseline. A better rerun on the same repositories is
regression evidence, not a G1 admission claim.
### Risks and mitigations
- **Indeterminate inflation**: strict uncertainty may initially lower apparent coverage. Report the stage funnel and
reason distribution; improve the dominant reason rather than weakening the lattice.
- **Summary cost**: a repository-wide symbol graph may threaten the pre-push budget. Reuse Context caches, use finite
summary sets, and fail the budget gate when any measured change exceeds 120 seconds.
- **Authority vocabulary growth**: permission implication can become target-specific code. Keep implications in
promoted facts and include the fact identifier in every match.
- **Admission scarcity**: the current pool is already development data. Source and review a separate population
through `valid-detection-benchmark`; do not relabel the current pool as untouched.
+5 -5
View File
@@ -1,15 +1,15 @@
{ {
"feature_name": "access-control-prover", "feature_name": "access-control-prover",
"created_at": "2026-10-14T13:48:18Z", "created_at": "2026-10-14T13:48:18Z",
"updated_at": "2026-10-29T15:40:00Z", "updated_at": "2026-10-29T12:16:52Z",
"language": "en", "language": "en",
"phase": "requirements-generated", "phase": "tasks-generated",
"approvals": { "approvals": {
"requirements": {"generated": true, "approved": false}, "requirements": {"generated": true, "approved": true},
"design": {"generated": true, "approved": false}, "design": {"generated": true, "approved": true},
"tasks": {"generated": true, "approved": false} "tasks": {"generated": true, "approved": false}
}, },
"ready_for_implementation": false, "ready_for_implementation": false,
"depends_on": ["authorization-obligations", "valid-detection-benchmark", "hunter-learning-loop"], "depends_on": ["authorization-obligations", "valid-detection-benchmark", "hunter-learning-loop"],
"approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility); requirements await re-approval, then design and tasks must be revisited against the amendment before implementation resumes. 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design; design.md amended in merge mode against the amendment (resolution component, resource-bound and voter witnesses, programmatic Jetty registration, stale-anchor refusal, error reasons), tasks.md still to be regenerated after design approval. 2026-10-29 (latest): maintainer directives lifted the specification to the capability level -- per-target vocabulary is acquired data (one-time initialization job with extraction and proposal modes behind the facts gate), never enumerated in acceptance criteria; engine/target split, GoF pattern discipline, complexity caps, and idiomatic naming recorded in .kiro/steering/code-standards.md; breadth map added at .kiro/steering/breadth-map.md. requirements.md, design.md and tasks.md were re-lifted accordingly; all three phases re-opened for approval in order." "approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility). 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design. 2026-10-29 (capability lift): per-target vocabulary became acquired data; engine/target split, pattern discipline and complexity caps were recorded in steering; all phases reopened. 2026-10-29 (abstract-interpretation amendment): the maintainer accepted the design-review recommendations by asking to make the amendments. Requirements and design were regenerated with a three-way judgment, explicit evidence algebra, stable symbols, resource/authority domains, label-blind acquisition and separate admission measurement; that directive is recorded as approval to regenerate tasks. The merged task plan passed an independent task-graph sanity review after two design repairs. Tasks await human approval before implementation."
} }
+326 -299
View File
@@ -1,321 +1,348 @@
# Implementation Plan # Implementation Plan
- [x] 1. Paths from the caller index ## Completed baseline
- [x] 1.1 Add `PathRecord` and `compose_paths` in `src/openultrasast/semantic/obligations/paths.py` (P)
- Upward walk through a `callers(path, function)` callable, depth and fan-out bounds, stop at the first entry,
chains kept per file, deduplicated; unreached targets returned
- Done when `tests/test_obligation_paths.py` covers depth 1 to 3, unreached, fan-out cut and same-name files,
and `check_obligations` accepts the records as `paths` (one test runs the checker with them)
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
- _Boundary: obligations.paths_
- [x] 1.2 Checker opt-in `assume_entries_protected` and label `unguarded_path` in `semantic/obligations/check.py` The completed work below produced the immutable 2026-10-28 development baseline. Amendment tasks 7-13 replace
- Default off; `_finding` receives the entry records' access by handler; evidence as the design states; its final resolution, dominance, fact-acquisition and measurement semantics without erasing the evidence.
`findings_to_static` explains the label; `LABELS` updated
- Done when the existing checker tests pass unchanged and a new test shows: with paths, no policy, no siblings,
the default gives no finding and the opt-in gives one `unguarded_path` finding for a non-public entry and none
for an entry whose record access is `public`
- _Requirements: 4.1a_
- _Boundary: obligations.check (opt-in only)_
- [x] 2. Entry recognisers - [x] 1. Baseline paths and checker premise
- [x] 2.1 Stapler and Jetty recognisers in `mapping.py` (P) - [x] 1.1 Add bounded caller-index paths with concrete hop records
- Public `do[A-Z]...` methods on exposed types or with Stapler-typed or `@QueryParameter` parameters; Jetty - Delivered depth/fan-out traversal, entry stopping, file-qualified hops and unreached targets.
`AbstractHandler`, `HttpServlet`, JAX-RS resources; evidence strings; `public` only for `UnprotectedRootAction` - Baseline fixtures cover depth, fan-out, deduplication and multiple same-name files.
- Done when `tests/test_mapping_frameworks.py` names the entries of a fixture Descriptor, rejects a private - The final call semantics are superseded by tasks 8-9.
helper named `doX`, and the existing mapping tests still pass - _Requirements: 3.1, 3.9, 3.10_
- _Requirements: 1.1, 1.6, 1.7, 1.8_ - _Boundary: Concrete Paths_
- _Boundary: mapping_ - [x] 1.2 Add the opt-in recognised-entry-is-protected premise
- [x] 2.2 Symfony, Livewire and Laravel controller, Remix export and Tornado recognisers in `mapping.py` (P) - Existing checker defaults remain unchanged; prover evidence identifies the premise.
- Done when fixture files of each shape yield `route` entries with evidence and the negative fixtures yield none - _Requirements: 4.6_
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.7, 1.8_ - _Boundary: Obligation Checker_
- _Boundary: mapping_
- [ ] 3. Enforcement witnesses and facts - [x] 2. Baseline entry recognition
- [x] 3.1 `semantic/obligations/enforcement.py` producing router and decorator `Discharge` witnesses (P) - [x] 2.1 Add the first Java entry recognisers and evidence
- Laravel route middleware and controller `middleware`/`authorizeResource`; Symfony `IsGranted`, `Security`, - Delivered development cases for declared web methods, servlets, resources and handlers.
security.yaml `access_control`; Tornado `authenticated`; names listed in the facts headers - _Requirements: 1.1, 1.5_
- Done when `tests/test_obligation_enforcement.py` shows a guarded Laravel route, a Symfony attribute and a - _Boundary: Mapping_
Tornado decorator each produce one witness that `OrderDominance` accepts for the handler's operation - [x] 2.2 Add the initial PHP, JavaScript, TypeScript and Python entry recognisers
- _Requirements: 2.1, 2.2, 2.5, 2.6_ - Delivered positive and negative fixture coverage with entry evidence.
- _Boundary: obligations.enforcement_ - _Requirements: 1.1, 1.5_
- [ ] 3.2 Operation and discharger rows for the pool's shapes in `ruleset/obligations/{java,php,python,typescript}.toml` (P) - _Boundary: Mapping_
- Java: credential lookups and outbound connection tests in `doTestConnection`-style handlers, item reads and
writes, `checkPermission`/`checkAnyPermission` as path guards, `hasPermission` as ownership check;
Remix: session requirements as path guards, record reads by request id as protected reads needing
`ownership_check`; Tornado: handler `open`/`get` as entries needing `path_guard`; Jetty service start as
privileged action. Every name checked in the clones or documentation, said in the header
- Done when `tests/test_obligation_facts.py` loads the files and one operation and one discharger per
language resolve
- _Requirements: 2.3, 2.4, 2.7_
- _Boundary: ruleset/obligations_
- [x] 3.3 Handler-named operations: `handlers` and `framework` fields in the facts loader, `find_operations(..., handlers_of=)`, Stapler form-validation rows in `java.toml`, Tornado handler rows in `python.toml`, Remix action rows in `typescript.toml` - [x] 3. Baseline enforcement and operation facts
- Done when `tests/test_obligation_facts.py` loads a handler-named row, `tests/test_obligation_operations.py` - [x] 3.1 Add route-, declaration- and policy-scoped enforcement witnesses
(or the existing operations test file) yields the operation at the declaration line only for listed - Delivered initial external-handler witnesses and integration fixtures.
handlers, and the prover smoke on the 23 fixes reaches and checks the Jenkins form-validation methods - _Requirements: 2.2, 2.4_
- _Requirements: 2.8_ - _Boundary: Enforcement Adapter_
- _Boundary: obligations.facts, obligations.operations_ - [x] 3.2 Close the unfinished per-target fact inventory as superseded
- The partially completed inventory is not treated as complete implementation.
- Tasks 11.3-11.7 replace it with label-blind acquisition and promoted target knowledge.
- _Requirements: 2.1, 2.2, 2.9, 2.10_
- _Boundary: Target Knowledge_
- [x] 3.3 Add handler-named operation facts
- Delivered opt-in handler operations at declaration lines for recognised entries.
- _Requirements: 2.3_
- _Boundary: Fact Interpreter_
- [x] 4. The prover - [x] 4. Baseline prover, traces and structural control flow
- [x] 4.1 `hunt/prover.py`: `prove_change` over the hunter `Context`, `Coverage`, findings as hunt defects - [x] 4.1 Integrate entries, paths, operations and witnesses in the deterministic prover
- Touched functions from head lines, build output left out, targets include direct callees holding operations, - Delivered hunt-defect findings and failure-visible baseline coverage with no model calls.
IRs of hop files passed to the checker, extra witnesses from enforcement, coverage counts by reason, no model - Final three-way judgment is superseded by task 10.6.
- Done when `tests/test_hunt_prover.py` shows: an unguarded Stapler handler calling `lookupCredentials` gives - _Requirements: 4.7_
one finding with entry, hops and missing `path_guard`; a `checkPermission` above the call gives none; a - _Boundary: Prover Integration_
guarded Laravel route gives none; an unrecognised framework gives coverage only; the finding dict has - [x] 4.2 Emit deterministic ordered traces and local argument provenance
`path`, `line`, `family`, `function`, `evidence`, `id` - Delivered canonical text/JSON traces for discharged and unguarded baseline pairs.
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 6.2, 7.1, 8.1, 8.2_ - _Requirements: 9.1, 9.2, 9.3, 9.5_
- _Depends: 1.1, 2.1, 3.1, 3.2_ - _Boundary: Canonical Trace_
- [x] 4.3 Attach traces to prover records and findings
- Delivered trace references through the replay envelope.
- _Requirements: 8.1, 9.7_
- _Boundary: Prover Integration_
- [x] 4.4 Build structural control-flow graphs
- Delivered branches, loops, exits, exception regions, dominators and control context.
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- _Boundary: Structural CFG_
- [x] 4.5 Add baseline CFG dominance and guard effects
- Delivered value-only rejection and control-context trace fields.
- Strict unknown semantics are superseded by task 10.1.
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- _Boundary: CFG Dominance_
- [x] 4.2 `semantic/obligations/trace.py`: `Guard`, `Hop`, `Trace`, `text()` and `record()`; argument provenance through the IR's binds within each hop, forwarded along the path - [x] 5. Baseline replay arm
- Done when `tests/test_obligation_trace.py` serialises a three-hop fixture deterministically (text and JSON), - Delivered checkpointed records, standard metrics, coverage tables, zero-byte refusal and immutable records.
traces an argument from an entry parameter to `request_input`, from a literal to `constant`, from an identity - _Requirements: 5.1, 5.2, 5.4, 5.6_
source to `authenticated_context`, and marks an unfollowable value `unknown` - _Boundary: Development Runner_
- _Requirements: 9.1, 9.3, 9.4, 9.5_
- _Boundary: obligations.trace_
- [x] 4.3 The prover emits a trace per examined pair (discharged and unguarded), the record and the defect carry them
- Done when `tests/test_hunt_prover.py` shows a discharged pair and an unguarded pair each produce one trace with
the verdict, the unguarded finding references its trace, and `prover_arm` records carry `traces`
- _Requirements: 9.1, 9.2, 9.5, 9.6_
- _Depends: 4.2_
- [x] 4.4 `semantic/cfg.py`: structural control-flow graph per function from the syntax tree, dominators, control context (P) - [x] 6. Baseline development measurement
- Java, PHP, Python, JavaScript and TypeScript constructs of requirement 10.1; `cfg_unavailable` otherwise - Measured 23 repositories and 483 changes: 65.2% fix-push recall, 15.0 flagged ordinary pushes per 100,
- Done when `tests/test_semantic_cfg.py` shows, per language, that a guard before an unconditional return 0 instrument failures; recorded at benchmarks/measurements/2026-10-28-access-control-prover/.
dominates, a guard inside an if-branch does not dominate a statement after the branch, a guard inside a - The population is development evidence only and remains immutable.
loop body does not dominate a statement after the loop, a try/catch is represented, and context reports - _Requirements: 5.2, 5.7, 12.9_
conditions and loops - _Boundary: Development Measurement_
- _Requirements: 10.1, 10.2, 10.4_
- _Boundary: semantic.cfg_
- [x] 4.5 `obligations/cfg_dominance.py::CfgDominance`, failure effects, and the trace's control components; the prover selects it; Java parameter names and handler-operation arguments cleaned
- Done when `tests/test_obligation_cfg_dominance.py` shows a value-only predicate does not discharge while the
same predicate guarding a throw does; a branch-only guard yields `some_paths` and no discharge; the prover
smoke on the 23 fixes keeps or raises its recall; `tests/test_mapping_frameworks.py` shows Java parameter
names without annotations
- _Requirements: 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
- _Depends: 4.4_
- [ ] 5. The arm ## Amendment implementation
- [x] 5.1 `benchmarks/valid/prover_arm.py`: run over a pool slice with checkpoints, summarise with the valid metrics
- Record envelope copied from `hunter_arm`; instrument fields; resume; `summary.json` with recall (pushes and
locations, Wilson), false alarms per 100, precision, instrument failures, seconds per change and the list
over 120 s, per-repository and per-framework coverage, `uncovered` repositories; refuse on zero-byte reads
- Done when `tests/test_prover_arm.py` runs a two-change fixture pool end to end and the summary carries
every field above; a record with zero bytes read makes `summarize` refuse
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 6.1, 7.2_
- _Depends: 4.1_
- [ ] 6. Measurement (maintainer, not Codex) - [ ] 7. Abstract evidence foundation
- [x] 6.1 Run the arm on `benchmarks/valid/train/pool-train.toml` over the training clones; write - [ ] 7.1 Implement the evidence-relation algebra and completeness value
`benchmarks/measurements/<date>-access-control-prover/` with NOTES, summary, coverage table; compare with the - Encode the specified precision order, complete join table, dimension conjunction and witness aggregation.
DSPy and hunter arms of the same pool - Preserve every incompleteness reason deterministically and deduplicate reasons without losing stage identity.
- _Requirements: 5.2, 5.3, 7.2_ - Done when exhaustive table tests and algebraic law tests pass for every relation pair.
- Done 2026-10-28: `benchmarks/measurements/2026-10-28-access-control-prover/` -- 23-repo - _Requirements: 11.1, 11.6, 11.7_
pool (483 changes, 93 locations), recall 65.2% (Wilson 44.9-81.2), 15.0 FA/100, 0 - _Boundary: Abstract Evidence Domain_
instrument failures; on the 5 repos shared with the 2026-10-14 hunt arms, prover 4/5 vs - [ ] 7.2 Add resource abstract values and operation-fact fields
DSPy 2/5 and hunter 2/5 (union 4/5), including trigger.dev which neither arm found. - Represent exact resources, finite alias sets, request-global scope and unknown resources.
- Extend operation facts to state when a request-global guard is sufficient.
- Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures.
- _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Resource Domain_
- _Depends: 7.1_
- [ ] 7.3 Add authority abstract values and promoted implication facts
- Represent exact, finite and unknown authorities separately from resources.
- Load authority implications from promoted facts and retain the supporting fact identifier.
- Done when sufficient, insufficient and unknown authority fixtures produce the specified relations.
- _Requirements: 2.1, 2.2, 2.5, 2.7, 2.8, 11.1_
- _Boundary: Authority Domain_
- _Depends: 7.1_
- [ ] 7.4 Extend judgment, trace and coverage contracts with abstract state
- Add three-way judgments, completeness and evidence relations without changing legacy defaults.
- Add canonical serialization for resource and authority relations and all incompleteness reasons.
- Done when old trace fixtures remain stable in legacy mode and new fixtures round-trip all abstract fields.
- _Requirements: 7.1, 7.2, 7.5, 7.6, 9.1, 9.2, 9.6_
- _Boundary: Judgment Contracts, Canonical Trace_
- _Depends: 7.1, 7.2, 7.3_
- [ ] 7. Resolution foundation `[2026-10-29]` - [ ] 8. Stable symbols and conservative call resolution
- [ ] 7.1 `CallSite` receiver capture: `receiver: str | None` field on `semantic/ir.py`'s `CallSite`, captured - [ ] 8.1 Capture call receiver and arity evidence in the semantic IR
in `semantic/cst.py` call extraction (receiver expression text for `$obj->method` and `x.y.z` shapes; `None` - Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
for bare calls); every existing consumer unchanged - Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
- Done when IR fixtures show receiver text carried through call extraction for PHP/TS shapes, `CallSite` still - _Requirements: 3.2, 3.3_
constructs without it for existing callers, and all existing IR/cst tests pass unchanged - _Boundary: Semantic IR_
- _Requirements: 3.9_ - [ ] 8.2 Build stable declaration identities and the per-change symbol index
- _Boundary: semantic.ir, semantic.cst_ - Identify declarations by module, enclosing type, name and available signature.
- [ ] 7.2 (P) `obligations.resolution`: `ClassRecord`, `class_table(texts, language)` cached per change on the - Cache the symbol and inheritance indexes on the per-change context.
hunter `Context`, `qualify(receiver, enclosing_class, name, arity, table)` returning `Class.name`, - Done when duplicate type names, same-name methods and overloads receive stable distinct identities.
`Class.name/arity`, or the bare name; base-class attachment rule (attaches to extending classes only) - _Requirements: 3.2, 3.5, 6.2_
- Done when `tests/test_obligation_resolution.py` covers: two same-name methods on different classes give - _Boundary: Symbol Index_
distinct keys; same name different arity gives distinct keys; an unresolvable receiver gives the bare name
with `receiver_unresolved` counted; an abstract-base method attaches to a subclass call and not to an
unrelated class
- _Requirements: 3.9_
- _Boundary: obligations.resolution, hunt.producers (Context cache field only)_
- [ ] 8. Guard binding and policy witnesses `[2026-10-29]`
- [ ] 8.1 (P) Resource-bound witnesses: optional `resource` field on `Discharge` (additive, default `None`);
enforcement mints tokens (route path for router middleware, attribute/subject for a policy-layer guard,
`None` for auth-class guards); policy-layer witness scoped to the action with the attribute or subject as
resource (a policy decision -- voter, capability check, or equivalent -- reachable from the action or a
function it calls). Which constructs count as policy decisions comes from the facts rows (task 10), never
engine code
- Done when `tests/test_obligation_enforcement.py` shows a resource-mismatch fixture (guard binds another
object's resource, the operation stays undischarged under `resource_bound`) and a policy-witness fixture
yielding a witness scoped to the action with the attribute as resource
- _Requirements: 2.10_
- _Boundary: obligations.enforcement, obligations.operations (dataclass field only)_
- [ ] 8.2 Checker opt-in `resource_bound` and all-kind caller-hop re-judgement in `check.py`'s `_on_caller_hop`:
a witness with `resource` set suppresses only operations whose resource token matches; every discharger kind
is re-judged on earlier hops at the hop's call line; `guards_before_call` participates in the re-judgement
- Done when the existing checker tests pass unchanged and new tests show: keyword off keeps today's behaviour
(including the `assume_entries_protected`-gated hop re-judgement); keyword on, a mismatched-resource witness
does not suppress; a guard one call deeper than the handler discharges; a guard dominating before a hop
call yields no finding
- _Requirements: 2.10, 3.8, 3.10_
- _Boundary: obligations.check (opt-in only)_
- _Depends: 8.1_ - _Depends: 8.1_
- [ ] 8.3 Resolve exact, finite-candidate and unresolved call targets
- Return complete exact targets, incomplete finite candidate sets, or unresolved results with reasons.
- Remove global bare-name fallback from prover-mode resolution.
- Done when ambiguity never merges methods and unresolved calls cannot produce complete paths.
- _Requirements: 3.3, 3.4, 11.1, 11.6_
- _Boundary: Call Resolution_
- _Depends: 8.2_
- [ ] 8.4 Resolve inheritance and base-declaration attachment
- Attach base operations and entries only through repository-defined descendant relations.
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
- _Requirements: 3.5_
- _Boundary: Call Resolution_
- _Depends: 8.2, 8.3_
- [ ] 9. Recogniser registry `[2026-10-29]` - [ ] 9. Interprocedural summaries and concrete paths
- [ ] 9.1 `mapping/` package with a recogniser registry (Strategy pattern): registry table consulted by the - [ ] 9.1 Extract local function summaries
language dispatch in `mapping.py`; the registration-sites strategy lands first -- call-site handler - Collect each function's operations, witnesses, resolved calls and local completeness without traversing callers.
registration resolved through `resolution.class_table`, entries at the registered class's request-method - Done when summaries are deterministic and contain no target names outside fact identifiers.
declarations with `registered_by` evidence, registration-site vocabulary carried as fact data (task 10), not - _Requirements: 3.1, 3.6, 11.7_
engine code; the recogniser returns unresolved registrations alongside the entries so the prover can count - _Boundary: Function Summaries_
them - _Depends: 7.4, 8.3_
- Done when `tests/test_mapping_registration_sites.py` shows a registration-site fixture (vocabulary from - [ ] 9.2 Compute monotone summaries over recursive call components
data rows) yielding entries at the registered class's file with registration evidence, an unresolved - Iterate finite summary sets over strongly connected components until stable.
registration returned as such (no entry), `mapping.py`'s dispatch consults the registry, and the existing - Done when direct recursion and mutual recursion terminate with stable accumulated evidence.
mapping tests pass unchanged - _Requirements: 3.7, 11.6_
- _Requirements: 1.9_ - _Boundary: Function Summaries_
- _Boundary: mapping_ - _Depends: 9.1_
- _Depends: 7.2_ - [ ] 9.3 Add summary caches and explicit exploration cutoffs
- [ ] 9.2 Migrate the first legacy recogniser family (Stapler) from `mapping.py`'s inline blocks into a - Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
`mapping/` strategy module, proving the migration path; remaining families migrate per the breadth map, not - Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
this task - _Requirements: 3.8, 6.2, 6.3, 7.2_
- Done when Stapler entries come from `mapping/stapler.py` through the registry with the existing Stapler - _Boundary: Function Summaries, Context Cache_
tests unchanged, and `mapping.py` contains no Stapler-specific regex - _Depends: 9.2_
- _Requirements: 1.1_ - [ ] 9.4 Materialise exact evidence paths from summaries
- _Boundary: mapping_ - Emit concrete entry-to-operation hops only through exact target edges.
- Preserve every complete chain independently and retain candidate-edge diagnostics separately.
- Done when guarded and unguarded paths to one operation remain separate and candidate edges never complete a path.
- _Requirements: 3.9, 3.10, 11.4, 11.5_
- _Boundary: Concrete Paths_
- _Depends: 9.3_
- [ ] 10. Fact initialization job `[2026-10-29]` - [ ] 10. Strict authorization judgment
- [ ] 10.1 (P) `tools/fact_init.py`: the one-time initialization pass per target. Extraction mode walks the - [ ] 10.1 Return abstract dominance relations without prover fallback
legacy `mapping.py` blocks and current TOML rows and extracts embedded target vocabulary into the acquirable - Return MUST, MAY, NO or UNKNOWN with guard effect and control context.
structures. Proposal mode takes an LLM or maintainer proposal for a target's vocabulary (entry shapes, - Keep legacy order dominance unchanged, but return UNKNOWN in prover mode for unavailable graphs or line mappings.
registration sites, handler-name families, policy-decision constructs). The gate: every row passes the facts - Done when no prover fixture is discharged or flagged solely through textual-order fallback.
header discipline (source, version, region) and engine verification admits it (the row must match what the - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
target's code says); only then promoted to a closed row - _Boundary: CFG Dominance_
- Done when `tests/test_fact_init.py` shows: extraction mode recovers the vocabulary of one legacy family - _Depends: 7.1_
into rows; a proposal matching a fixture repository is promoted; a proposal the fixture contradicts is - [ ] 10.2 Match witness kind, scope and resource
rejected with the reason; nothing lands in the TOML files without passing the gate - Apply request-global sufficiency only when the operation fact declares it.
- _Requirements: 2.7_ - Return NO for definite mismatch and UNKNOWN for unresolved resource compatibility.
- _Boundary: tools.fact_init, ruleset/obligations (rows it produces)_ - Done when same-resource, alias, global, mismatch and unknown fixtures match the design algebra.
- [ ] 10.2 First initialization run for the training pool's target families: produce the handler-name rows for - _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_
webhook/build-trigger and data-exposure shapes, the registration-site vocabulary, and the policy-decision - _Boundary: Witness Matcher_
constructs, as data rows admitted through the gate (names verified against the pool clones; header updated) - _Depends: 7.2, 10.1_
- Done when the pool's gate-1 miss shapes (Stapler webhook/data-exposure handlers, programmatic registration, - [ ] 10.3 Match authority and promoted implications
policy voters) are all carried as rows with headers, no engine module names a target, and - Apply only promoted authority implications and preserve their evidence.
`tests/test_obligation_facts.py` loads each family - Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided.
- _Requirements: 2.7, 2.9, 2.10, 1.9_ - Done when stronger, weaker, unrelated and unknown authorities produce the expected relations.
- _Boundary: ruleset/obligations_ - _Requirements: 2.2, 2.5, 2.7, 2.8, 11.1_
- _Depends: 10.1, 9.1, 8.1_ - _Boundary: Witness Matcher_
- _Depends: 7.3, 10.1_
- [ ] 10.4 Compute one judgment per complete or diagnostic path
- Conjoin witness dimensions, aggregate witnesses per obligation and apply the mixed-obligation rule.
- Emit discharged only for all-MUST, indeterminate for any MAY/UNKNOWN, and unguarded for at least one NO.
- Done when MUST/NO, MAY/NO, UNKNOWN and empty-witness fixtures match the specified truth table.
- _Requirements: 4.1, 4.2, 4.3, 11.2, 11.3, 11.4_
- _Boundary: Pair Judgment_
- _Depends: 10.2, 10.3_
- [ ] 10.5 Aggregate multiple paths in the opt-in checker mode
- Return unguarded if any complete path is unguarded, discharged only for complete all-discharged enumeration,
and indeterminate otherwise.
- Keep the legacy checker API and defaults unchanged.
- Done when mixed guarded, unguarded and incomplete path sets produce the specified aggregate.
- _Requirements: 3.10, 4.1, 4.2, 4.3, 4.4, 4.5, 11.5_
- _Boundary: Obligation Checker_
- _Depends: 9.4, 10.4_
- [ ] 10.6 Integrate three-way judgments into the prover
- Emit hunt findings only for unguarded pairs and send no indeterminate pair to the proof arm.
- Carry all judgments, completeness reasons, resources, authorities and stable identifiers in traces and coverage.
- Record stage-specific exceptions without folding them into quiet coverage.
- Done when end-to-end fixtures distinguish unguarded, discharged and indeterminate records and findings.
- _Requirements: 4.4, 4.5, 4.6, 4.7, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1, 8.2, 8.3, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
- _Boundary: Prover Integration, Canonical Trace_
- _Depends: 7.4, 9.4, 10.5_
- [ ] 11. Prover resolution integration `[2026-10-29]` - [ ] 11. Target knowledge registry and acquisition
- [ ] 11.1 Target enumeration joins callees by qualified key with a `down_depth` budget (default 2, sharing the - [ ] 11.1 (P) Add the entry-recognition strategy registry
fan-out bound) so operations one call deeper are reached; the `callers` adapter keeps only callers whose - Route mapping dispatch through one registry without embedding target names in engine modules.
receiver binds to the target's class or subclass; unresolvable receivers fall back to the bare name and are - Done when a fixture strategy registers, runs and records its strategy identifier and evidence.
counted `receiver_unresolved`; unresolved registrations from mapping are counted `registration_unresolved` in - _Requirements: 1.1, 1.5, 1.7_
`Coverage` - _Boundary: Mapping Registry_
- Done when `tests/test_hunt_prover.py` shows: an operation one call deeper than the handler is reached and - _Depends: 7.1_
checked; a same-name method on another class yields a separate verdict (no cross-contamination); coverage - [ ] 11.2 Add the generic call-site registration strategy
carries `receiver_unresolved` when a receiver does not resolve and `registration_unresolved` when a - Resolve repository-defined registered handlers through stable symbols and return unresolved registration coverage.
registration target is outside the repository - Done when exact and unresolved registration fixtures produce entries or coverage respectively.
- _Requirements: 3.7, 3.9_ - _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6_
- _Boundary: hunt.prover_ - _Boundary: Registration Adapter_
- _Depends: 7.1, 7.2, 9.1_ - _Depends: 8.4, 11.1_
- [ ] 11.3 (P) Extract existing target knowledge into candidate records
- Read legacy recognition strategies and current facts without changing promoted outputs.
- Done when extraction produces deterministic candidate records with source/version/region evidence.
- _Requirements: 2.9_
- _Boundary: Fact Initialization_
- _Depends: 7.1_
- [ ] 11.4 Verify and promote proposed target knowledge
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
- _Requirements: 2.9, 2.10_
- _Boundary: Fact Initialization_
- _Depends: 11.3_
- [ ] 11.5 Record acquisition provenance and enforce label-isolated mode
- Persist input revision, evidence digest, accepted/rejected proposals and output digest.
- Reject admission acquisition inputs containing vulnerability, fix-location or ordinary-change labels.
- Done when changed output after labels are opened marks the population spent.
- _Requirements: 12.1, 12.2, 12.3_
- _Boundary: Fact Initialization, Admission Guard_
- _Depends: 11.4_
- [ ] 11.6 Migrate one legacy recogniser through the registry
- Move one development-evidence family out of inline dispatch without changing its entries.
- Done when old fixtures stay stable and inline target-specific matching is removed for that family.
- _Requirements: 1.1, 1.7_
- _Boundary: Mapping Registry_
- _Depends: 11.1_
- [ ] 11.7 Run the first label-blind development acquisition
- Produce registration, handler-operation, enforcement, resource and authority knowledge through the gate.
- Treat the output as development data and record its acquisition digest; do not encode names in engine modules.
- Done when promoted outputs load, rejected proposals have reasons, and the old unfinished task 3.2 has no residue.
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 5.7, 12.9_
- _Boundary: Target Knowledge_
- _Depends: 10.3, 11.2, 11.5, 11.6_
- [ ] 12. Arm and coverage `[2026-10-29]` - [ ] 12. Development and admission runners
- [ ] 12.1 (P) Recognition or resolution exceptions inside `prove_change` are recorded under - [ ] 12.1 Materialise and verify baseline and candidate analyzer bundles
`error:<exception class name>` reason keys in coverage, never folded into `framework_unrecognised` or any - Resolve analyzer revisions, fact sets, acquisition records and configurations to immutable digest-verified inputs.
other quiet reason - Refuse comparison when any bundle artifact is unavailable or mismatched.
- Done when a fixture raising inside a recognition or resolution pass counts under `error:<class>` and - Done when the immutable baseline and a fixture candidate bundle can be reconstructed exactly.
`framework_unrecognised` stays empty for that fixture - _Requirements: 5.1, 5.5, 12.1_
- _Requirements: 7.3_ - _Boundary: Run Manifests_
- _Boundary: hunt.prover_ - [ ] 12.2 Add record provenance, stage funnel and judgment transitions
- [ ] 12.2 (P) Stale-anchor refusal in `prover_arm._prove`: every returned finding's `path` is validated - Persist analyzer, fact, acquisition, configuration, population and input digests per record.
against the files actually read; an absent path marks the record `instrument_failure` with reason - Summarise every analysis stage, completeness reason and transition between compared judgments.
`stale_anchor` and findings cleared; `summarize` refuses such runs listing the record ids - Done when a fixture run accounts for every record from entry recognition through final judgment.
- Done when `tests/test_prover_arm.py` shows a fixture finding on an absent path makes `_prove` mark the - _Requirements: 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8_
record and makes `summarize` refuse, mirroring the zero-byte refusal - _Boundary: Development Runner_
- _Requirements: 5.6_ - _Depends: 10.6, 12.1_
- _Boundary: prover_arm_ - [ ] 12.3 Enforce instrument and hard runtime gates
- Preserve zero-byte and stale-anchor refusal and add stage-aware deadline records.
- Fail the run if any measured change exceeds 120 seconds; never summarize the overrun as a passing result.
- Done when zero-byte, stale-anchor, deadline and 121-second fixtures all refuse the summary.
- _Requirements: 5.6, 6.1, 6.3_
- _Boundary: Runner Gates_
- _Depends: 12.2_
- [ ] 12.4 Register the four-arm development comparison
- Run baseline/candidate analyzer crossed with baseline/candidate facts over identical inputs.
- Report metric deltas, stage-funnel deltas and judgment transitions without admission language.
- Done when fixture arms isolate a fact-only lift from an analyzer-only lift.
- _Requirements: 5.2, 5.5, 5.7, 11.8_
- _Boundary: Development Runner_
- _Depends: 12.1, 12.2, 12.3_
- [ ] 12.5 Consume and validate an externally reviewed admission manifest
- Accept only a manifest produced by valid-detection-benchmark with review, isolation and immutable digest fields.
- Reject label exposure, changed acquisition output or a spent population.
- Done when valid, malformed, exposed and spent manifests produce deterministic accept/refuse results.
- _Requirements: 12.1, 12.2, 12.3, 12.6, 12.8_
- _Boundary: Admission Runner_
- _Depends: 11.5, 12.3_
- [ ] 12.6 Add paired scoring and the admission report
- Score vulnerable/fixed twins, reviewed ordinary changes, indeterminate rate, coverage, runtime and instruments.
- Preserve sample counts and uncertainty intervals and return NO-GO on any pre-registered gate failure.
- Done when fixture pairs detect vulnerable-only, fixed-side regression and mislabeled-negative cases.
- _Requirements: 5.2, 12.4, 12.5, 12.7, 12.8, 12.9_
- _Boundary: Admission Runner_
- _Depends: 12.5_
- [ ] 13. Measurement after the amendment (maintainer, not Codex) `[2026-10-29]` - [ ] 13. Cross-cutting validation and measurements
- [ ] 13.1 Re-run the arm on `benchmarks/valid/train/pool-train.toml` after tasks 7-12 land; write - [ ] 13.1 Validate abstract-domain and metamorphic laws
`benchmarks/measurements/<date>-access-control-prover-v2/` with NOTES, summary, coverage table; compare - Cover the full join table, conjunction, witness aggregation, monotonicity and mixed-obligation rule.
against the 2026-10-28 gate 1 run (recall 65.2%, FA 15.0/100) on the same pool - Prove that type renaming preserves a judgment and adding ambiguity can only preserve it or make it indeterminate.
- _Requirements: 5.2, 5.3, 7.2_ - Done when property and fixture tests pass deterministically across repeated runs.
- _Requirements: 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
- _Boundary: Abstract Evidence Validation_
- _Depends: 10.5_
- [ ] 13.2 (P) Validate cross-component analysis scenarios
- Exercise helper guards, registration entries, base attachment, resource mismatch, authority insufficiency,
ambiguous calls, unavailable CFGs, recursion and budget cutoff.
- Done when every scenario yields the expected trace, coverage reasons and three-way judgment.
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.5, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 4.1, 4.2, 4.3, 10.6_
- _Boundary: Prover Integration Validation_
- _Depends: 10.6, 11.7_
- [ ] 13.3 Run the four-arm development regression
- Run all four registered arms on the immutable current development population after proving nonzero input reads.
- Publish analyzer/fact attribution, stage funnel, three-way judgments, budget result and comparison to the
2026-10-28 baseline; label the result development-only.
- Done when all arm records and summary digests are complete and no admission claim appears.
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.1, 7.3, 11.8, 12.9_
- _Boundary: Development Measurement_
- _Depends: 12.4, 13.1, 13.2_
- [ ] 13.4 Run admission from an externally prepared untouched manifest
- Consume a reviewed manifest from valid-detection-benchmark after analyzer and facts are frozen.
- Verify label isolation, paired vulnerable/fixed behavior, reviewed ordinary labels and every admission gate.
- Done when the report returns an evidence-backed GO or NO-GO without modifying facts, strategies or thresholds.
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
- _Boundary: Admission Measurement_
- _Depends: 12.6, 13.3_
### Task constraints `[2026-10-29]` ## Task constraints
From `.kiro/steering/code-standards.md`, binding on every task above: - Complexity: McCabe at most 10 per touched function, at most 50 logical lines per function and 400 per new module.
- Engine/target split: abstract-domain, resolution, summary, judgment and trace modules contain no target names.
- Target knowledge: names enter only through promoted facts or registered target-aware strategies.
- Compatibility: legacy checker defaults and completed baseline records remain unchanged.
- Instrument rule: validation must prove nonzero input reads before interpreting any clean or zero result.
- Complexity caps: McCabe <= 10 per function; <= 50 LLOC per function; <= 400 LLOC per new module. A task is ## Baseline implementation record
not done while a function it touched exceeds a cap; split the function.
- Pattern discipline: tasks 9.1-9.2 implement Strategy + Registry; task 10.1 implements the acquisition
pipeline behind the Interpreter discipline (rows are the language); task 11.1 is the Adapter at the
engine seam. New recognisers land in `mapping/` strategy modules, never as `mapping.py` blocks.
- Idiomatic naming: names state what a value semantically is in the pipeline vocabulary; collections name
their elements (`entry_point_records`, `judgments_by_key`); booleans are predicates; no abbreviations like
`eps`, `recs`, `d2`.
- Deliveries A-C implemented the baseline paths, recognisers, witnesses, handler facts, prover, traces and CFG with
## Implementation Notes static checks and focused tests passing at delivery time.
- The full development arm then processed all 483 changes with zero instrument failures. Its maximum runtime exceeded
- Delivery A (2026-10-14) implements paths, the opt-in checker premise, Stapler/Jetty/Servlet/JAX-RS the amended hard budget, so the baseline is evidence, not an eligible arm.
entries, the Java facts, the deterministic prover and the checkpointed arm. No commit was made. - The 2026-10-28 ablation identified call resolution, resource/authority precision and failure visibility as the
- Task 3.2 stays open for the PHP, Python and TypeScript facts. Task 4.1's delivery A core is delivered; amendment levers. Those observations inform tasks 7-13 but are not admission labels.
its full-spec checkbox stays open for delivery B's enforcement witnesses and guarded Laravel acceptance
case. This delivery intentionally passes `extra_witnesses=()` as required by the maintainer's brief.
- Default checker behavior is preserved. Only opt-in checking includes a caller's statement witness,
using the existing dominance contract at the outgoing call site. The prover checks each complete path
independently so a guarded chain cannot hide an unguarded chain to the same operation.
- Java additions were checked against actual Git HEAD source objects in the sparse training clones;
checked call sites are named in the Java TOML header. No network or model call was made.
- Mapping accepts cached source texts and an opt-out from external manifest scanning. Default callers
retain their previous behavior; the delivery A prover uses source entries and its Context caches.
- Delivery A review was performed in the main context because the brief forbids model calls. The exact
requested static gate passed (format, Ruff and mypy); the exact pytest gate passed with 112 passed,
zero failed, exit 0. UV_NO_SYNC=1 was set; UV_CACHE_DIR was placed under /tmp because the default
cache is outside the writable sandbox. The unchanged unjustified-path checker test remains intact.
- Delivery B (2026-10-14) implements task 2.2's Symfony, Livewire/Laravel, Remix and Tornado
entries, task 3.1's scoped enforcement witnesses, task 3.3's handler facts and operation discovery,
and task 4.1's integration. Changes remain uncommitted as the brief requires.
- Handler facts are explicitly opt-in through file-qualified handler maps in the checker. The prover
uses handler obligations where a Stapler or Tornado entry reaches no listed call operation; existing
call-site findings and Delivery A tests remain unchanged. Remix retains its handler operation even
when listed calls exist, and independently requires a path guard and an ownership check. Public
access waives only the path guard for these handler facts. The checker default is unchanged.
- The prover supplies a local dominance adapter for handler facts: it checks guards within the handler
body while reporting the operation at its declaration. A final ordinary return does not turn a bare
permission predicate into a guard. OrderDominance and the existing producers and arms are unchanged.
- Remix arrow exports are named locally in the prover's IR copy, including TSX route modules. Symfony
recognises positional and named route paths and class prefixes. Tornado recognises local subclasses
and the pool terminal's terminado base when it explicitly invokes Tornado's WebSocketHandler method.
- PHP CST Point.row access crashed on the larger Kimai controller with the installed native parser.
New recognisers derive line numbers from byte offsets instead; a fixture past line 320 and the
23-change source smoke complete without a crash. Existing CST code was not modified.
- Task 3.2 remains partially complete: TypeScript adds clone-verified requireUserId and
authenticateApiRequest, retaining requireUser and authorize; PHP rows are retained and their header
records enforcement sources. Python retains the existing authenticated decorator and gains the
Tornado handler operation. No get_current_user fact was added: Git searches of Mage HEAD, the pool
revision and websocket fix 66440e4d3 found neither that API nor an authenticated decorator use, and
local framework documentation was unavailable. The brief forbids network access and unsourced names.
- Review used the main-context fallback because the brief prohibits model calls. Verdict: APPROVED
for tasks 2.2, 3.1, 3.3 and 4.1; task 3.2 remains open for its source-verification gap. No new
placeholder markers, credentials, dependencies, producer edits or arm edits were introduced.
- Fresh verification: UV_NO_SYNC=1 and UV_CACHE_DIR=/tmp/ousast-uv. The exact static gate passed:
ruff format --check ., ruff check ., mypy src/openultrasast (178 source files). The exact requested
pytest gate plus test_obligation_operations.py passed with 145 passed, zero failed, exit 0.
Initial missing-feature tests produced 24 failed and 14 passed; integration tests initially produced
eight failed and 20 passed. No test_search_* test was run.
- A separate file-scoped smoke read immutable Git source objects for all 23 access-control changes,
validated their commit objects and source digests, and recorded nonzero input bytes for every change.
All 23 executions completed, zero errors; 16 changes checked operations and seven remained unchecked.
It materialised only changed source files in temporary directories, so it is not a full-repository
recall measurement. The maintainer-owned task 6.1 and release readiness remain unclaimed.
- Delivery C (2026-10-14) implements tasks 4.2 and 4.3: ordered traces for every examined
path/operation pair, argument origins through local bindings and forwarded parameters, traces in
prover results and arm records, and trace references on unguarded findings. Existing checker
decisions and coverage accounting are unchanged. Declared route access is retained in the trace.
- Delivery C review used the main context because the brief prohibits model calls. Verdict: APPROVED
for tasks 4.2 and 4.3. Regression cases cover unresolved collections, overwritten bindings,
source-name boundaries, missing IR arguments, method parameter forwarding and return-order guards.
- Fresh delivery C verification with UV_NO_SYNC=1 and UV_CACHE_DIR=/tmp/ousast-uv: the exact
requested static gate passed (format, Ruff, mypy over 179 source files); the exact pytest gate
passed with 102 passed, zero failed, exit 0. The initial trace test failed collection because
trace.py did not yet exist; additional regression tests failed before their fixes. No
test_search_* test, network request or model call was made. Changes remain uncommitted.
+11 -10
View File
@@ -5,23 +5,24 @@ One screen: every feature's lane, its state today, and its next move. Aligns `.k
| lane | state today | next move | owner | | lane | state today | next move | owner |
|---|---|---|---| |---|---|---|---|
| Engine: obligation checker + dominance | Stable; target-agnostic; checker owns verdicts | All-kind caller-hop re-judgement (task 8.2, opt-in keyword) | access-control-prover | | Engine: obligation checker + dominance | Baseline checker stable; amended three-way judgment and strict CFG relation designed | Abstract domain + strict opt-in judgment (tasks 7, 10); legacy defaults unchanged | access-control-prover |
| Engine: resolution | Designed (class table + receiver/signature-qualified calls); code not started | Task 7.2 `obligations/resolution.py` | access-control-prover | | Engine: resolution | Stable-symbol and finite-candidate design approved; code not started | IR/symbol/call resolution tasks 8.1-8.4 | access-control-prover |
| Engine: paths / CFG / trace | Stable; `PathRecord` shape kept by design | No change; resolution plugs in before them | access-control-prover | | Engine: summaries / paths / trace | Baseline concrete paths and traces stable; uncertainty is not yet first-class | Monotone summaries + exact path materialization (task 9), abstract trace fields (7.4) | access-control-prover |
| Prover integration | `hunt/prover.py` proves changes; caller join is name-keyed | Receiver-qualified target join + down-depth budget (task 11.1) | access-control-prover | | Prover integration | `hunt/prover.py` proves changes with name-keyed joins and binary outcomes | Three-way finding/trace/coverage integration (task 10.6) | access-control-prover |
| Runner | `prover_arm.py` metrics, coverage, zero-byte refusal; gate 1 = 65.2% recall, 15.0 FA/100 | Stale-anchor refusal (task 12.2); re-measure after 7-12 (task 13) | access-control-prover | | Runner | Baseline arm: 65.2% recall, 15.0 flagged ordinary pushes/100; max runtime exceeded the amended 120s hard budget, so it is development evidence, not an eligible arm | Bundle/funnel/gate/four-arm work (task 12), then development and admission runs (task 13) | access-control-prover |
| Judge (zero-shot over traces) | `judge_pass.py`; gate 2 measured: safe but inert (keeps 15/15 TPs, drops 5/69 FAs) | No tuning; revisit only if trace facts change the FA mix | roadmap (measured 2026-10-28) | | Judge (zero-shot over traces) | `judge_pass.py`; gate 2 measured: safe but inert (keeps 15/15 TPs, drops 5/69 FAs) | No tuning; revisit only if trace facts change the FA mix | roadmap (measured 2026-10-28) |
| Recogniser registry | Designed (Strategy+Registry, `mapping/` package); code not started | Task 9.1 registry + registration-sites strategy; 9.2 migrates Stapler as the proof | access-control-prover | | Recogniser registry | Strategy+Registry approved; code not started | Registry + registration adapter + one-family migration (tasks 11.1, 11.2, 11.6) | access-control-prover |
| mapping.py monolith | All framework families inline (1,200+ lines); WordPress/Python registration blocks included | Migrate family by family after Stapler: jetty, symfony, livewire, remix, tornado, then PHP/Python registration tables | breadth (this map) | | mapping.py monolith | All framework families inline (1,200+ lines); WordPress/Python registration blocks included | Migrate family by family after Stapler: jetty, symfony, livewire, remix, tornado, then PHP/Python registration tables | breadth (this map) |
| Obligation facts (ruleset TOMLs) | Closed rows, hand-authored; header discipline enforced by loader | Extraction into acquirable structures via fact-init (task 10.1) | access-control-prover | | Obligation facts (ruleset TOMLs) | Closed rows, hand-authored; resource/authority algebra designed but not implemented | Resource/authority fields (tasks 7.2-7.3), label-blind development acquisition (11.7) | access-control-prover |
| Fact initialization job | Designed (`tools/fact_init.py`: extract + propose -> gate -> promote); code not started | Task 10.1 build; 10.2 first run for pool families as data rows | access-control-prover | | Fact initialization job | Label-blind extraction/proposal/verification/promotion and acquisition record approved; code not started | Tasks 11.3-11.5 | access-control-prover |
| Fact-proposal lane (standing) | Roadmap-level: LLM proposes, engine verifies, repetition promotes | Stays deferred to injection-spec track (roadmap gate 5) | roadmap | | Fact-proposal lane (standing) | Roadmap-level: LLM proposes, engine verifies, repetition promotes | Stays deferred to injection-spec track (roadmap gate 5) | roadmap |
| Injection-family object | Brief only (`engine-change-scoped-analysis`); taint flow + banks MVP exist as evidence | Spec after post-fix pilot holds recall | roadmap | | Injection-family object | Brief only (`engine-change-scoped-analysis`); taint flow + banks MVP exist as evidence | Spec after post-fix pilot holds recall | roadmap |
| Complexity gate | Caps in `code-standards.md`, review-time only | Wire ruff C901 / xenon into CI; threshold per steering | breadth (this map) | | Complexity gate | Caps in `code-standards.md`, review-time only | Wire ruff C901 / xenon into CI; threshold per steering | breadth (this map) |
| Idiomatic naming | Rule active for new/amended code; legacy debt un inventoried | Record rename inventory during family migrations; no drive-by renames | breadth (this map) | | Idiomatic naming | Rule active for new/amended code; legacy debt un inventoried | Record rename inventory during family migrations; no drive-by renames | breadth (this map) |
| Training pool | pool-train 23 repos / 483 changes frozen 2026-10-14 | Pre-2019 negatives re-audit before any future run (3/12 sampled carry real missing-check bugs) | valid-detection-benchmark | | Development pool | pool-train 23 repos / 483 changes frozen 2026-10-14; explicitly not admission evidence | Four-arm regression only after tasks 7-12; pre-2019 negatives still require review | access-control-prover |
| Admission population | Not yet supplied; current pool is spent for development | `valid-detection-benchmark` prepares a reviewed untouched manifest; access-control-prover only consumes it (task 13.4) | valid-detection-benchmark |
| Pre-push safety net | Implementation 67/79; wiring unchanged by this work | Unaffected until prover recall/FA hold at pilot | pre-push-safety-net | | Pre-push safety net | Implementation 67/79; wiring unchanged by this work | Unaffected until prover recall/FA hold at pilot | pre-push-safety-net |
Migration order for mapping.py families is deliberate: Stapler first (task 9.2 proves the registry path on Migration order for mapping.py families is deliberate: Stapler first (task 11.6 proves the registry path on
the family the pool exercises most), then one family per migration, each deleting its legacy block from the family the pool exercises most), then one family per migration, each deleting its legacy block from
`mapping.py` in the same change. `mapping.py` in the same change.