WARDEN-WP-0044-T03: original-intent lookup/context for owner reconciliation
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Durably record the original run Invocation, pins, adapter source and
tenant/realm provenance before dispatch; expose Journal.original_intent and
Adapter.reconcile_original(ReconcileContext) for NK-WP-0048-T15. Missing or
mismatched intent refuses and leaves the step uncertain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 903097@bnt-lap001
Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
This commit is contained in:
tegwick 2026-10-17 04:32:50 +02:00
parent d97652a3bb
commit a45cf475bc
4 changed files with 191 additions and 7 deletions

View file

@ -2,7 +2,7 @@
# Custodian Brief — ops-warden
**Domain:** infotech
**Last synced:** 2026-10-11 18:37 UTC
**Last synced:** 2026-10-16 00:05 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
@ -14,7 +14,7 @@ Progress: 1/4 done | workplan_id: `c8ee441e-1be1-5219-910c-e79ff23cc9ec`
- ! Tasks `611f0901`
*(wait: Warden's own declaration cannot classify the repair-path target workloads; the owner must declare the continuity actor/target and its z2-continuity resolution (see 2026-09-28 review).)*
- ! Tasks `54ad4864`
*(wait: Owners of agt-codex-interhub-bootstrap, agt-state-hub-bridge and atm-backup-daily must publish authoritative workload identity declarations.)*
*(wait: Owners of agt-codex-interhub-bootstrap (ops-hub) and agt-state-hub-bridge (ops-bridge) must publish authoritative workload identity declarations; atm-backup-daily declared by activity-core and compiled 2026-10-09 (z2-protected, admission unsatisfied: M1 below M2 floor).)*
- ! Tasks `93eaf1f2`
*(wait: Gated on T01 and on layer model v0.8 (or successor) reaching accepted; never convert against a proposed standard.)*
@ -30,6 +30,7 @@ Progress: 2/4 done | workplan_id: `f7e81013-6868-5d99-8528-ba75a442eca9`
**Open tasks:**
- ! T03 — Run and verify a case `4a0fa4a7`
*(wait: Catalog integration follows a proven run.)*
- ► T02 — Plan a case run `bcbf6c91`
### Tamper-resistant credential governance + mass rotation/lockdown (Strand B)
@ -43,7 +44,7 @@ Progress: 2/3 done | workplan_id: `42a097db-1c24-558e-a724-030bb2b4443e`
**Open tasks:**
- ! Prove routing and publication `a8b1b855`
*(wait: Founder HOLD 2026-09-21: no rotation until SECRETS-WP-0006-T06 moves the publish lane off the legacy whynot-design npm path; then rotate and prove a fresh publish.)*
*(wait: Hold kept by D3 until SECRETS-WP-0006-T06 moves publishing off the legacy whynot-design npm path; then rotate and prove a fresh publish.)*
## Inbox Hygiene

View file

@ -168,6 +168,52 @@ class Invocation:
pins: Pins
attempt_id: str = ""
operation: str = "admission"
tenant: str = ""
realm: str = ""
INTENT_SCHEMA = "warden.case.original-intent.v1"
@dataclass(frozen=True, slots=True)
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.
"""
binding: str
invocation: Invocation
source: str
def payload(self) -> str:
return json.dumps(
{"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source}
| asdict(self.invocation),
separators=(",", ":"),
sort_keys=True,
)
@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"])
@dataclass(frozen=True, slots=True)
class ReconcileContext:
"""Original intent and the new reconciliation attempt, kept separate."""
original: OriginalIntent
attempt: Invocation
@dataclass(frozen=True, slots=True)
@ -178,6 +224,7 @@ class Adapter:
verify: Callable[[Invocation, OwnerResult], bool]
rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None
reconcile: Callable[[Invocation], OwnerResult] | None = None
reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None
class Journal:
@ -205,6 +252,33 @@ class Journal:
self.db.execute(
"CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)"
)
self.db.execute(
"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:
"""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",
(binding,),
).fetchone()
if row is None:
raise Refused("original intent record missing; step stays uncertain")
intent = OriginalIntent.decode(row[0])
o = intent.invocation
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"):
if getattr(o, name) != getattr(current, name):
raise Refused(f"original intent {name} mismatch")
return intent
@contextmanager
def locked(self):
@ -279,9 +353,15 @@ class Journal:
state: StepState,
evidence: OwnerResult | None = None,
invocation: Invocation | None = None,
intent: OriginalIntent | None = None,
):
self.db.execute("BEGIN IMMEDIATE")
try:
if intent is not None:
self.db.execute(
"INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)",
(intent.invocation.attempt_id, binding, intent.payload()),
)
self.db.execute(
"INSERT INTO receipts(binding,payload) VALUES (?,?)",
(
@ -594,7 +674,12 @@ class Executor:
raise Refused("bounded journal history requires owner archival")
running = begin(state, invocation.pins, retry=retry)
dispatched = self._attempt(invocation, running, "run")
self.journal.save(binding, running, invocation=dispatched)
self.journal.save(
binding,
running,
invocation=dispatched,
intent=OriginalIntent(binding, dispatched, adapter.source),
)
evidence = None
try:
with contained_output():
@ -626,7 +711,12 @@ class Executor:
raise Refused("bounded journal history requires owner archival")
running = begin_rollback(state, invocation.pins)
dispatched = self._attempt(invocation, running, "rollback")
self.journal.save(binding, running, invocation=dispatched)
self.journal.save(
binding,
running,
invocation=dispatched,
intent=OriginalIntent(binding, dispatched, adapter.source),
)
evidence = None
try:
with contained_output():
@ -648,7 +738,7 @@ class Executor:
) -> StepState:
"""Explicit owner-native state inspection; never redispatches the mutation."""
adapter = self._admission(descriptor, invocation, initial)
if adapter.reconcile is None:
if adapter.reconcile is None and adapter.reconcile_original is None:
raise Refused("owner reconciliation adapter absent")
with self.journal.locked():
binding = f"{invocation.step}:{invocation.target}"
@ -667,11 +757,20 @@ class Executor:
Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN),
),
)
original = (
self.journal.original_intent(binding, invocation, adapter.source)
if adapter.reconcile_original is not None
else None
)
dispatched = self._attempt(invocation, state, "reconcile")
self.journal.save(binding, state, invocation=dispatched)
with contained_output():
result = self._result(
adapter, dispatched, adapter.reconcile(dispatched)
adapter,
dispatched,
adapter.reconcile_original(ReconcileContext(original, dispatched))
if original is not None
else adapter.reconcile(dispatched),
)
if result.outcome not in (
Outcome.NO_CHANGE,

View file

@ -629,3 +629,72 @@ def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig):
)
with pytest.raises(Refused, match="secret-sensitivity"):
executor.admit(sources[0], source, sources[2], sources[3], **kwargs)
def _lose_result(executor, args, calls):
old = executor.adapters[args[0].adapter]
def lost(inv):
calls.append(inv)
raise TimeoutError("lost")
seen = []
def by_original(ctx):
seen.append(ctx)
return old.reconcile(ctx.attempt)
executor.adapters[old.reference] = replace(
old, invoke=lost, reconcile=None, reconcile_original=by_original
)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
return seen
def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal = Journal(executor.journal.directory) # restart: fresh handle
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
(ctx,) = seen
original = ctx.original.invocation
assert original.attempt_id == calls[0].attempt_id
assert original.operation == "run" and ctx.attempt.operation == "reconcile"
assert ctx.attempt.attempt_id != original.attempt_id
assert original.pins == args[1].pins
assert (original.principal, original.tenant, original.realm) == (
args[1].principal,
args[1].tenant,
args[1].realm,
)
assert ctx.original.source == executor.adapters[args[0].adapter].source
def test_missing_original_intent_refuses_and_stays_uncertain(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal.db.execute("DELETE FROM intents")
with pytest.raises(Refused, match="original intent record missing"):
executor.reconcile(*args)
assert seen == []
binding = f"{args[1].step}:{args[1].target}"
assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN
@pytest.mark.parametrize(
"change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}]
)
def test_original_intent_mismatch_refuses(rig, change):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
with pytest.raises(Refused):
executor.reconcile(args[0], replace(args[1], **change), args[2])
assert seen == []
def test_original_intent_source_mismatch_refuses(rig):
executor, args, calls, _, _ = rig
_lose_result(executor, args, calls)
binding = f"{args[1].step}:{args[1].target}"
with pytest.raises(Refused, match="source mismatch"):
executor.journal.original_intent(binding, calls[0], "other-source")

View file

@ -161,6 +161,21 @@ reverses exactly this case's steps.
Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through
this path, with no case-specific script.
2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source
and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an
`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run
Invocation — attempt, step, owner, target, actor, principal, lane, all six pins,
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
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
materialization and the native run remain open.
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