feat(prover): access-control prover, delivery A: path records, Stapler and Jetty entries, Java operation rows, the prover and its scoring arm

Codex delivery under spec access-control-prover. `semantic/obligations/paths.py` builds entry-to-function path
records from the cached caller index; `mapping.py` names Stapler web methods and Jetty handlers as entries;
`check_obligations` gains the opt-in premise `assume_entries_protected` with the label `unguarded_path`;
`hunt/prover.py` proves a change with no model call and states its coverage; `benchmarks/valid/prover_arm.py`
scores it with the valid-benchmark metrics. Two fixes after the first smoke run: a product file git holds empty
is read, not unread (four changes failed on `__init__.py` and empty assets), and absent-input reasons are
counted by name.

Smoke on the 23 fix changes of the training pool: 7 pushes caught (30 percent, Wilson 16 to 51), 28 of 93
locations, precision 7 of 8 flagged, no instrument failure; three changes over the 120 second bound. Twelve of
the 17 Jenkins fixes reach the fixed method but check no operation because the method is the operation;
the spec now carries handler-named operations for delivery B.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-14 16:50:04 +02:00
co-authored by Claude Fable 5.1
parent e514551cb6
commit beb272a5ec
15 changed files with 1352 additions and 17 deletions
@@ -4,7 +4,8 @@ An obligated operation is reported when it is reached (a path record ends at it,
an entry point) and none of the dischargers it accepts dominates it. The label says why the obligation exists: a declared
policy clause (``declared_policy_violation``), the practice of sibling handlers (``consistency_violation``), or, in the
degraded mode without path records, the operation fact alone (``function_local``). With path records and neither a clause
nor a sibling norm, nothing justifies the obligation and nothing is reported (design gating 3). Findings are ``suspicion``;
nor a sibling norm, nothing is reported unless the caller opts into the recognised-entry premise (``unguarded_path``).
Findings are ``suspicion``;
the obligation, the missing discharger, the evidence and the known fix travel as data.
"""
@@ -12,7 +13,7 @@ from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from ...findings import StaticFinding
from ..facts import SemanticFacts
@@ -23,7 +24,7 @@ from .operations import Discharge, Operation, find_discharges, find_operations,
from .policy import DeclaredPolicy
from .siblings import Anomaly, SiblingSet, consistency_anomalies, sibling_sets
LABELS = ("declared_policy_violation", "consistency_violation", "function_local")
LABELS = ("declared_policy_violation", "consistency_violation", "function_local", "unguarded_path")
_ROUTE_LITERAL = re.compile(r"@\w[\w.]*\.(?:route|get|post|put|delete|patch|api_route|websocket)\(\s*['\"]([^'\"]+)['\"]")
@@ -64,6 +65,7 @@ def check_obligations(
store_shapes: Sequence[object],
min_siblings: int,
extra_witnesses: Sequence[Discharge] = (),
assume_entries_protected: bool = False,
) -> ObligationResult:
operations: list[Operation] = []
witnesses: list[Discharge] = list(extra_witnesses)
@@ -85,6 +87,7 @@ def check_obligations(
anomalies, under = consistency_anomalies(sets, min_siblings=min_siblings)
anomaly_index: dict[tuple[str, str], Anomaly] = {(a.handler, a.missing): a for a in anomalies}
entry_handlers = {f"{getattr(e, 'path', '')}::{getattr(e, 'function_name', '')}" for e in entries}
entry_index = {f"{getattr(e, 'path', '')}::{getattr(e, 'function_name', '')}": e for e in entries}
reached = _reached(operations, paths, entry_handlers)
if not paths and operations:
for language in sorted({irs[op.path][0].language for op in operations if op.path in irs}):
@@ -103,9 +106,18 @@ def check_obligations(
degradations.append({"stage": "obligations", "reason": "obligations_store_row_invalid", "records": list(invalid_rows)})
for operation, path_record in reached:
handler = f"{operation.path}::{operation.function}"
hops = getattr(path_record, "hops", ())
first = hops[0][0] if hops and isinstance(hops[0], (tuple, list)) else str(hops[0]) if hops else ""
entry = entry_index.get(str(first))
route = _route_path(operation, texts.get(operation.path, ""))
if assume_entries_protected and entry is not None and route is None:
route = str(getattr(entry, "name", "")) or None
declared_access = policy.route_access(route) if policy is not None and route else None
same = [w for w in witnesses if _relevant(w, operation)]
same = [
w
for w in witnesses
if _relevant(w, operation) or (assume_entries_protected and _on_caller_hop(w, path_record, operation, dominance))
]
dominated_kinds = {w.kind for w in same if valid_witness(w) and dominance.dominates(w, operation, path_record)}
# (a) the operation's own obligation: discharged when any accepted discharger dominates. A declared public route
# waives the path guard here too (Req 4.4), so the operation falls through to the next kind it accepts.
@@ -116,7 +128,17 @@ def check_obligations(
required[0],
)
finding = _finding(
operation, missing, same, anomaly_index.get((handler, missing)), policy, route, declared_access, store_shapes, paths, facts
operation,
missing,
same,
anomaly_index.get((handler, missing)),
policy,
route,
declared_access,
store_shapes,
paths,
facts,
entry if assume_entries_protected else None,
)
if finding is not None:
findings.append(finding)
@@ -127,7 +149,17 @@ def check_obligations(
if kind == "path_guard" and declared_access == "public":
continue # declared public: the guard obligation is waived, the identity one is not
finding = _finding(
operation, kind, same, anomaly_index[(handler, kind)], policy, route, declared_access, store_shapes, paths, facts
operation,
kind,
same,
anomaly_index[(handler, kind)],
policy,
route,
declared_access,
store_shapes,
paths,
facts,
entry if assume_entries_protected else None,
)
if finding is not None:
findings.append(finding)
@@ -152,6 +184,8 @@ def findings_to_static(result: ObligationResult) -> list[StaticFinding]:
if finding.known_fix:
tags.append(f"mechanism:{finding.known_fix}")
why = "; ".join(finding.evidence) if finding.evidence else "the operation fact alone (function-local mode)"
if finding.label == "unguarded_path":
why = "unguarded path from a recognised protected entry; " + why
resource = op.resource or "a resource"
provenance = f" (a {finding.missing} is present but bound from {finding.provenance})" if finding.provenance else ""
statics.append(
@@ -190,6 +224,7 @@ def _finding(
store_shapes: Sequence[object],
paths: Sequence[object],
facts: ObligationFacts,
assumed_entry: object | None = None,
) -> ObligationFinding | None:
wrong = next((w for w in same if w.kind == missing and not valid_witness(w)), None)
declared = policy.resources.get(operation.resource or "") if policy is not None else None
@@ -211,6 +246,16 @@ def _finding(
elif anomaly is not None:
label = "consistency_violation"
evidence = anomaly.siblings + anomaly.evidence[len(anomaly.siblings) :]
elif (
paths and assumed_entry is not None and getattr(assumed_entry, "access_level", "public") != "public" and declared_access != "public"
):
label = "unguarded_path"
evidence = (
f"entry {getattr(assumed_entry, 'kind', '')}: {getattr(assumed_entry, 'path', '')}::"
f"{getattr(assumed_entry, 'function_name', '')}",
*(str(item) for item in getattr(assumed_entry, "access_evidence", ())),
"no declared policy clause and no sibling norm; the prover assumes a recognised entry is protected",
)
elif paths:
return None # path-aware mode: without a clause or a sibling norm, nothing justifies the obligation (gating 3)
else:
@@ -245,6 +290,20 @@ def _reached(operations: Sequence[Operation], paths: Sequence[object], entry_han
return reached
def _on_caller_hop(witness: Discharge, path_record: object | None, operation: Operation, dominance: Dominance) -> bool:
"""Use the same dominance contract at the caller's outgoing call in opt-in mode."""
for node, call_line in getattr(path_record, "hops", ())[:-1]:
if node != f"{witness.path}::{witness.function}":
continue
if witness.scope in {"router", "decorator"}:
return True
if witness.kind not in {"path_guard", "ownership_check"} or call_line is None:
return False
caller_operation = replace(operation, path=witness.path, function=witness.function, line=call_line)
return dominance.dominates(witness, caller_operation, None)
return False
def _relevant(witness: Discharge, operation: Operation) -> bool:
return witness.scope == "hop" or (witness.path, witness.function) == (operation.path, operation.function)
@@ -0,0 +1,53 @@
"""Compose bounded caller chains whose first hop is a recognised entry."""
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
@dataclass(frozen=True)
class PathRecord:
hops: tuple[tuple[str, int | None], ...]
entry: str
complete: bool
def compose_paths(
*,
callers: Callable[[str, str], Sequence[tuple[str, int]]],
entries: set[str],
targets: Sequence[tuple[str, str]],
max_depth: int = 3,
fan_out: int = 25,
) -> tuple[list[PathRecord], list[str]]:
"""Walk at most max_depth caller edges. The adapter resolves sites to path::function nodes.
Each caller hop holds the line of its outgoing call; the final target has no outgoing call.
Stop at the first entry, and never revisit a node within a chain.
"""
if max_depth < 0 or fan_out < 1:
raise ValueError("invalid path bounds")
found: dict[tuple[tuple[str, int | None], ...], PathRecord] = {}
unreached: list[str] = []
for path, function in dict.fromkeys(targets):
target = f"{path}::{function}"
reached = False
pending: list[tuple[tuple[str, int | None], ...]] = [((target, None),)]
while pending:
hops = pending.pop()
node = hops[0][0]
if node in entries:
found.setdefault(hops, PathRecord(hops, node, True))
reached = True
continue
if len(hops) - 1 >= max_depth:
continue
node_path, _, node_function = node.rpartition("::")
seen = {hop[0] for hop in hops}
for caller, line in list(dict.fromkeys(callers(node_path, node_function)))[:fan_out]:
if caller not in seen:
pending.append(((caller, line), *hops))
if not reached:
unreached.append(target)
return list(found.values()), unreached