Keep original case provenance while revalidating a fresh caller session
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
This commit is contained in:
parent
b593c87983
commit
08c098e64a
4 changed files with 180 additions and 1 deletions
|
|
@ -152,3 +152,41 @@ recovery can count. No actual original holder/verifier or native adapter is adde
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -663,7 +663,20 @@ class Executor:
|
|||
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 @@ class Executor:
|
|||
acceptance_evidence,
|
||||
materialization_evidence,
|
||||
descriptor_evidence,
|
||||
session_evidence,
|
||||
receipt_session,
|
||||
],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
|
|
@ -803,6 +816,13 @@ class Executor:
|
|||
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] = (
|
||||
|
|
|
|||
105
tests/test_case_current_session_reference.py
Normal file
105
tests/test_case_current_session_reference.py
Normal file
|
|
@ -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
|
||||
|
|
@ -151,6 +151,22 @@ state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89"
|
|||
|
||||
### 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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue