feat(WARDEN-WP-0014): T4 — key-cape login orchestration lane

Adds a lane: secret|login field to RouteEntry. The login lane is an
interactive auth bootstrap: it skips the caller-auth precheck (no token
yet — that's the point) and the secret-read gate (it establishes the
identity the gate needs), runs the owner's login command interactively
as the caller via inherited stdio, and rejects --exec. The token stays
in the caller's own store; warden never captures it (G2 holds). Audited
as action: login. key-cape-oidc-login populated as the reference login
entry. Advisory proxy hint updated now that T3 has shipped.

172 passed, lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 17:31:55 +02:00
parent 1a02ec6753
commit 1c3d1b4d52
6 changed files with 136 additions and 32 deletions

View File

@@ -55,6 +55,7 @@ _REQUIRED_FIELDS = (
"status",
)
_VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login")
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
DEFAULT_STALE_DAYS = 90
@@ -227,6 +228,12 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
"a proxyable lane must declare the command warden runs as the caller."
)
lane = str(raw.get("lane", "secret"))
if lane not in _VALID_LANES:
raise CatalogError(
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
)
return RouteEntry(
id=entry_id,
title=str(raw["title"]),
@@ -245,6 +252,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
fetch_command=handoff["fetch_command"],
exec_capable=exec_capable,
policy_ref=handoff["policy_ref"],
lane=lane,
)

View File

@@ -36,6 +36,13 @@ class RouteEntry:
fetch_command: Optional[str] = None # command skeleton run *as the caller*
exec_capable: bool = False # may `warden access --fetch/--exec` proxy it
policy_ref: Optional[str] = None # flex-auth check the fetch path runs first
# Proxy lane semantics (WP-0014 T4):
# "secret" — read a value (gated by flex-auth secret-read; caller must already
# be authenticated; value transits via inherit-stdout or child env).
# "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have
# no identity yet), no caller-auth precheck (the point is to get one),
# run interactively as the caller; warden never captures the token.
lane: str = "secret"
@property
def is_active(self) -> bool: