WARDEN-WP-0044-T03: original-intent lookup/context for owner reconciliation
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:
parent
d97652a3bb
commit
a45cf475bc
4 changed files with 191 additions and 7 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue