227 lines
12 KiB
Diff
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),
|