feat(case): KERNEL-2/3 lane resolution and mode-aware admission; CATALOG-1 draft lanes
nk48 1.3 owner review (owner-review-ops-warden.json): - KERNEL-2: Executor.admit parses catalog_source and resolves descriptor.lane; refuses a missing, non-active, undelegated or ungraded lane, an owner outside owner_repo, and a mint/change effect on a lane not graded risk high. catalog-routing-acceptance still covers the whole catalog digest. - KERNEL-3: dispatch mode is read from the type's follow_through step (warden_executes/attended). The step must declare both booleans and a lane and agree with the descriptor's lane and owner. Attended: human actor, WARDEN_AGENT_ID unset, parent-session revocation scope, caller lane equal to the step lane. Owner: native delegation, owner_repo == descriptor.owner, no Warden-executed lane and no Warden-brokered mint. - CATALOG-1: draft entries keycape-attended-case-change (OW-1.3-C1), platform-secret-delivery, consumer-owner-deployment, apps-consumer-handover. Not promoted; promotion waits for owner confirmation. - Fixture catalogs and type steps in tests; published routing_lanes coverage and the workload-join count follow the four new not-applicable lanes. PARAMETER_PINS untouched (PIN-1 waits for founder acknowledgment). 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
117fec9241
commit
a14376cf62
8 changed files with 363 additions and 13 deletions
|
|
@ -98,7 +98,7 @@ on_apply:
|
||||||
# 2026-10-08: platform-supplier-credentials added as a not-applicable path pattern,
|
# 2026-10-08: platform-supplier-credentials added as a not-applicable path pattern,
|
||||||
# so routing not_applicable rose 16 -> 17.
|
# so routing not_applicable rose 16 -> 17.
|
||||||
classification_coverage:
|
classification_coverage:
|
||||||
measured: "2026-10-10"
|
measured: "2026-10-17"
|
||||||
attribution: self-measured
|
attribution: self-measured
|
||||||
signing_targets:
|
signing_targets:
|
||||||
resolved: 0
|
resolved: 0
|
||||||
|
|
@ -107,7 +107,7 @@ classification_coverage:
|
||||||
routing_lanes:
|
routing_lanes:
|
||||||
resolved: 4
|
resolved: 4
|
||||||
unknown: 22
|
unknown: 22
|
||||||
not_applicable: 18
|
not_applicable: 22
|
||||||
|
|
||||||
verdict_caching: none
|
verdict_caching: none
|
||||||
input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py"
|
input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py"
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,24 @@
|
||||||
# declares it, and is null where the field set has not been established --
|
# declares it, and is null where the field set has not been established --
|
||||||
# null means unknown, never 'one field'.
|
# null means unknown, never 'one field'.
|
||||||
|
|
||||||
generated_at: "2026-10-14T20:20:19Z"
|
generated_at: "2026-10-17T10:04:57Z"
|
||||||
source: ops-warden/registry/routing/catalog.yaml
|
source: ops-warden/registry/routing/catalog.yaml
|
||||||
catalog_revision: "402b008986c3a710c4f5b41bb1007a55c8d4363e"
|
catalog_revision: "402b008986c3a710c4f5b41bb1007a55c8d4363e"
|
||||||
catalog_revision_date: "2026-10-14T22:20:15+02:00"
|
catalog_revision_date: "2026-10-14T22:20:15+02:00"
|
||||||
catalog_dirty: false
|
catalog_dirty: true
|
||||||
high_risk_lane_count: 35
|
high_risk_lane_count: 38
|
||||||
concrete_path_count: 17
|
concrete_path_count: 17
|
||||||
|
|
||||||
# Graded high but not a single KV address -- a routing pattern, a broker
|
# Graded high but not a single KV address -- a routing pattern, a broker
|
||||||
# grant, or a non-KV lane. Nothing here for a policy to deny.
|
# grant, or a non-KV lane. Nothing here for a policy to deny.
|
||||||
no_concrete_path:
|
no_concrete_path:
|
||||||
|
- consumer-owner-deployment
|
||||||
- database-dynamic-credentials
|
- database-dynamic-credentials
|
||||||
- flex-auth-decision-envelope-signing-key
|
- flex-auth-decision-envelope-signing-key
|
||||||
- fluid-telegram-operator
|
- fluid-telegram-operator
|
||||||
- informed-decision-sitting-requester-login
|
- informed-decision-sitting-requester-login
|
||||||
- inter-hub-bootstrap-ssh
|
- inter-hub-bootstrap-ssh
|
||||||
|
- keycape-attended-case-change
|
||||||
- keycape-client-secret-generate
|
- keycape-client-secret-generate
|
||||||
- net-kingdom-lldap-bind-credential
|
- net-kingdom-lldap-bind-credential
|
||||||
- net-kingdom-privacyidea-admin-token
|
- net-kingdom-privacyidea-admin-token
|
||||||
|
|
@ -35,6 +37,7 @@ no_concrete_path:
|
||||||
- openbao-shamir-recovery-ceremony
|
- openbao-shamir-recovery-ceremony
|
||||||
- openbao-structure-survey
|
- openbao-structure-survey
|
||||||
- ops-warden-warden-sign-token
|
- ops-warden-warden-sign-token
|
||||||
|
- platform-secret-delivery
|
||||||
- platform-supplier-credentials
|
- platform-supplier-credentials
|
||||||
- secrets-engine-approval-client-login
|
- secrets-engine-approval-client-login
|
||||||
- secrets-engine-requester-login
|
- secrets-engine-requester-login
|
||||||
|
|
|
||||||
|
|
@ -1604,3 +1604,89 @@ entries:
|
||||||
automatable: false
|
automatable: false
|
||||||
steps:
|
steps:
|
||||||
- "This lane covers first generation only and refuses an existing path (exit 3). Rotation is a separate railiance-platform task that does not exist yet; route rotation needs to railiance-platform."
|
- "This lane covers first generation only and refuses an existing path (exit 3). Rotation is a separate railiance-platform task that does not exist yet; route rotation needs to railiance-platform."
|
||||||
|
|
||||||
|
# CATALOG-1 (nk48 1.3 owner review, 2026-10-17): four lanes for the 1.3
|
||||||
|
# SecurityCaseType oidc-browser-relying-party. Pointer only, draft. Each
|
||||||
|
# stays draft until its owner confirms the pointer
|
||||||
|
# (wiki/playbooks/catalog-lane-promotion.md); the kernel refuses draft lanes.
|
||||||
|
- id: keycape-attended-case-change
|
||||||
|
title: SecurityCase attended KeyCape change (ensure-groups, bind-env, register-client)
|
||||||
|
# OW-1.3-C1: dedicated attended change authority replacing key-cape-oidc-login
|
||||||
|
# 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.
|
||||||
|
risk: high
|
||||||
|
workload_ref:
|
||||||
|
applicability: not-applicable
|
||||||
|
reason: "Per-case attended change lane; the consuming service is a case parameter."
|
||||||
|
need_keywords: [keycape-attended-case-change, ensure-groups, bind-env, register-client, attended keycape change]
|
||||||
|
owner_repo: key-cape/net-kingdom
|
||||||
|
subsystem: key-cape registry and Deployment env, net-kingdom groups
|
||||||
|
warden_executes: false
|
||||||
|
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
|
||||||
|
canon_ref: net-kingdom/security-case-types/oidc-browser-relying-party/type.yaml
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
status: draft
|
||||||
|
delegation:
|
||||||
|
mode: interim
|
||||||
|
intended_owner: key-cape
|
||||||
|
blocked_on: "key-cape and net-kingdom confirm the pointer and the re-frozen 1.3 descriptors (OW-1.3-C1)."
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
verified: source-read
|
||||||
|
- id: platform-secret-delivery
|
||||||
|
title: SecurityCase deliver-secret, KeyCape client secret delivered by Platform
|
||||||
|
risk: high
|
||||||
|
workload_ref:
|
||||||
|
applicability: not-applicable
|
||||||
|
reason: "Per-case delivery lane; each case's consuming service gets its own client entry."
|
||||||
|
need_keywords: [platform-secret-delivery, deliver-secret, keycape client secret delivery]
|
||||||
|
owner_repo: railiance-platform
|
||||||
|
subsystem: railiance-platform External Secrets delivery
|
||||||
|
warden_executes: false
|
||||||
|
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
|
||||||
|
canon_ref: railiance-platform/AGENTS.md
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
status: draft
|
||||||
|
delegation:
|
||||||
|
mode: native
|
||||||
|
intended_owner: railiance-platform
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
- id: consumer-owner-deployment
|
||||||
|
title: SecurityCase deliver-consumer-secret, consumer deployment by its owner
|
||||||
|
# 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.
|
||||||
|
risk: high
|
||||||
|
workload_ref:
|
||||||
|
applicability: not-applicable
|
||||||
|
reason: "Per-case consumer deployment; the consumer is a case parameter."
|
||||||
|
need_keywords: [consumer-owner-deployment, deliver-consumer-secret]
|
||||||
|
owner_repo: railiance-apps
|
||||||
|
subsystem: railiance-apps consumer deployment
|
||||||
|
warden_executes: false
|
||||||
|
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
|
||||||
|
canon_ref: railiance-apps/AGENTS.md
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
status: draft
|
||||||
|
delegation:
|
||||||
|
mode: native
|
||||||
|
intended_owner: railiance-apps
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
- id: apps-consumer-handover
|
||||||
|
title: SecurityCase hand-over to the consuming service owner
|
||||||
|
risk: standard
|
||||||
|
workload_ref:
|
||||||
|
applicability: not-applicable
|
||||||
|
reason: "Per-case hand-over; the consumer owner is a case parameter."
|
||||||
|
need_keywords: [apps-consumer-handover, hand-over, consumer handover]
|
||||||
|
owner_repo: railiance-apps
|
||||||
|
subsystem: railiance-apps consumer hand-over
|
||||||
|
warden_executes: false
|
||||||
|
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
|
||||||
|
canon_ref: railiance-apps/AGENTS.md
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
status: draft
|
||||||
|
delegation:
|
||||||
|
mode: native
|
||||||
|
intended_owner: railiance-apps
|
||||||
|
reviewed: "2026-10-17"
|
||||||
|
|
|
||||||
|
|
@ -553,6 +553,67 @@ class Journal:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_lane(catalog_source: bytes, descriptor: Descriptor):
|
||||||
|
"""KERNEL-2: the descriptor's lane must be an active, fitting catalog entry.
|
||||||
|
|
||||||
|
catalog-routing-acceptance still covers the whole catalog digest; this
|
||||||
|
ties the one lane the descriptor names to an entry inside that digest.
|
||||||
|
"""
|
||||||
|
from .routing.catalog import CatalogError, _parse_entry
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = yaml.safe_load(catalog_source)
|
||||||
|
items = raw["entries"]
|
||||||
|
if not isinstance(items, list):
|
||||||
|
raise TypeError
|
||||||
|
entries = {}
|
||||||
|
for index, item in enumerate(items):
|
||||||
|
parsed = _parse_entry(item, index)
|
||||||
|
if parsed.id in entries:
|
||||||
|
raise ValueError
|
||||||
|
entries[parsed.id] = parsed
|
||||||
|
except (yaml.YAMLError, CatalogError, KeyError, TypeError, ValueError):
|
||||||
|
raise Refused("invalid catalog source shape") from None
|
||||||
|
entry = entries.get(descriptor.lane)
|
||||||
|
if entry is None:
|
||||||
|
raise Refused("descriptor lane is not in the catalog")
|
||||||
|
if not entry.is_active:
|
||||||
|
raise Refused("descriptor lane is not active in the catalog")
|
||||||
|
if entry.delegation is None:
|
||||||
|
raise Refused("descriptor lane has no catalog delegation record")
|
||||||
|
if not entry.is_graded:
|
||||||
|
raise Refused("descriptor lane carries no risk grade")
|
||||||
|
if descriptor.owner not in entry.owner_repo.split("/"):
|
||||||
|
raise Refused("descriptor owner does not fit catalog lane owner_repo")
|
||||||
|
if descriptor.effect in ("mint", "change") and entry.risk != "high":
|
||||||
|
raise Refused("catalog lane risk does not cover the descriptor effect")
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _step_mode(case_type: dict, case: dict, descriptor: Descriptor) -> str:
|
||||||
|
"""KERNEL-3: warden, attended or owner, from the type step's own text."""
|
||||||
|
steps = [
|
||||||
|
step
|
||||||
|
for step in case_type.get("follow_through") or []
|
||||||
|
if isinstance(step, dict) and step.get("id") == descriptor.step
|
||||||
|
]
|
||||||
|
step = steps[0] if len(steps) == 1 else {}
|
||||||
|
executes, attended = step.get("warden_executes"), step.get("attended")
|
||||||
|
if (
|
||||||
|
type(executes) is not bool
|
||||||
|
or type(attended) is not bool
|
||||||
|
or (executes and attended)
|
||||||
|
or not isinstance(step.get("lane"), str)
|
||||||
|
):
|
||||||
|
raise Refused("type step does not declare how the kernel dispatches it")
|
||||||
|
owner = step.get("owner")
|
||||||
|
if isinstance(owner, str) and re.fullmatch(r"\{[a-z_]+\}", owner):
|
||||||
|
owner = (case.get("parameters") or {}).get(owner[1:-1])
|
||||||
|
if step["lane"] != descriptor.lane or owner != descriptor.owner:
|
||||||
|
raise Refused("type step lane or owner disagrees with descriptor")
|
||||||
|
return "warden" if executes else "attended" if attended else "owner"
|
||||||
|
|
||||||
|
|
||||||
class Executor:
|
class Executor:
|
||||||
"""Injected fixed owner adapters and provenance verifier; none ship by default.
|
"""Injected fixed owner adapters and provenance verifier; none ship by default.
|
||||||
|
|
||||||
|
|
@ -650,6 +711,21 @@ class Executor:
|
||||||
actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
actor != "human" or os.getenv("WARDEN_AGENT_ID")
|
||||||
):
|
):
|
||||||
raise Refused("agent mint is forbidden")
|
raise Refused("agent mint is forbidden")
|
||||||
|
entry = _catalog_lane(catalog_source, descriptor)
|
||||||
|
mode = _step_mode(case_type, case, descriptor)
|
||||||
|
if mode == "attended" and (
|
||||||
|
actor != "human"
|
||||||
|
or os.getenv("WARDEN_AGENT_ID")
|
||||||
|
or "parent-session" not in descriptor.revocation_scopes
|
||||||
|
):
|
||||||
|
raise Refused("attended step requires a human parent session")
|
||||||
|
if mode == "owner" and (
|
||||||
|
entry.effective_delegation.mode != "native"
|
||||||
|
or entry.owner_repo != descriptor.owner
|
||||||
|
or entry.warden_executes
|
||||||
|
or descriptor.effect == "mint"
|
||||||
|
):
|
||||||
|
raise Refused("owner step lane is not the owner's native path")
|
||||||
parameters = case.get("parameters", {})
|
parameters = case.get("parameters", {})
|
||||||
declared = {
|
declared = {
|
||||||
item["name"]: item
|
item["name"]: item
|
||||||
|
|
|
||||||
135
tests/test_case_admission_modes.py
Normal file
135
tests/test_case_admission_modes.py
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
"""KERNEL-2 lane resolution and KERNEL-3 mode-aware admission (fixture only)."""
|
||||||
|
from dataclasses import asdict, replace
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from warden.case_receipt_state import Refused
|
||||||
|
from warden.case_run import Descriptor, digest
|
||||||
|
from tests.test_case_run import MINT_STEP, fixture_catalog, lane_entry, make_rig
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rig(tmp_path):
|
||||||
|
return make_rig(tmp_path)
|
||||||
|
|
||||||
|
EVIDENCE = {"acceptance", "materialization", "descriptor", "session"}
|
||||||
|
|
||||||
|
|
||||||
|
def admit(rig, *, step=None, entries=None, catalog=None, desc=None, **over):
|
||||||
|
executor, _, _, sources, kwargs = rig
|
||||||
|
case, typ, descriptor, _ = sources
|
||||||
|
if desc is not None:
|
||||||
|
base = Descriptor.decode(descriptor)
|
||||||
|
descriptor = json.dumps(asdict(replace(base, **desc))).encode()
|
||||||
|
doc = yaml.safe_load(typ)
|
||||||
|
doc["spec"]["execution_descriptors"] = {"mint-secret": digest(descriptor)}
|
||||||
|
if step is not None:
|
||||||
|
doc["spec"]["follow_through"] = step
|
||||||
|
typ = yaml.safe_dump(doc).encode()
|
||||||
|
if catalog is None:
|
||||||
|
catalog = fixture_catalog(*(entries or [lane_entry()]))
|
||||||
|
executor.evidence_verifier = lambda purpose, source, evidence: evidence in EVIDENCE
|
||||||
|
return executor.admit(case, typ, descriptor, catalog, **{**kwargs, **over})
|
||||||
|
|
||||||
|
|
||||||
|
def mode_step(executes, attended):
|
||||||
|
return [dict(MINT_STEP, warden_executes=executes, attended=attended)]
|
||||||
|
|
||||||
|
|
||||||
|
def refused(message, rig, **kw):
|
||||||
|
with pytest.raises(Refused, match=message):
|
||||||
|
admit(rig, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
# Positive modes
|
||||||
|
def test_warden_mode_admits(rig):
|
||||||
|
assert admit(rig)[1].lane == "issuer-lane"
|
||||||
|
|
||||||
|
|
||||||
|
def test_attended_mode_admits_human_parent_session(rig, monkeypatch):
|
||||||
|
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
||||||
|
admitted = admit(
|
||||||
|
rig,
|
||||||
|
step=mode_step(False, True),
|
||||||
|
desc=dict(effect="change", revocation_scopes=("parent-session",)),
|
||||||
|
)
|
||||||
|
assert admitted[1].actor == "human"
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_mode_admits_native_owner_lane(rig):
|
||||||
|
admitted = admit(rig, step=mode_step(False, False), desc=dict(effect="change"))
|
||||||
|
assert admitted[1].owner == "owner"
|
||||||
|
|
||||||
|
|
||||||
|
# KERNEL-2
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"entries, catalog, message",
|
||||||
|
[
|
||||||
|
(None, b"not: [a catalog", "invalid catalog source shape"),
|
||||||
|
([lane_entry("other-lane")], None, "descriptor lane is not in the catalog"),
|
||||||
|
([lane_entry(status="draft")], None, "descriptor lane is not active in the catalog"),
|
||||||
|
([{k: v for k, v in lane_entry().items() if k != "delegation"}], None,
|
||||||
|
"descriptor lane has no catalog delegation record"),
|
||||||
|
([{k: v for k, v in lane_entry().items() if k != "risk"}], None,
|
||||||
|
"descriptor lane carries no risk grade"),
|
||||||
|
([lane_entry(owner="someone-else")], None,
|
||||||
|
"descriptor owner does not fit catalog lane owner_repo"),
|
||||||
|
([lane_entry(risk="standard")], None,
|
||||||
|
"catalog lane risk does not cover the descriptor effect"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_catalog_lane_refusals(rig, entries, catalog, message):
|
||||||
|
refused(message, rig, entries=entries, catalog=catalog)
|
||||||
|
|
||||||
|
|
||||||
|
# KERNEL-3: the step must declare dispatch and agree with the descriptor
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"step, message",
|
||||||
|
[
|
||||||
|
([], "type step does not declare how the kernel dispatches it"),
|
||||||
|
(mode_step(True, True), "type step does not declare how the kernel dispatches it"),
|
||||||
|
([{k: v for k, v in MINT_STEP.items() if k != "lane"}],
|
||||||
|
"type step does not declare how the kernel dispatches it"),
|
||||||
|
([dict(MINT_STEP, warden_executes="yes")],
|
||||||
|
"type step does not declare how the kernel dispatches it"),
|
||||||
|
([dict(MINT_STEP, lane="other-lane")], "type step lane or owner disagrees with descriptor"),
|
||||||
|
([dict(MINT_STEP, owner="other")], "type step lane or owner disagrees with descriptor"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_step_declaration_refusals(rig, step, message):
|
||||||
|
refused(message, rig, step=step)
|
||||||
|
|
||||||
|
|
||||||
|
def test_attended_refuses_agent_session(rig, monkeypatch):
|
||||||
|
monkeypatch.setenv("WARDEN_AGENT_ID", "agent-1")
|
||||||
|
refused("attended step requires a human parent session", rig,
|
||||||
|
step=mode_step(False, True), desc=dict(effect="change"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_attended_refuses_agent_actor(rig):
|
||||||
|
refused("attended step requires a human parent session", rig,
|
||||||
|
step=mode_step(False, True), desc=dict(effect="change", actor="agent"),
|
||||||
|
actor="agent")
|
||||||
|
|
||||||
|
|
||||||
|
def test_attended_refuses_caller_lane_other_than_step_lane(rig):
|
||||||
|
refused("principal actor or lane mismatch", rig,
|
||||||
|
step=mode_step(False, True), desc=dict(effect="change"), lane="login-lane")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"entry, desc",
|
||||||
|
[
|
||||||
|
(lane_entry(delegation={"mode": "interim", "intended_owner": "owner",
|
||||||
|
"blocked_on": "fixture", "reviewed": "2026-10-17"}),
|
||||||
|
dict(effect="change")),
|
||||||
|
(lane_entry(owner="owner/other"), dict(effect="change")),
|
||||||
|
(lane_entry(warden_executes=True), dict(effect="change")),
|
||||||
|
(lane_entry(), {}), # owner step may not be a Warden-brokered mint
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_owner_mode_refusals(rig, entry, desc):
|
||||||
|
refused("owner step lane is not the owner's native path", rig,
|
||||||
|
step=mode_step(False, False), entries=[entry], desc=desc)
|
||||||
|
|
@ -44,6 +44,9 @@ def browser(rig, version="1.1", mutate=None):
|
||||||
case["spec"]["parameters"]["consumer_namespace"] = "forgejo"
|
case["spec"]["parameters"]["consumer_namespace"] = "forgejo"
|
||||||
# This extension and all native provenance are TEST ONLY, never source assent.
|
# This extension and all native provenance are TEST ONLY, never source assent.
|
||||||
typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])}
|
typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])}
|
||||||
|
for step in typ["spec"]["follow_through"]:
|
||||||
|
if step["id"] == "mint-secret": # TEST ONLY: route to the fixture lane
|
||||||
|
step.update(owner="owner", lane="issuer-lane")
|
||||||
if mutate:
|
if mutate:
|
||||||
mutate(case, typ)
|
mutate(case, typ)
|
||||||
changed = (
|
changed = (
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,47 @@ from warden.case_receipt_state import Outcome, Refused
|
||||||
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
|
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def lane_entry(lane="issuer-lane", owner="owner", **over):
|
||||||
def rig(tmp_path):
|
"""Fixture catalog entry (TEST ONLY; never a real routing pointer)."""
|
||||||
|
entry = {
|
||||||
|
"id": lane,
|
||||||
|
"title": "fixture lane",
|
||||||
|
"need_keywords": [lane],
|
||||||
|
"owner_repo": owner,
|
||||||
|
"subsystem": "fixture",
|
||||||
|
"warden_executes": False,
|
||||||
|
"wiki_ref": "wiki/fixture.md",
|
||||||
|
"canon_ref": "fixture/canon.md",
|
||||||
|
"reviewed": "2026-10-17",
|
||||||
|
"status": "active",
|
||||||
|
"risk": "high",
|
||||||
|
"workload_ref": {"applicability": "not-applicable", "reason": "fixture"},
|
||||||
|
"delegation": {
|
||||||
|
"mode": "native",
|
||||||
|
"intended_owner": owner,
|
||||||
|
"reviewed": "2026-10-17",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
entry.update(over)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_catalog(*entries):
|
||||||
|
return yaml.safe_dump({"entries": list(entries) or [lane_entry()]}).encode()
|
||||||
|
|
||||||
|
|
||||||
|
CATALOG = fixture_catalog()
|
||||||
|
|
||||||
|
MINT_STEP = {
|
||||||
|
"id": "mint-secret",
|
||||||
|
"owner": "owner",
|
||||||
|
"lane": "issuer-lane",
|
||||||
|
"warden_executes": True,
|
||||||
|
"attended": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_rig(tmp_path):
|
||||||
desc = Descriptor(
|
desc = Descriptor(
|
||||||
"warden.case.execution.v1",
|
"warden.case.execution.v1",
|
||||||
"mint-secret",
|
"mint-secret",
|
||||||
|
|
@ -43,6 +82,7 @@ def rig(tmp_path):
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"execution_descriptors": {"mint-secret": digest(descriptor)},
|
"execution_descriptors": {"mint-secret": digest(descriptor)},
|
||||||
|
"follow_through": [dict(MINT_STEP)],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).encode()
|
).encode()
|
||||||
|
|
@ -106,7 +146,7 @@ def rig(tmp_path):
|
||||||
("ops-mason-materialization", digest(case), "materialization"),
|
("ops-mason-materialization", digest(case), "materialization"),
|
||||||
("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"),
|
("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"),
|
||||||
("type-machine-seam-acceptance", digest(typ), "descriptor"),
|
("type-machine-seam-acceptance", digest(typ), "descriptor"),
|
||||||
("catalog-routing-acceptance", digest(b"catalog"), "descriptor"),
|
("catalog-routing-acceptance", digest(CATALOG), "descriptor"),
|
||||||
("caller-session:human:operator:issuer-lane", digest(case), "session"),
|
("caller-session:human:operator:issuer-lane", digest(case), "session"),
|
||||||
("session-revocation:issuer-lane", digest(case), "session-revoked"),
|
("session-revocation:issuer-lane", digest(case), "session-revoked"),
|
||||||
("grant-revocation:issuer-lane", digest(case), "grant-revoked"),
|
("grant-revocation:issuer-lane", digest(case), "grant-revoked"),
|
||||||
|
|
@ -136,8 +176,13 @@ def rig(tmp_path):
|
||||||
descriptor_evidence="descriptor",
|
descriptor_evidence="descriptor",
|
||||||
session_evidence="session",
|
session_evidence="session",
|
||||||
)
|
)
|
||||||
admitted = executor.admit(case, typ, descriptor, b"catalog", **kwargs)
|
admitted = executor.admit(case, typ, descriptor, CATALOG, **kwargs)
|
||||||
return (executor, admitted, calls, (case, typ, descriptor, b"catalog"), kwargs)
|
return (executor, admitted, calls, (case, typ, descriptor, CATALOG), kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rig(tmp_path):
|
||||||
|
return make_rig(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig):
|
def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig):
|
||||||
|
|
@ -387,7 +432,7 @@ def test_changed_catalog_pins_cannot_inherit_old_success(rig):
|
||||||
executor, args, calls, sources, kwargs = rig
|
executor, args, calls, sources, kwargs = rig
|
||||||
executor.run(*args)
|
executor.run(*args)
|
||||||
with pytest.raises(Refused):
|
with pytest.raises(Refused):
|
||||||
executor.admit(*sources[:3], b"changed-catalog", **kwargs)
|
executor.admit(*sources[:3], fixture_catalog(lane_entry(title="changed")), **kwargs)
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -601,7 +646,7 @@ def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal(
|
||||||
executor, args, calls, sources, kwargs = rig
|
executor, args, calls, sources, kwargs = rig
|
||||||
executor.run(*args)
|
executor.run(*args)
|
||||||
original_attempt = calls[-1].attempt_id
|
original_attempt = calls[-1].attempt_id
|
||||||
new_catalog = b"independently-reviewed-catalog-v2"
|
new_catalog = fixture_catalog(lane_entry(title="independently reviewed v2"))
|
||||||
old_verifier = executor.evidence_verifier
|
old_verifier = executor.evidence_verifier
|
||||||
|
|
||||||
def verifier(purpose, source, evidence):
|
def verifier(purpose, source, evidence):
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ def test_repo_catalog_uses_only_explicit_workload_references():
|
||||||
# names fluid-telegram, which has published no workload declaration.
|
# names fluid-telegram, which has published no workload declaration.
|
||||||
# not_applicable 18 since 2026-10-10: keycape-client-secret-generate is a
|
# not_applicable 18 since 2026-10-10: keycape-client-secret-generate is a
|
||||||
# per-case generation lane; each case's consumer gets its own entry.
|
# per-case generation lane; each case's consumer gets its own entry.
|
||||||
assert len(report["not_applicable"]) == 18
|
# not_applicable 22 since 2026-10-17: the four CATALOG-1 draft lanes
|
||||||
|
# (nk48 1.3 owner review) are per-case lanes like the one above.
|
||||||
|
assert len(report["not_applicable"]) == 22
|
||||||
assert {row["lane"] for row in report["resolved"]} == {
|
assert {row["lane"] for row in report["resolved"]} == {
|
||||||
"ops-warden-warden-sign-token",
|
"ops-warden-warden-sign-token",
|
||||||
"issue-core-ingestion-api-key",
|
"issue-core-ingestion-api-key",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue