fix(case): validate original intent and bind its running receipt
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
tegwick 2026-10-17 10:33:43 +02:00
parent a45cf475bc
commit 117fec9241
4 changed files with 421 additions and 17 deletions

View file

@ -125,3 +125,30 @@ is now the tested producer revision. Its evidence module is imported lazily only
by `check-evidence`, outside this admission path; Warden nevertheless pins and
copies it with the complete reviewed producer set. Missing or modified bytes in
any of the four modules refuse admission. The test covers every dependency.
## Original-intent consistency boundary — 2026-10-17
`OriginalIntent` and the optional additive
`reconcile_original(ReconcileContext(original, attempt))` callback retain the
original run separately from a fresh reconciliation attempt. Existing one-argument
reconcile adapters remain source-compatible. Intent and pre-dispatch receipt are
committed in one SQLite transaction; lookup now requires their exact attempt,
actor, step/owner/target, source and pin correspondence. Missing legacy context,
an orphan receipt, changed context or an invalid bounded field contract refuses;
there is no backfill or new runtime holder.
Original intent serialization accepts only the fixed reviewed Invocation/Pins
field contract and bounded operational identifiers. Duplicate, oversized and
malformed original records refuse. Current Executor.admit leaves tenant and realm
empty: those reserved fields are not admitted tenant/realm provenance, and future
nonempty coordinates need an authoritative owner contract. Raw owner payloads,
credentials, parameters and caller output are not part of this context contract.
Local dispatch correspondence is not independent owner proof. A real owner must
still authenticate the exact original created-only result and original revocation,
current native revision and the fresh attempt's session/grant evidence before
recovery can count. No actual original holder/verifier or native adapter is added.
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.

View file

@ -180,16 +180,76 @@ class OriginalIntent:
"""Durable record of the original mutating dispatch, written before dispatch.
Binds the original Invocation (attempt, step, owner, target, actor, principal,
lane), all pins, the admitted adapter source and the tenant/realm provenance
the caller was admitted with. It is evidence of what Warden dispatched, not
owner proof of creation: owners still authenticate created-only provenance.
lane), all pins and the admitted adapter source. Current admit leaves tenant
and realm empty; their reserved fields are not admitted provenance. This is
local dispatch correspondence, not owner proof of creation. Owners still
authenticate original created-only provenance independently.
"""
binding: str
invocation: Invocation
source: str
def validate(self) -> None:
"""Fixed bounded metadata only; tenant/realm are not authority claims.
Current admit leaves tenant/realm empty. Nonempty future coordinates need
separate owner provenance; serializability alone must never supply it.
"""
if (
type(self.invocation) is not Invocation
or type(self.invocation.pins) is not Pins
):
raise Refused("exact original invocation required")
i = self.invocation
fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id)
if (
not all(
type(value) is str and IDENTIFIER.fullmatch(value) for value in fields
)
or type(i.actor) is not str
or i.actor not in ("human", "agent")
or type(i.operation) is not str
or i.operation not in ("run", "rollback")
or not all(
type(value) is str and (not value or IDENTIFIER.fullmatch(value))
for value in (i.tenant, i.realm)
)
or type(self.source) is not str
or not DIGEST.fullmatch(self.source)
or type(self.binding) is not str
or self.binding != f"{i.step}:{i.target}"
):
raise Refused("invalid original intent metadata")
if set(Invocation.__dataclass_fields__) != {
"step",
"owner",
"target",
"actor",
"principal",
"lane",
"pins",
"attempt_id",
"operation",
"tenant",
"realm",
}:
raise Refused("original invocation field contract changed")
if not all(
type(v) is str and DIGEST.fullmatch(v)
for v in (
i.pins.case,
i.pins.case_type,
i.pins.adapter,
i.pins.catalog,
i.pins.acceptance,
i.pins.run_sheet,
)
):
raise Refused("invalid original intent pins")
def payload(self) -> str:
self.validate()
return json.dumps(
{"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source}
| asdict(self.invocation),
@ -199,13 +259,44 @@ class OriginalIntent:
@classmethod
def decode(cls, raw: str) -> OriginalIntent:
obj = json.loads(raw)
fields = {f for f in Invocation.__dataclass_fields__}
if set(obj) != fields | {"schema", "binding", "source"} or obj["schema"] != INTENT_SCHEMA:
raise Refused("unknown original intent schema")
inv = {k: obj[k] for k in fields}
inv["pins"] = Pins(**inv["pins"])
return cls(obj["binding"], Invocation(**inv), obj["source"])
def unique(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Refused("duplicate original intent field")
result[key] = value
return result
try:
if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
raise Refused("original intent input exceeds bound")
obj = json.loads(raw, object_pairs_hook=unique)
fields = {
"step",
"owner",
"target",
"actor",
"principal",
"lane",
"pins",
"attempt_id",
"operation",
"tenant",
"realm",
}
if (
type(obj) is not dict
or set(obj) != fields | {"schema", "binding", "source"}
or obj["schema"] != INTENT_SCHEMA
):
raise Refused("unknown original intent schema")
inv = {k: obj[k] for k in fields}
inv["pins"] = Pins(**inv["pins"])
result = cls(obj["binding"], Invocation(**inv), obj["source"])
result.validate()
return result
except (TypeError, ValueError, KeyError):
raise Refused("invalid original intent record") from None
@dataclass(frozen=True, slots=True)
@ -256,28 +347,76 @@ class Journal:
"CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)"
)
def original_intent(self, binding: str, current: Invocation, source: str) -> OriginalIntent:
def original_intent(
self, binding: str, current: Invocation, source: str
) -> OriginalIntent:
"""Latest original run intent for binding, refused unless it matches exactly.
Missing or mismatched records refuse; the step stays uncertain.
"""
row = self.db.execute(
"SELECT payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
"SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1",
(binding,),
).fetchone()
if row is None:
raise Refused("original intent record missing; step stays uncertain")
intent = OriginalIntent.decode(row[0])
intent = OriginalIntent.decode(row[1])
o = intent.invocation
if row[0] != o.attempt_id:
raise Refused("original intent attempt identity differs")
if intent.binding != binding or o.operation != "run":
raise Refused("original intent binding mismatch")
if o.pins != current.pins:
raise Refused("original intent pins mismatch")
if intent.source != source:
raise Refused("original intent source mismatch")
for name in ("step", "owner", "target", "principal", "lane", "tenant", "realm"):
for name in (
"step",
"owner",
"target",
"actor",
"principal",
"lane",
"tenant",
"realm",
):
if getattr(o, name) != getattr(current, name):
raise Refused(f"original intent {name} mismatch")
# Match the persisted original running receipt. An orphan context or a
# payload from another attempt cannot become the current lookup key.
receipts = self.db.execute(
"SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?",
(binding, MAX_HISTORY + 1),
).fetchall()
if len(receipts) > MAX_HISTORY:
raise Refused("original intent receipt history exceeds bound")
matched = False
try:
for (raw,) in receipts:
if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES:
raise Refused("original receipt input exceeds bound")
payload = json.loads(raw)
if (
payload.get("attempt_id") != o.attempt_id
or payload.get("operation") != "run"
):
continue
last = payload.get("receipts", [])[-1]
if last.get("outcome") != "running":
continue
if (
payload.get("journal_schema") != "warden.case.step-journal.v1"
or payload.get("step_id") != o.step
or payload.get("owner") != o.owner
or payload.get("pins") != asdict(o.pins)
):
raise Refused("original running receipt binding differs")
matched = True
break
except (TypeError, ValueError, KeyError, IndexError, AttributeError):
raise Refused("invalid original running receipt") from None
if not matched:
raise Refused("original running receipt missing; no backfill")
return intent
@contextmanager
@ -355,6 +494,23 @@ class Journal:
invocation: Invocation | None = None,
intent: OriginalIntent | None = None,
):
if intent is not None:
if type(intent) is not OriginalIntent:
raise Refused("exact original intent required")
intent.validate()
expected = (
Outcome.RUNNING
if intent.invocation.operation == "run"
else Outcome.ROLLBACK_RUNNING
)
if (
intent.binding != binding
or intent.invocation != invocation
or intent.invocation.pins != state.pins
or not state.last
or state.last.outcome is not expected
):
raise Refused("original intent and running receipt differ")
self.db.execute("BEGIN IMMEDIATE")
try:
if intent is not None:

View file

@ -0,0 +1,198 @@
"""Original-intent consistency checks with generated metadata only.
Reuse the existing synthetic owner rig. These checks do not provide a native
holder or independently authenticate original creation from local SQL/JSON.
"""
from dataclasses import asdict, replace
import json
from pathlib import Path
import sqlite3
import subprocess
import sys
import pytest
from tests.test_case_run import rig as rig
from warden.case_receipt_state import Outcome, Refused
from warden.case_run import OriginalIntent
def interrupted(rig):
executor, args, calls, sources, kwargs = rig
old = executor.adapters[args[0].adapter]
seen = []
def lost(inv):
calls.append(inv)
raise TimeoutError("generated lost response")
def original(ctx):
seen.append(ctx)
return old.reconcile(ctx.attempt)
executor.adapters[old.reference] = replace(
old, invoke=lost, reconcile=None, reconcile_original=original
)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
return executor, args, calls, old, seen
def alter_payload(executor, **changes):
row = executor.journal.db.execute(
"SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1"
).fetchone()
obj = json.loads(row[1])
obj.update(changes)
executor.journal.db.execute(
"UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0])
)
def test_original_actor_mismatch_is_refused(rig):
executor, args, calls, old, _ = interrupted(rig)
alter_payload(executor, actor="agent")
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig):
executor, args, calls, old, _ = interrupted(rig)
actual = calls[0].attempt_id
alter_payload(executor, attempt_id="attempt-unrelated")
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
assert (
executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0]
== actual
)
def test_orphan_intent_without_original_running_receipt_is_refused(rig):
executor, args, calls, old, _ = interrupted(rig)
executor.journal.db.execute(
"DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)"
)
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig):
_, args, _, _, _ = rig
inv = replace(
args[1],
target={"unexpected": "synthetic-owner-payload"},
operation="run",
attempt_id="attempt-fixture",
)
record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64)
with pytest.raises(Refused):
OriginalIntent.decode(record.payload())
# This is a direct serialization API gap, not an observed normal-admit leak.
# Owner trust is the existing synthetic fixture; this proves call shape only.
@pytest.mark.parametrize(
"field,value",
[
("principal", "another-person"),
("tenant", "another-tenant"),
("realm", "another-realm"),
],
)
def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(
rig, field, value
):
executor, args, _, old, _ = interrupted(rig)
alter_payload(executor, **{field: value})
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
executor, args, calls, _, _ = rig
executor.journal.db.execute(
"CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END"
)
with pytest.raises(sqlite3.IntegrityError):
executor.run(*args)
assert not calls
assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == (
0,
)
assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == (
0,
)
def test_existing_missing_original_refuses_before_callback(rig):
executor, args, _, _, seen = interrupted(rig)
executor.journal.db.execute("DELETE FROM intents")
with pytest.raises(Refused):
executor.reconcile(*args)
assert not seen
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
def test_existing_current_parent_revocation_remains_mandatory(rig):
executor, args, _, _, _ = interrupted(rig)
old = executor.evidence_verifier
def refuse_session(purpose, source, evidence):
return (
False
if purpose.startswith("session-revocation:")
else old(purpose, source, evidence)
)
executor.evidence_verifier = refuse_session
with pytest.raises(Refused):
executor.reconcile(*args)
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
def test_existing_intent_survives_actual_distinct_interpreter(rig):
executor, args, calls, old, _ = interrupted(rig)
code = """import json,sys
from pathlib import Path
from warden.case_run import Journal,Invocation
from warden.case_receipt_state import Pins
obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
with j.locked():
i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
"""
result = subprocess.run(
[sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source],
input=json.dumps(asdict(args[1])),
text=True,
capture_output=True,
timeout=15,
env={
"PATH": "/usr/bin:/bin",
"PYTHONPATH": str(Path(__file__).parents[1] / "src"),
"PYTHONDONTWRITEBYTECODE": "1",
},
check=True,
)
assert json.loads(result.stdout) == {
"attempt": calls[0].attempt_id,
"operation": "run",
}

View file

@ -168,14 +168,37 @@ Invocation — attempt, step, owner, target, actor, principal, lane, all six pin
tenant, realm — plus admitted adapter source) in the same SQLite transaction as
the pre-dispatch journal row. `Journal.original_intent(binding, current, source)`
returns it after restart and refuses on a missing record or any
pins/source/step/owner/target/principal/lane/tenant/realm mismatch, before any
pins/source/step/owner/target/actor/principal/lane/tenant/realm mismatch, before any
journal write, so the step stays uncertain. An adapter's
`reconcile_original(ReconcileContext(original, attempt))` receives the original
intent separately from the new reconcile attempt. This is Warden's record of what
it dispatched, not owner created-only provenance; tenant/realm are as supplied at
admission. Owner descriptors/adapters, contained session integration, Forgejo
it dispatched, not owner created-only provenance. Current Executor.admit leaves
tenant/realm empty; the reserved coordinates establish no admitted provenance.
Owner descriptors/adapters, contained session integration, Forgejo
materialization and the native run remain open.
2026-10-17 source consistency follow-through: retained the additive intents table
and reconcile_original callback and fixed four reproduced local defects: original
actor mismatch, payload/SQL attempt-ID mismatch, orphan intent without its
matching original running receipt, and arbitrary nonmetadata values accepted by
the original-intent serializer. Exact bounded field validation, duplicate-free
bounded decode and save-time intent/invocation/running-state equality now preserve
the original receipt linkage before owner lookup. No new context store, native
verifier, trust root, adapter activation or authority migration was introduced.
The actual canonical focused kernel and new consistency suite passes 54 checks;
owner-installed Ruff check/format passes. The existing generated-data/synthetic
owner fixture and a distinct interpreter prove source correspondence/refusal,
not independent native custody or creation. Earlier comparison probes and failed
preparation are retained. Current caller and fresh session/grant revocation checks
remain required; original owner payload/revocation, exact native readback and
created-only provenance still need independent authenticated owner admission.
No JSON/SQL context, unknown tenant/realm, changed run-sheet or legacy missing
intent supplies recovery, replay or undo authority. T03 remains progress and all
native acceptance gates/statuses/dependencies/UUIDs remain unchanged. Evidence:
Custodian original-context-compare-r2 and original-context-owner-followthrough-r3
under docs/changes/netkingdom-target-2026-10-17/; publication/projection is separate.
Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09):
- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate
--client-id <id> --case-ref <case-ref> --case-file <case.yaml>` as the caller's