the-custodian/docs/changes/netkingdom-target-2026-10-16/browser-case/implementation.patch

552 lines
31 KiB
Diff
Raw Normal View History

diff --git a/tools/security-case-types/README.md b/tools/security-case-types/README.md
index 45e1106..dc4fc91 100644
--- a/tools/security-case-types/README.md
+++ b/tools/security-case-types/README.md
@@ -50,3 +50,48 @@ This ordinary command refuses:1.2 is acknowledged, not implemented. For source-o
The Python API is load_type(default_root, exact_reference, version_roots={"1.2": explicit_root}). Each key must be an exact major.minor and each root a Path. CLI --version-root VERSION=PATH may repeat for DIFFERENT versions. A matching explicit version selects exactly one root; a missing/wrong/ambiguous source refuses without trying the default or another revision. Versions without a mapping continue using --types. No directory discovery, highest/latest selection, status promotion or automatic owner-adapter adoption occurs. Duplicate mappings (even identical), duplicate YAML fields, mismatched source ID/version, malformed/traversal references and selected symlinks refuse. Direct case validation also requires the case reference to match the supplied type ID/version.
An explicit root is source input, not an owner acknowledgment or grant. Callers retain exact source hashes/type/case/render in their own reviewed receipts. Existing two-argument load_type callers, including ops-mason, retain their default behavior until their owner deliberately adopts the optional input. Adapter/global provenance/native first-reference gates remain separate in NK-WP-0048-T06 and the existing ORGREF driver.
+
+## Check existing owner evidence compatibility
+
+The normal validator CLI now accepts `check-evidence` using the same exact type
+selection, declarations, validation and deterministic render as `render`:
+
+```sh
+python3 tools/security-case-types/security_case_validator.py check-evidence \
+ security-cases/forgejo-netkingdom-oidc.yaml --types security-case-types \
+ --structure-request /path/to/mason-structure-request.json \
+ --warden-journal /path/to/existing/journal.sqlite
+```
+
+Normal admission still refuses the acknowledged 1.1 and 1.2 types. Explicit
+`--allow-unimplemented` permits source preparation, with the existing
+`--version-root 1.2=security-case-type-revisions/1.2` for that version. No owner
+protocol is automatically adopted, no journal is created, and no status changes.
+
+This is the cross-owner **compatibility preflight**, not native acceptance. It
+consumes the existing `ops-mason.case-structure-request.v1` and reads latest rows
+from the existing `warden.case.step-journal.v1` SQLite journal in read-only mode.
+Mason source/request hashes, complete current structural inventory and structural
+projection must match the fresh render. Warden case/type and type-declared machine
+descriptor pins, owners, bounded histories, created-object scope and successful
+attempt/revocation fingerprints must match. Unknown or duplicate fields, source
+substitution and missing completed-attempt fingerprints refuse. Running, uncertain,
+failed or rolled-back outcomes retain their exact meanings; an earlier success
+never overrides the latest journal row. Generated-value paths require distinct
+parent-session and broker-grant fingerprint fields.
+
+Exit 0 means these supplied artifacts are source-compatible; it never means the
+case is complete. No artifact is required solely to render missing gates. Output
+always has `native_complete: false`, `execution.permitted: false`, and explicit
+unresolved native gates. A Mason Request has no native Observation provenance;
+a Warden journal retains fingerprints rather than portable signed/native proof.
+This command deliberately offers no trust callback, native-success flag, dispatch
+or way to promote self-authored JSON into native evidence. Actual independent
+owner evidence admission, status-hash binding, accepted machine descriptors,
+delivery, human sign-in/consumer denial, revocation and created-only rollback
+remain NK-WP-0048-T06 owner integration work. It does not mark any type implemented.
+
+Integration tests invoke actual sibling Mason `prepare` and Warden `Journal.save`
+with metadata fixtures only. They need those owner source checkouts, and existing
+type suites need the public KeyCape request file and provider declarations beside
+the checkout. `NET_KINGDOM_ROOT` selects the source root for the 1.2 package tests.
diff --git a/tools/security-case-types/security_case_evidence.py b/tools/security-case-types/security_case_evidence.py
new file mode 100644
index 0000000..0bf0a84
--- /dev/null
+++ b/tools/security-case-types/security_case_evidence.py
@@ -0,0 +1,200 @@
+"""Read-only compatibility checks for existing Mason and Warden evidence seams.
+
+This checks real owner artifacts against a fresh NetKingdom render. These source
+protocols carry no portable authenticated native evidence. A matching request or
+journal can NEVER establish materialized/live/implemented or authorize dispatch.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+import re
+import sqlite3
+
+import yaml
+
+LIMIT = 262144
+DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
+ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,191}\Z")
+
+
+class EvidenceRefused(ValueError):
+ pass
+
+
+def sha(source):
+ return "sha256:" + hashlib.sha256(source).hexdigest()
+
+
+def read(path):
+ path = Path(path)
+ if path.is_symlink() or not path.is_file() or path.stat().st_size > LIMIT:
+ raise EvidenceRefused("unsafe or oversized metadata source")
+ return path.read_bytes()
+
+
+def _unique(pairs):
+ result = {}
+ for key, value in pairs:
+ if key in result:
+ raise EvidenceRefused("duplicate metadata field")
+ result[key] = value
+ return result
+
+
+def decode(source):
+ if type(source) is not bytes or len(source) > LIMIT:
+ raise EvidenceRefused("oversized metadata input")
+ try:
+ return json.loads(source, object_pairs_hook=_unique)
+ except (ValueError, UnicodeError):
+ raise EvidenceRefused("invalid metadata JSON") from None
+
+
+def check_structure(request, case_source, type_source, rendered):
+ """Consume ops-mason.case-structure-request.v1, not a new native flag."""
+ fields = {"schema", "case", "case_digest", "type_ref", "type_digest",
+ "render_digest", "plan_digest", "bundle_digest", "structural_digest",
+ "reviewed_render_digest", "run_source_changed", "objects"}
+ if type(request) is not dict or set(request) != fields:
+ raise EvidenceRefused("unknown Mason structural request fields")
+ if request["schema"] != "ops-mason.case-structure-request.v1":
+ raise EvidenceRefused("unknown Mason structural request protocol")
+ for field in fields & {"case_digest", "type_digest", "render_digest", "plan_digest",
+ "bundle_digest", "structural_digest", "reviewed_render_digest"}:
+ if type(request[field]) is not str or not DIGEST.fullmatch(request[field]):
+ raise EvidenceRefused("malformed structural source pin")
+ if (request["case"], request["case_digest"], request["type_ref"],
+ request["type_digest"], request["render_digest"]) != (
+ rendered["case"], sha(case_source), rendered["type"], sha(type_source), rendered["digest"]):
+ raise EvidenceRefused("Mason request has stale case/type/render pins")
+ if type(request["run_source_changed"]) is not bool or request["run_source_changed"] != (
+ request["reviewed_render_digest"] != rendered["digest"]):
+ raise EvidenceRefused("Mason run drift flag disagrees with pins")
+ owners = {"openbao_policy": ("railiance-platform", "native-structure"),
+ "openbao_role": ("railiance-platform", "native-structure"),
+ "kv_path": ("railiance-platform", "layout-only"),
+ "catalog_entry": ("ops-warden", "pointer-source")}
+ expected = []
+ for item in rendered["materialization"]:
+ if item["kind"] not in owners:
+ raise EvidenceRefused("unsupported structural owner kind")
+ owner, proof = owners[item["kind"]]
+ expected.append({"id": item["id"], "kind": item["kind"], "target": item["template"],
+ "expected_digest": sha(json.dumps(item, sort_keys=True, separators=(",", ":")).encode()),
+ "evidence_owner": owner, "proof": proof})
+ if request["objects"] != expected:
+ raise EvidenceRefused("Mason structural inventory differs from exact render")
+ case = yaml.safe_load(case_source)
+ acc = case["spec"].get("acceptance") or {}
+ acceptance = ({"record": acc["record"], "at": acc.get("at"),
+ "by": [item.get("role") for item in acc.get("by", [])]} if acc.get("record") else None)
+ structural = {"case": rendered["case"], "type": rendered["type"],
+ "acceptance": acceptance, "materialization": rendered["materialization"]}
+ if request["structural_digest"] != sha(json.dumps(structural, sort_keys=True, separators=(",", ":")).encode()):
+ raise EvidenceRefused("Mason structural projection pin differs from current source")
+ # A request has neither Observation provenance nor StatusBinding authority.
+ return {"request_digest": sha(json.dumps(request, sort_keys=True, separators=(",", ":")).encode()),
+ "objects": len(expected), "run_source_changed": request["run_source_changed"],
+ "native_materialization_verified": False}
+
+
+def read_journal(path):
+ """Read bounded Warden rows through SQLite read-only mode; never create a DB."""
+ path = Path(path)
+ if path.is_symlink() or not path.is_file() or path.stat().st_size > 16 * 1024 * 1024:
+ raise EvidenceRefused("unsafe or oversized Warden journal")
+ try:
+ connection = sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True)
+ try:
+ connection.execute("PRAGMA query_only=ON")
+ connection.execute("PRAGMA trusted_schema=OFF")
+ rows = connection.execute(
+ "SELECT seq,binding,payload FROM receipts ORDER BY seq LIMIT 1025").fetchall()
+ finally:
+ connection.close()
+ if len(rows) > 1024:
+ raise EvidenceRefused("Warden journal inventory exceeds limit")
+ latest = {}
+ for seq, binding, payload in rows:
+ if type(seq) is not int or type(binding) is not str or not ID.fullmatch(binding):
+ raise EvidenceRefused("invalid Warden journal identity")
+ if type(payload) is not str:
+ raise EvidenceRefused("invalid Warden journal payload")
+ latest[binding] = decode(payload.encode())
+ return tuple(latest.values())
+ except sqlite3.Error:
+ raise EvidenceRefused("invalid Warden journal database") from None
+
+
+def check_steps(rows, case_source, type_source, rendered):
+ """Validate latest durable Warden states; they remain local evidence only."""
+ expected = {item["id"]: item for item in rendered["follow_through"]}
+ fields = {"step_id", "owner", "pins", "allowed_created", "retry_safe", "receipts",
+ "journal_schema", "evidence_digest", "session_revoked", "session_revocation_digest",
+ "grant_revocation_digest", "attempt_id", "operation"}
+ type_spec = yaml.safe_load(type_source)["spec"]
+ descriptors = type_spec.get("execution_descriptors", {})
+ seen, result = set(), []
+ for row in rows:
+ if type(row) is not dict or set(row) != fields or row["journal_schema"] != "warden.case.step-journal.v1":
+ raise EvidenceRefused("unknown Warden journal receipt fields")
+ step = row["step_id"]
+ if type(step) is not str or step not in expected or step in seen or row["owner"] != expected[step]["owner"]:
+ raise EvidenceRefused("unknown, duplicate or wrong-owner Warden step")
+ seen.add(step)
+ pins = row["pins"]
+ if type(pins) is not dict or set(pins) != {"case", "case_type", "adapter", "catalog", "acceptance", "run_sheet"}:
+ raise EvidenceRefused("unknown Warden source pins")
+ if any(type(value) is not str or not DIGEST.fullmatch(value) for value in pins.values()):
+ raise EvidenceRefused("malformed Warden source pin")
+ if (pins["case"], pins["case_type"]) != (sha(case_source), sha(type_source)):
+ raise EvidenceRefused("Warden receipt has stale case/type pins")
+ if descriptors.get(step) != pins["adapter"]:
+ raise EvidenceRefused("Warden receipt has no exact type-pinned machine descriptor")
+ history = row["receipts"]
+ if type(history) is not list or not 1 <= len(history) <= 128:
+ raise EvidenceRefused("invalid Warden receipt history")
+ outcomes = {"running", "verified-success", "verified-no-change", "failed-before-change",
+ "failed-after-change", "uncertain", "rollback-running", "verified-rolled-back", "rollback-uncertain"}
+ allowed = row["allowed_created"]
+ if type(allowed) is not list or len(allowed) > 32 or len(set(allowed)) != len(allowed) or any(
+ type(value) is not str or not ID.fullmatch(value) for value in allowed):
+ raise EvidenceRefused("invalid Warden created-object scope")
+ if type(row["retry_safe"]) is not bool:
+ raise EvidenceRefused("invalid Warden retry policy")
+ for number, receipt in enumerate(history, 1):
+ if type(receipt) is not dict or set(receipt) != {"attempt", "outcome", "created"} or (
+ type(receipt["attempt"]) is not int or receipt["attempt"] != number or
+ receipt["outcome"] not in outcomes or type(receipt["created"]) is not list or
+ any(type(v) is not str or v not in allowed for v in receipt["created"]) or
+ len(set(receipt["created"])) != len(receipt["created"])):
+ raise EvidenceRefused("invalid Warden receipt state")
+ last = history[-1]["outcome"]
+ if last in {"verified-success", "verified-no-change", "verified-rolled-back"}:
+ proofs = [row["evidence_digest"], row["session_revocation_digest"]]
+ if expected[step]["value_handling"] == "generated_in_custody":
+ proofs.append(row["grant_revocation_digest"])
+ if row["session_revoked"] is not True or any(
+ type(value) is not str or not DIGEST.fullmatch(value) for value in proofs):
+ raise EvidenceRefused("Warden completed state lacks revocation fingerprints")
+ if type(row["attempt_id"]) is not str or not ID.fullmatch(row["attempt_id"]) or row["operation"] not in {"run", "rollback", "reconcile"}:
+ raise EvidenceRefused("Warden completed state lacks exact attempt identity")
+ result.append({"step": step, "outcome": last, "native_authenticated": False})
+ return result, sorted(set(expected) - seen)
+
+
+def audit(case_source, type_source, rendered, *, structure=None, journal_rows=()):
+ structural = check_structure(structure, case_source, type_source, rendered) if structure is not None else None
+ steps, missing = check_steps(journal_rows, case_source, type_source, rendered)
+ type_spec = yaml.safe_load(type_source)["spec"]
+ return {"schema": "netkingdom.case-evidence-compatibility.v1",
+ "case_digest": sha(case_source), "type_digest": sha(type_source), "render_digest": rendered["digest"],
+ "structure": structural, "steps": steps, "missing_follow_through": missing,
+ "required_verification": [item["id"] for item in rendered["verification"]],
+ "missing_machine_descriptors": [item["id"] for item in rendered["follow_through"]
+ if item["id"] not in type_spec.get("execution_descriptors", {})],
+ "unresolved_native_gates": ["authenticated-owner-evidence-admission", "accepted-to-materialized-status-binding",
+ "human-sign-in", "consumer-group-denial", "parent-session-and-broker-grant-revocation", "created-only-rollback"],
+ "native_complete": False, "execution": {"permitted": False}}
diff --git a/tools/security-case-types/security_case_validator.py b/tools/security-case-types/security_case_validator.py
index 03e00d3..1dc63d6 100644
--- a/tools/security-case-types/security_case_validator.py
+++ b/tools/security-case-types/security_case_validator.py
@@ -630,6 +630,15 @@ def main(argv: list[str] | None = None) -> int:
rd.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH",
help="select an exact version root; no search or fallback")
rd.add_argument("--allow-unimplemented", action="store_true")
+ ev = sub.add_parser("check-evidence", help="check existing Mason/Warden source artifacts; never native acceptance")
+ ev.add_argument("case")
+ ev.add_argument("declarations", nargs="*")
+ ev.add_argument("--repos-root", default=str(TOOL_DIR.parents[2]))
+ ev.add_argument("--types", required=True)
+ ev.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH")
+ ev.add_argument("--allow-unimplemented", action="store_true")
+ ev.add_argument("--structure-request", help="ops-mason structure-request JSON")
+ ev.add_argument("--warden-journal", help="existing Warden journal.sqlite; opened read-only")
args = ap.parse_args(argv)
if args.cmd == "validate-type":
@@ -644,7 +653,7 @@ def main(argv: list[str] | None = None) -> int:
selection_issues, version_roots = parse_version_roots(args.version_root)
if has_errors(selection_issues):
return _report(selection_issues)
- issues, type_data, _ = load_type(Path(args.types), (case.get("spec") or {}).get("type", ""),
+ issues, type_data, selected_type_dir = load_type(Path(args.types), (case.get("spec") or {}).get("type", ""),
version_roots=version_roots)
if type_data is None:
return _report(issues)
@@ -663,6 +672,23 @@ def main(argv: list[str] | None = None) -> int:
issues = load_issues + issues
if rendered is None:
return _report(issues)
+ if args.cmd == "check-evidence":
+ import security_case_evidence as evidence
+ try:
+ source = evidence.read(args.case)
+ type_source = evidence.read(selected_type_dir / "type.yaml")
+ # Refuse source changes between ordinary validation/render and audit.
+ if yaml.safe_load(source) != case or yaml.safe_load(type_source) != type_data:
+ raise evidence.EvidenceRefused("case/type source changed during validation")
+ structure = evidence.decode(evidence.read(args.structure_request)) if args.structure_request else None
+ rows = evidence.read_journal(args.warden_journal) if args.warden_journal else ()
+ report = evidence.audit(source, type_source, rendered, structure=structure, journal_rows=rows)
+ except (evidence.EvidenceRefused, OSError, ValueError, TypeError, KeyError):
+ print("ERROR evidence: incompatible or unsafe owner metadata (diagnostics withheld)", file=sys.stderr)
+ return 1
+ json.dump(report, sys.stdout, indent=2, sort_keys=True)
+ print()
+ return 0
json.dump(rendered, sys.stdout, indent=2, sort_keys=True)
print()
return _report(issues)
diff --git a/tools/security-case-types/tests/test_case_evidence.py b/tools/security-case-types/tests/test_case_evidence.py
new file mode 100644
index 0000000..135ff60
--- /dev/null
+++ b/tools/security-case-types/tests/test_case_evidence.py
@@ -0,0 +1,200 @@
+"""Real owner source API compatibility, with synthetic metadata only; no native evidence."""
+import json
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+
+import pytest
+import yaml
+
+TOOL = Path(__file__).resolve().parents[1]
+REPO = TOOL.parents[1]
+OWNERS = Path("/home/worsch")
+sys.path.insert(0, str(TOOL))
+import security_case_evidence as e
+import security_case_validator as v
+
+
+@pytest.fixture
+def source():
+ case_path = REPO / "security-cases/forgejo-netkingdom-oidc.yaml"
+ raw = case_path.read_bytes()
+ case = yaml.safe_load(raw)
+ issues, doc, folder = v.load_type(REPO / "security-case-types", case["spec"]["type"])
+ paths, issues = v.discover_declarations(doc, OWNERS)
+ declarations, issues = v.composer.load_declarations(paths)
+ issues, rendered = v.render_case(case, doc, declarations, require_implemented=False)
+ assert rendered is not None and not v.has_errors(issues)
+ return raw, (folder / "type.yaml").read_bytes(), rendered
+
+
+@pytest.fixture
+def mason(tmp_path, source):
+ sys.path.insert(0, str(OWNERS / "ops-mason/src"))
+ from ops_mason import case_structure
+ root = OWNERS / "ops-mason"
+ bundle = tmp_path / "bundle"
+ shutil.copytree(root / "builds/case-forgejo-netkingdom-oidc", bundle)
+ # Existing Mason source tests identify this historic bundle's wrong consumer.
+ rel = "credential-change-requests/CCR-DRAFT-FORGEJO-NETKINGDOM-OIDC-FORGEJO.yaml"
+ ccr = yaml.safe_load((bundle / rel).read_bytes())
+ ccr["target"]["workload"] = "forgejo"
+ (bundle / rel).write_text(yaml.safe_dump(ccr, sort_keys=False, width=110))
+ manifest = json.loads((bundle / "manifest.json").read_bytes())
+ manifest["files"][rel] = e.sha((bundle / rel).read_bytes()).split(":")[1]
+ (bundle / "manifest.json").write_text(json.dumps(manifest))
+ request = case_structure.prepare(REPO / "security-cases/forgejo-netkingdom-oidc.yaml",
+ root / "plans/case-forgejo-netkingdom-oidc.md", bundle,
+ nk_root=OWNERS / "net-kingdom", allow_unimplemented=True)
+ return json.loads(json.dumps(request.as_dict()))
+
+
+def test_real_mason_request_recomputes_all_source_and_projection_pins(source, mason):
+ result = e.audit(*source, structure=mason)
+ assert result["structure"]["objects"] == 5
+ assert result["native_complete"] is False
+ assert result["structure"]["native_materialization_verified"] is False
+ assert "mint-secret" in result["missing_follow_through"]
+ assert "register-client" in result["missing_machine_descriptors"]
+ assert "consumer-enforces-groups" in result["required_verification"]
+
+
+@pytest.mark.parametrize("field", ["case_digest", "type_digest", "render_digest", "structural_digest"])
+def test_changed_source_or_projection_refused(source, mason, field):
+ mason[field] = "sha256:" + "0" * 64
+ with pytest.raises(e.EvidenceRefused):
+ e.audit(*source, structure=mason)
+
+
+def test_rehashed_structural_inventory_cannot_add_role_or_mint(source, mason):
+ mason["objects"][0]["target"] += "-sibling"
+ with pytest.raises(e.EvidenceRefused):
+ e.audit(*source, structure=mason)
+
+
+def test_structure_unknown_native_success_flag_is_refused(source, mason):
+ mason["native_complete"] = True
+ with pytest.raises(e.EvidenceRefused):
+ e.audit(*source, structure=mason)
+
+
+@pytest.fixture
+def warden(tmp_path, source):
+ sys.path.insert(0, str(OWNERS / "ops-warden/src"))
+ from warden.case_run import Invocation, Journal, OwnerResult
+ from warden.case_receipt_state import Outcome, Pins, Receipt, StepState
+ raw, type_raw, rendered = source
+ # Test-only migration: current acknowledged real type has no descriptors.
+ doc = yaml.safe_load(type_raw)
+ descriptor_digest = e.sha(b"fixture-descriptor")
+ doc["spec"]["execution_descriptors"] = {"mint-secret": descriptor_digest}
+ type_raw = yaml.safe_dump(doc).encode()
+ pins = Pins(e.sha(raw), e.sha(type_raw), descriptor_digest, *([e.sha(b"fixture")] * 3))
+ state = StepState("mint-secret", "railiance-platform", pins, (), False,
+ (Receipt(1, Outcome.RUNNING), Receipt(2, Outcome.SUCCESS)))
+ invocation = Invocation("mint-secret", "railiance-platform", "forgejo", "human", "fixture-human",
+ "keycape-client-secret-generate", pins, "attempt-fixture", "run")
+ receipt = OwnerResult(Outcome.SUCCESS, "fixture-evidence", (), True,
+ "fixture-parent-revocation", "fixture-grant-revocation", "attempt-fixture", "run")
+ journal = Journal(tmp_path / "private-journal")
+ journal.save("mint-secret:forgejo", state, receipt, invocation)
+ return (raw, type_raw, rendered), journal
+
+
+def test_actual_warden_journal_save_is_read_without_mutation_and_not_native_proof(warden):
+ source, journal = warden
+ path = journal.directory / "journal.sqlite"
+ before = path.read_bytes()
+ rows = e.read_journal(path)
+ result = e.audit(*source, journal_rows=rows)
+ assert path.read_bytes() == before
+ assert result["steps"] == [{"step": "mint-secret", "outcome": "verified-success", "native_authenticated": False}]
+ assert result["native_complete"] is False
+
+
+@pytest.mark.parametrize("field", ["session_revocation_digest", "grant_revocation_digest", "evidence_digest"])
+def test_missing_revocation_or_owner_fingerprint_refused(warden, field):
+ source, journal = warden
+ row = e.read_journal(journal.directory / "journal.sqlite")[0]
+ row[field] = None
+ with pytest.raises(e.EvidenceRefused):
+ e.audit(*source, journal_rows=(row,))
+
+
+def test_unresolved_attempt_not_promoted_to_success(warden):
+ source, journal = warden
+ row = e.read_journal(journal.directory / "journal.sqlite")[0]
+ row["receipts"][-1]["outcome"] = "uncertain"
+ assert e.audit(*source, journal_rows=(row,))["steps"][0]["outcome"] == "uncertain"
+
+
+def test_no_descriptor_in_actual_acknowledged_type_refuses_journal_success(source, warden):
+ _, journal = warden
+ row = e.read_journal(journal.directory / "journal.sqlite")[0]
+ row["pins"]["case_type"] = e.sha(source[1])
+ with pytest.raises(e.EvidenceRefused, match="machine descriptor"):
+ e.audit(*source, journal_rows=(row,))
+
+
+def test_missing_or_symlink_journal_never_created(tmp_path):
+ path = tmp_path / "missing.sqlite"
+ with pytest.raises(e.EvidenceRefused):
+ e.read_journal(path)
+ assert not path.exists()
+ target = tmp_path / "target.sqlite"
+ target.write_bytes(b"not sqlite")
+ path.symlink_to(target)
+ with pytest.raises(e.EvidenceRefused):
+ e.read_journal(path)
+
+
+def test_duplicate_and_oversized_json_refused():
+ for source in (b'{"schema":1,"schema":2}', b"x" * (e.LIMIT + 1)):
+ with pytest.raises(e.EvidenceRefused):
+ e.decode(source)
+
+
+@pytest.mark.parametrize("version", ["1.1", "1.2"])
+def test_existing_cli_exact_source_selection_and_normal_admission_refusal(version):
+ case = REPO / ("security-cases/forgejo-netkingdom-oidc.yaml" if version == "1.1" else
+ "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/forgejo.yaml")
+ argv = [sys.executable, str(TOOL / "security_case_validator.py"), "check-evidence", str(case),
+ "--types", str(REPO / "security-case-types"), "--repos-root", str(OWNERS),
+ "--version-root", "1.2=" + str(REPO / "security-case-type-revisions/1.2")]
+ refused = subprocess.run(argv, capture_output=True, text=True)
+ assert refused.returncode == 1 and "only implemented types" in refused.stderr
+ result = subprocess.run(argv + ["--allow-unimplemented"], capture_output=True, text=True)
+ assert result.returncode == 0, result.stderr
+ report = json.loads(result.stdout)
+ assert report["native_complete"] is False and report["execution"]["permitted"] is False
+ assert ("deliver-consumer-secret" in report["missing_follow_through"]) == (version == "1.2")
+
+
+def test_cli_consumes_actual_mason_request_and_withholds_rejected_payload(tmp_path, mason):
+ request = tmp_path / "request.json"
+ request.write_text(json.dumps(mason))
+ args = [sys.executable, str(TOOL / "security_case_validator.py"), "check-evidence",
+ str(REPO / "security-cases/forgejo-netkingdom-oidc.yaml"), "--types", str(REPO / "security-case-types"),
+ "--repos-root", str(OWNERS), "--allow-unimplemented", "--structure-request", str(request)]
+ result = subprocess.run(args, capture_output=True, text=True)
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout)["structure"]["objects"] == 5
+ mason["unexpected"] = "sensitive-fixture-diagnostic"
+ request.write_text(json.dumps(mason))
+ result = subprocess.run(args, capture_output=True, text=True)
+ assert result.returncode == 1 and "sensitive-fixture-diagnostic" not in result.stderr + result.stdout
+
+
+def test_actual_journal_latest_uncertainty_does_not_replay_prior_success(warden):
+ source, journal = warden
+ from warden.case_receipt_state import Pins, Receipt, StepState, Outcome
+ row = e.read_journal(journal.directory / "journal.sqlite")[0]
+ history = tuple(Receipt(item["attempt"], Outcome(item["outcome"]), tuple(item["created"]))
+ for item in row["receipts"])
+ state = StepState(row["step_id"], row["owner"], Pins(**row["pins"]), (), False,
+ history + (Receipt(3, Outcome.UNCERTAIN),))
+ journal.save("mint-secret:forgejo", state)
+ rows = e.read_journal(journal.directory / "journal.sqlite")
+ assert len(rows) == 1
+ assert e.audit(*source, journal_rows=rows)["steps"][0]["outcome"] == "uncertain"
diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md
index d50a1cb..074efd1 100644
--- a/workplans/NK-WP-0048-security-case-types.md
+++ b/workplans/NK-WP-0048-security-case-types.md
@@ -559,3 +559,28 @@ docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-vali
FormalNetownerreview/reference-source integration and all existing native adapter,
provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no
new residual or firstcase/typeimplementation/native grant is inferred.
+
+
+2026-10-16 — isolated source preparation for T06 (canonical integration pending):
+Added `check-evidence` to the existing validator CLI. It reuses exact version
+selection and ordinary type/case/render admission, checks actual Mason structural
+request source/inventory/projection pins and reads actual Warden journal rows
+read-only. Successful rows require exact type-declared descriptors and attempt/
+revocation fingerprints. Existing acknowledged types without machine descriptors
+remain plan-only. The compatibility result never claims authenticated native
+completion: Request and journal fingerprints are not portable native evidence.
+Owner authentication/status binding and actual native gates remain unresolved;
+T06/T07 statuses and both type/case lifecycles remain unchanged. No dispatch,
+credential access or authority expansion is introduced. See tool README for the
+existing owner protocol boundaries and source integration tests.
+
+Concrete next Warden owner integration: current `Executor.admit` refuses every
+case parameter whose sensitivity is not public/operational, so browser
+`redirect_uris`, `allowed_scopes`, `client_type`, `mfa_required` (and 1.2
+`consumer_namespace`) fail after otherwise prepared admission. The sensitive
+classification is a security-change review level, not secret content. The
+Warden owner must validate through NetKingdom's existing exact-version case/type
+validator, then project only descriptor-declared safe target metadata; do not
+remove the credential/value refusal or copy complete parameters into journals.
+Actual owner descriptor acknowledgment and independent evidence admission remain
+prerequisites, with normal admission still refusing unimplemented types.