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:
parent
69fdba2907
commit
207d29e8d1
4 changed files with 19 additions and 3 deletions
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue