Make the risk grade fail safe, and gate CI on absence
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

The mechanism behind RISK-F-0003 was sharper than the finding described.
is_high_risk was risk == "high", but risk was never absent at the model layer:
RouteEntry.risk carried a dataclass default of "standard". An omitted grade was
not unhandled, it was actively resolved to the permissive value — fail-open by
construction, which is why nothing ever warned.

The default is now "ungraded" and is_high_risk returns true for anything outside
an explicit low-risk vocabulary (standard / low / accepted). An omitted grade and
an unrecognised grade from a newer catalog both resolve to high, so the boundary
fails safe in both directions rather than reading an unknown value as permission.

test_every_repo_catalog_lane_is_explicitly_graded is the CI gate that stops an
ungraded lane being committed, per ADR-0007: absence is not a grade.

"accepted" is in the low-risk vocabulary deliberately, ready for the
maturity-derived default — an experimental-context lane may be explicitly
accepted, which is a graded decision rather than an omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-20 01:13:35 +02:00
parent ac85259c20
commit d0d4f9d8fc
4 changed files with 110 additions and 5 deletions

View file

@ -11,6 +11,13 @@ from dataclasses import dataclass, field
from typing import List, Optional
# Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including
# the "ungraded" default and any value from a newer catalog — are treated as
# high by is_high_risk, so the read-boundary fails safe in both directions.
LOW_RISK_GRADES = frozenset({"standard", "low", "accepted"})
GRADED_RISK = frozenset({"standard", "low", "accepted", "high", "critical"})
@dataclass
class RotationGuide:
"""Structured-but-advisory renewal guidance for a lane (WARDEN-WP-0026 T06).
@ -111,7 +118,11 @@ class RouteEntry:
# Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values.
rotation: Optional[RotationGuide] = None
# Agent read-boundary risk class (WP-0026 T04). high → agents use wrap/out/exec only.
risk: str = "standard" # "standard" | "high"
# Default is "ungraded", which FAILS SAFE: it is treated as high. Before
# ADR-0007 this defaulted to "standard", so a lane that simply omitted the
# field was silently placed outside the read-boundary (RISK-F-0003) — the
# control was never relaxed by decision, it was never reached.
risk: str = "ungraded" # "standard" | "high" | "ungraded"
# Delegation register (WP-0030). None → implicit interim with unknown owner.
delegation: Optional[Delegation] = None
@ -119,10 +130,20 @@ class RouteEntry:
def is_active(self) -> bool:
return self.status == "active"
@property
def is_graded(self) -> bool:
"""False when this lane carries no explicit risk grade (ADR-0007)."""
return self.risk in GRADED_RISK
@property
def is_high_risk(self) -> bool:
"""True when this lane is on the agent raw-read deny list (WP-0026 T04)."""
return self.risk == "high"
"""True when this lane is on the agent raw-read deny list (WP-0026 T04).
Anything not explicitly graded low is high. An ungraded lane, or one
carrying a grade this version does not recognise, is treated as high
rather than waved through ADR-0007: absence is not a grade.
"""
return self.risk not in LOW_RISK_GRADES
@property
def has_rotation(self) -> bool: