Make the risk grade fail safe, and gate CI on absence
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:
parent
ac85259c20
commit
d0d4f9d8fc
4 changed files with 110 additions and 5 deletions
|
|
@ -119,5 +119,5 @@
|
|||
| task | WARDEN-WP-0032-T02 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T03 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T04 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T05 | todo | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T06 | wait | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -709,3 +709,62 @@ def test_cli_route_show_includes_delegation(repo_catalog_env):
|
|||
assert data["delegation"]["mode"] == "interim"
|
||||
assert data["delegation"]["intended_owner"] == "tenant-engine"
|
||||
assert data["delegation"]["implicit"] is False
|
||||
|
||||
|
||||
# --- ADR-0007: absence is not a grade (WARDEN-WP-0032-T06) ------------------
|
||||
|
||||
|
||||
def _bare_entry(**overrides):
|
||||
"""A minimal RouteEntry, so these tests exercise defaults and nothing else."""
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
fields = dict(
|
||||
id="x",
|
||||
title="t",
|
||||
need_keywords=[],
|
||||
owner_repo="r",
|
||||
subsystem="s",
|
||||
warden_executes=False,
|
||||
wiki_ref="w",
|
||||
canon_ref="c",
|
||||
reviewed="2026-08-20",
|
||||
status="active",
|
||||
)
|
||||
fields.update(overrides)
|
||||
return RouteEntry(**fields)
|
||||
|
||||
|
||||
def test_every_repo_catalog_lane_is_explicitly_graded():
|
||||
"""The CI gate. A lane added without a `risk` grade is a defect (ADR-0007)."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
ungraded = sorted(e.id for e in catalog.entries if not e.is_graded)
|
||||
assert ungraded == [], (
|
||||
f"{len(ungraded)} catalog lane(s) carry no explicit risk grade: {ungraded}. "
|
||||
"ADR-0007: absence is not a grade — grade the lane on merit."
|
||||
)
|
||||
|
||||
|
||||
def test_ungraded_lane_fails_safe_to_high_risk():
|
||||
"""RISK-F-0003 regression: an omitted grade must not wave a lane through.
|
||||
|
||||
Before ADR-0007 the dataclass default was "standard", so a lane that simply
|
||||
omitted the field landed outside the agent read-boundary silently.
|
||||
"""
|
||||
entry = _bare_entry()
|
||||
assert entry.risk == "ungraded"
|
||||
assert entry.is_graded is False
|
||||
assert entry.is_high_risk is True
|
||||
|
||||
|
||||
def test_unrecognised_grade_is_treated_as_high():
|
||||
"""A grade from a newer catalog must not be read as permission."""
|
||||
entry = _bare_entry(risk="spicy")
|
||||
assert entry.is_high_risk is True
|
||||
assert entry.is_graded is False
|
||||
|
||||
|
||||
def test_low_risk_vocabulary_is_explicit():
|
||||
for grade in ("standard", "low", "accepted"):
|
||||
entry = _bare_entry(risk=grade)
|
||||
assert entry.is_high_risk is False, grade
|
||||
assert entry.is_graded is True, grade
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ policy covers these paths (T06).
|
|||
|
||||
```task
|
||||
id: WARDEN-WP-0032-T06
|
||||
status: wait
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "8c082416-0ff9-45dc-8cbd-e9ca7913a6ef"
|
||||
```
|
||||
|
|
@ -235,6 +235,31 @@ Feed back to `ZONE-WP-0001-T03` whether ops-warden can supply the join it needs
|
|||
today no lane references a workload or an environment, so the `M0`–`M3` ladder
|
||||
has nothing to attach to from this side.
|
||||
|
||||
**Enforcement half done 2026-08-20.** It did not need the zone model: `ADR-0007`
|
||||
already decided absence is a defect, which is enough to make the code fail safe
|
||||
and to gate CI.
|
||||
|
||||
The real mechanism turned out to be sharper than `RISK-F-0003` described.
|
||||
`is_high_risk` was `risk == "high"`, but `risk` was **not** absent at the model
|
||||
layer — `RouteEntry.risk` carried a dataclass default of `"standard"`. So an
|
||||
omitted grade was not unhandled; it was actively resolved to the permissive
|
||||
value. Fail-open by construction, which is why nothing warned.
|
||||
|
||||
Now: the default is `"ungraded"`, and `is_high_risk` returns true for anything
|
||||
not in an explicit low-risk vocabulary (`standard` / `low` / `accepted`). An
|
||||
omitted grade **and** a grade from a newer catalog both resolve to high, so the
|
||||
boundary fails safe in both directions. `is_graded` exposes the distinction, and
|
||||
`test_every_repo_catalog_lane_is_explicitly_graded` is the CI gate that stops an
|
||||
ungraded lane being committed. Four regression tests cover it.
|
||||
|
||||
`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.
|
||||
|
||||
**Still open on this task:** whether the maturity-derived default replaces the
|
||||
`ungraded` sentinel entirely (waits on `ZONE-WP-0001-T03`), and verifying
|
||||
OpenBao's `agent-high-risk-boundary` policy covers these paths.
|
||||
|
||||
## Related
|
||||
|
||||
- `zone-engine` `ZONE-WP-0001` — the model, and where this work is led from
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue