the-custodian/docs/changes/netkingdom-target-2026-10-18/keycape-current-client-r1/ops-warden-source/owner-source.patch
codex 7c682d9c66
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Reject generated-only commits / generated-commit-guard (push) Successful in 9s
Publish shared current-token client and fresh-session inverse source evidence
2026-10-18 11:43:13 +02:00

227 lines
12 KiB
Diff

diff --git a/docs/security-case-execution-kernel-v1.md b/docs/security-case-execution-kernel-v1.md
--- a/docs/security-case-execution-kernel-v1.md
+++ b/docs/security-case-execution-kernel-v1.md
@@ -152,3 +152,41 @@
NetKingdom's current registration adapter still has no reconciliation callback;
source tests do not authorize its post-restart undo, replay or a changed caller/
session/run-sheet migration. WARDEN-WP-0044-T03 remains a native acceptance gate.
+
+## Current caller versus original session reference — 2026-10-18 source
+
+`Executor.admit` now accepts the optional `original_session_reference` for an
+exact existing original-run continuation. It is metadata, never current
+authentication. The normal `session_evidence` is independently verified as the
+current caller's principal/actor/lane proof, including after restart, and remains
+the evidence rechecked immediately before every journal write/owner callback.
+
+The optional reference preserves the original run-sheet digest rather than
+replacing immutable provenance with the new login. Before admitting it, under
+the existing journal lock, the kernel requires an existing matching step journal
+and `Journal.original_intent` correspondence: the original running receipt,
+attempt, owner/target/principal/actor/lane, all six pins and exact adapter source.
+Missing/orphan/legacy context, a changed reference or caller and a stale current
+session refuse. The reference must differ from the current evidence identifier;
+the actual session owner must additionally establish distinct authenticated
+token/session identity, not just two labels. No new original context is created,
+no completed operation can replay, and uncertainty is not cleared by readmission.
+
+Omitting this option retains the old admission/pin semantics. Supplying a fresh
+session alone still creates a different run sheet and cannot reuse a previous
+journal. No existing descriptor, Invocation, Pins or receipt schema changes.
+The new source seam does not implement changed-source/catalog/caller migration
+or loss of rollback result; those still require the existing owner reconciliation
+and recovery evidence. It is not general session management or another issuer.
+
+A source integration fixture runs the actual producer-owned KeyCape status CLI
+over HTTPS, the actual Warden kernel and a separately compiled generated Go
+consumer. It verifies revoked-parent refusal before writes, distinct inverse
+authentication in another Python process, original journal pin retention,
+no business replay, exact synthetic target restore and independent read-only
+negative cleanup observation. Case/type/catalog/owner attestations and the
+target are synthetic; this is not Forgejo/browser/native acceptance or a real
+approval. KeyCape's proposed online profile remains unadmitted. The private
+unsigned CLI observation is not portable execution authority; actual owner
+custody and original grant ownership are still mandatory before an inactive
+response can establish revocation. No native verifier or adapter is installed.
diff --git a/src/warden/case_run.py b/src/warden/case_run.py
--- a/src/warden/case_run.py
+++ b/src/warden/case_run.py
@@ -663,7 +663,20 @@
materialization_evidence: str,
descriptor_evidence: str,
session_evidence: str,
+ original_session_reference: str | None = None,
) -> tuple[Descriptor, Invocation, StepState]:
+ # Current authentication is distinct from an immutable original receipt
+ # reference. The latter can only resume an exact existing journal intent;
+ # it is never passed to the current-session verifier as authority.
+ receipt_session = session_evidence
+ if original_session_reference is not None:
+ if (
+ type(original_session_reference) is not str
+ or not IDENTIFIER.fullmatch(original_session_reference)
+ or original_session_reference == session_evidence
+ ):
+ raise Refused("distinct original session reference required")
+ receipt_session = original_session_reference
for source in (case_source, type_source, descriptor_source, catalog_source):
bounded_source(source)
descriptor = Descriptor.decode(descriptor_source)
@@ -781,7 +794,7 @@
acceptance_evidence,
materialization_evidence,
descriptor_evidence,
- session_evidence,
+ receipt_session,
],
separators=(",", ":"),
).encode()
@@ -803,6 +816,13 @@
descriptor.allowed_created,
descriptor.retry_safe,
)
+ if original_session_reference is not None:
+ with self.journal.locked():
+ binding = f"{invocation.step}:{invocation.target}"
+ existing = self.journal.load(binding, initial)
+ if not existing.last:
+ raise Refused("original dispatch journal required; no backfill")
+ self.journal.original_intent(binding, invocation, adapter.source)
# Keep the mode and exact authenticated catalog bytes with this admission;
# dispatch must not substitute a later ambient catalog or lose its mode.
self._admitted[invocation] = (
diff --git a/tests/test_case_current_session_reference.py b/tests/test_case_current_session_reference.py
--- /dev/null
+++ b/tests/test_case_current_session_reference.py
@@ -0,0 +1,105 @@
+"""Current caller authentication never replaces historical dispatch provenance."""
+from dataclasses import replace
+
+import pytest
+
+from tests.test_case_run import make_rig
+from warden.case_receipt_state import Outcome, Refused
+from warden.case_run import Executor, Journal
+
+
+def fresh_verifier(executor, *, allow=True):
+ original = executor.evidence_verifier
+ def verify(purpose, source, evidence):
+ if purpose.startswith("caller-session:") and evidence == "fresh-session":
+ return allow
+ return original(purpose, source, evidence)
+ executor.evidence_verifier = verify
+ return verify
+
+
+def rows(executor):
+ return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
+ for table in ("receipts", "intents"))
+
+
+def readmit(executor, sources, kwargs, reference="session"):
+ return executor.admit(*sources, **{**kwargs, "session_evidence": "fresh-session",
+ "original_session_reference": reference})
+
+
+def test_normal_new_session_cannot_rewrite_original_journal_pins(tmp_path):
+ executor, admitted, _, sources, kwargs = make_rig(tmp_path)
+ executor.run(*admitted)
+ fresh_verifier(executor)
+ before = rows(executor)
+ changed = executor.admit(*sources, **{**kwargs,"session_evidence":"fresh-session"})
+ assert changed[1].pins != admitted[1].pins
+ with pytest.raises(Refused,match="journal pins changed"):
+ executor.rollback(*changed)
+ assert rows(executor) == before
+
+
+def test_fresh_process_inverse_retains_original_pins_without_replay(tmp_path):
+ executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
+ assert executor.run(*admitted).last.outcome is Outcome.SUCCESS
+ before = rows(executor)
+ fresh = Executor(Journal(executor.journal.directory), executor.adapters,
+ fresh_verifier(executor))
+ renewed = readmit(fresh, sources, kwargs)
+ assert renewed[1].pins == admitted[1].pins
+ assert rows(fresh) == before
+ with pytest.raises(Refused):
+ fresh.run(*renewed)
+ assert len(calls) == 1
+ assert fresh.rollback(*renewed).last.outcome is Outcome.ROLLED_BACK
+
+
+@pytest.mark.parametrize("kind", ["missing-intent", "orphan-intent", "wrong-principal", "wrong-reference", "same-as-current", "invalid-reference", "inactive-current"])
+def test_original_reference_is_never_authentication_or_new_intent(tmp_path, kind):
+ executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
+ if kind != "missing-intent":
+ executor.run(*admitted)
+ if kind == "orphan-intent":
+ executor.journal.db.execute("DELETE FROM intents")
+ if kind == "wrong-principal":
+ kwargs = {**kwargs,"principal":"another-operator"}
+ fresh_verifier(executor, allow=kind != "inactive-current")
+ before = rows(executor)
+ reference = {"wrong-reference": "unrelated-session", "same-as-current": "fresh-session",
+ "invalid-reference": "not valid"}.get(kind, "session")
+ with pytest.raises(Refused):
+ readmit(executor, sources, kwargs, reference)
+ assert rows(executor) == before
+
+
+def test_unknown_effect_stays_unknown_after_current_readmission(tmp_path):
+ executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
+ old = executor.adapters[admitted[0].adapter]
+ def interrupted(inv):
+ calls.append(inv)
+ raise KeyboardInterrupt
+ executor.adapters[old.reference] = replace(old, invoke=interrupted)
+ with pytest.raises(KeyboardInterrupt):
+ executor.run(*admitted)
+ fresh_verifier(executor)
+ before = rows(executor)
+ renewed = readmit(executor, sources, kwargs)
+ assert rows(executor) == before
+ with pytest.raises(Refused):
+ executor.run(*renewed, retry=True)
+ state = executor.journal.load("mint-secret:fixture-client", renewed[2])
+ assert state.last.outcome is Outcome.UNCERTAIN
+ assert len(calls) == 1
+
+
+def test_current_session_revalidated_after_readmission_before_journal_write(tmp_path):
+ executor, admitted, _, sources, kwargs = make_rig(tmp_path)
+ executor.run(*admitted)
+ fresh_verifier(executor)
+ renewed = readmit(executor, sources, kwargs)
+ executor.evidence_verifier = lambda *_: False
+ before = rows(executor)
+ with pytest.raises(Refused, match="caller session no longer verified"):
+ executor.rollback(*renewed)
+ assert rows(executor) == before
diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md
--- a/workplans/WARDEN-WP-0044-security-case-follow-through.md
+++ b/workplans/WARDEN-WP-0044-security-case-follow-through.md
@@ -151,6 +151,22 @@
### T03 — Run and verify a case
+2026-10-18 current-session continuation: the new optional
+`Executor.admit(original_session_reference=...)` separates immutable original
+run-sheet provenance from fresh independently verified current session evidence.
+It requires the exact existing journal and original running intent under the
+normal lock, refuses missing/orphan/changed context and retains all original
+pins. Current authority is rechecked before every subsequent write/callback;
+neither the original reference nor a fresh login can replay a completed effect
+or clear uncertainty. No descriptor/Invocation/receipt schema or default
+admission behavior changes. Source integration uses the actual KeyCape CLI over
+HTTPS, a fresh Python owner process and actual Warden SQLite, with synthetic
+case/catalog/owner/effect attestations only. Exact source/proof is retained by
+CUST-WP-0080 under `keycape-current-client-r1`. Native owner mappings, current
+operation/effect authority, credential custody, case/type/descriptor refreeze,
+Forgejo inverse semantics and first native acceptance remain open; no lane,
+adapter, runtime or caller scope is activated by this source change.
+
`warden case run <case.yaml>` executes the plan. Owner entry points run through
the existing proxy as the caller; steps that need an attended login use the
existing contained login envelope (silent command, session revoked afterwards),