feat(case): attended-change lane kind with session_lane; grade apps-consumer-handover high

LANE-KIND accepts an attended step on a login lane, or on an attended-change
lane whose session_lane resolves to an active, reviewed login lane.
keycape-attended-case-change -> session_lane key-cape-oidc-login (OW-1.3-C1).
consumer-owner-deployment and apps-consumer-handover -> pending-owner-confirmation:
no catalog login lane authenticates a railiance-apps operator session.
apps-consumer-handover risk: high (KERNEL-2, change effect).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 903097@bnt-lap001
Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
This commit is contained in:
tegwick 2026-10-17 12:13:49 +02:00
parent 69fdba2907
commit 207d29e8d1
4 changed files with 19 additions and 3 deletions

View file

@ -1615,6 +1615,8 @@ entries:
# for kernel-dispatched KeyCape registry, group and Deployment env changes.
# The session may be established by a key-cape OIDC login; that login lane
# stays the identity step, not the change authority.
lane: attended-change
session_lane: key-cape-oidc-login
risk: high
workload_ref:
applicability: not-applicable
@ -1656,6 +1658,8 @@ entries:
# Descriptor actor is agent: ADR-0004 read boundary applies. Owner-side
# delivery only; no raw value reaches the agent session. If that cannot be
# shown, the actor changes to human and the railiance-apps digest changes.
lane: attended-change
session_lane: pending-owner-confirmation
risk: high
workload_ref:
applicability: not-applicable
@ -1674,7 +1678,9 @@ entries:
reviewed: "2026-10-17"
- id: apps-consumer-handover
title: SecurityCase hand-over to the consuming service owner
risk: standard
lane: attended-change
session_lane: pending-owner-confirmation
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case hand-over; the consumer owner is a case parameter."

View file

@ -138,8 +138,13 @@ def check_follow_through(doc: dict, catalog: Optional[Catalog] = None) -> Follow
findings.append(Finding("catalog", "LANE-MISSING", where, f"lane '{lane}' is not in the catalog"))
elif not entry.is_active or not entry.reviewed:
findings.append(Finding("catalog", "LANE-UNREVIEWED", where, f"lane '{lane}' is not active and reviewed"))
elif attended and entry.lane == "attended-change":
session = catalog.get(entry.session_lane or "")
if session is None or not session.is_active or not session.reviewed or session.lane != "login":
findings.append(Finding("catalog", "LANE-KIND", where,
f"attended-change lane '{lane}' session_lane '{entry.session_lane}' does not resolve to an active, reviewed login lane"))
elif attended and entry.lane != "login":
findings.append(Finding("catalog", "LANE-KIND", where, f"attended step's lane '{lane}' is not a login lane"))
findings.append(Finding("catalog", "LANE-KIND", where, f"attended step's lane '{lane}' is not a login or attended-change lane"))
if executes:
warden_steps.append(sid)

View file

@ -69,7 +69,7 @@ _REQUIRED_FIELDS = (
"workload_ref",
)
_VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login", "ceremony")
_VALID_LANES = ("secret", "login", "ceremony", "attended-change")
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
# Default review cadence for a catalog pointer — "is this still the right owner
@ -697,6 +697,9 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
raise CatalogError(
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
)
session_lane = str(raw["session_lane"]).strip() if raw.get("session_lane") else None
if lane == "attended-change" and not session_lane:
raise CatalogError(f"entry {entry_id!r} is an attended-change lane and must name session_lane")
risk_value = raw.get("risk")
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
@ -728,6 +731,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
exec_capable=exec_capable,
policy_ref=handoff["policy_ref"],
lane=lane,
session_lane=session_lane,
exec_owner=str(raw["exec_owner"]) if raw.get("exec_owner") else None,
exec_command=handoff["exec_command"],
pointer_command=handoff["pointer_command"],

View file

@ -177,6 +177,7 @@ class RouteEntry:
# "ceremony" — attended owner operation such as Shamir seal/unseal. It is a
# pointer plus approval boundary, never an executable access lane.
lane: str = "secret"
session_lane: Optional[str] = None # attended-change: login lane whose session establishes it
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem
# (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the
# catalog routes to it and keeps ops-warden's own --fetch/--exec proxy as a transparent