diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/owner-work-record-note.md b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/owner-work-record-note.md new file mode 100644 index 00000000..e17a2b88 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/owner-work-record-note.md @@ -0,0 +1,46 @@ +## 2026-10-16 workload-only candidate lock and actual-manager preparation + +Prepared under existing REINAH-WP-0008-T07 / GLAS-WP-0012 acceptance work. +The combined-runtime lock still selects Glas `30be8180`, so the rebuilt +`818b1b7c` artifact cannot include published workload-only delivery `7357115c`. +The prepared lock-only patch advances that dependency; llm-connect `37436bb5` +and sandboxer `221126cb` remain pinned to the prior admitted source set. Rein +source is `be5ef400`; an isolated preparation commit `c811fc43` contains the +patch. This isolated commit is not an owner publication. + +The existing owner builder produced candidate runtime +`634e65fee5517ebee0d1080805fd84c81bd46af26a2b38ec1ab14bd2be17bf8f`, using the +same native Claude 2.1.266 binary (`19842705…`), frozen uv.lock and source-content +verification. The runtime is only in `/tmp/netkingdom-runtime-next-20261016/runtime`. +Canonical owner repositories, retained artifact pins and native owner +configuration were not changed by this preparation. + +All 141 Glas tests and seven rein runtime-lock/rehearsal tests pass. The complete +runtime digest recomputed after all proofs remains `634e65fee5517…`. Using the candidate's installed packages, the actual +SandboxManager and Glas gateway execute the unchanged +`harness.agent-dev-local@1.1.1` profile (still `unverified`): real rein/Glas CLI +startup, Claude --version, read-only artifact, absent synthetic host file, +inherited environment and child-process denial, exact task identity/private +state, deterministic Git effect in the disposable workspace, task removal and +manager destruction pass. A separately labelled synthetic route fixture proves +only the explicit workload receives its synthetic provider value, output is +redacted, every housekeeping command has empty routes and the following command +is clean. Real manager refusals for wrong actor/project/run, expired execution, +expiry cleanup, explicit cancellation and post-cancellation execution also pass. +No model, provider request, real credential read or spend occurred. + +First proof attempt correctly encountered PermissionError on wrong consumer but +the proof runner expected only ValueError/RuntimeError. Its finally cleanup ran; +the runner was amended, with the failed attempt preserved in proof-attempts.json. +The artifact was unchanged. This is a refined test attempt, not a new unchanged +execution success. + +Owner review/integration of the lock patch must produce an owner source revision +and a rebuilt artifact whose complete digest is verified again. Candidate +preparation does not authorize native installation or admission. T07's queued +spend memo, native artifact installation/rehearsal, renewal and file-pin updates, +required approvals and bounded real-model/tool proof remain open. GLAS native +acceptance and actual interactive assignment migration, denied external tool +paths, bounded delegation and revocation remain open; no readiness or supervision +promotion is inferred. Older `818b1b7c`, `ee3d0a53`, installed `b6e4e8a4` and their +failed/unknown outcomes remain historical evidence. diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/proof-attempts.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/proof-attempts.json new file mode 100644 index 00000000..dec93703 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/proof-attempts.json @@ -0,0 +1,31 @@ +{ + "attempts": [ + { + "attempt": 1, + "outcome": "proof-runner-exception", + "gateway_proof_completed": true, + "failure": "Manager correctly refused mismatched consumer with PermissionError; runner expected ValueError or RuntimeError", + "recovery": "Fixture finally destroyed sandbox. Runner catch amended to include PermissionError. Artifact unchanged.", + "unchanged_success": false + }, + { + "attempt": 2, + "outcome": "passed", + "scope": "exact unmodified1.1.1 gateway + manager identity/expiry/cancellation", + "artifact_unchanged": true + }, + { + "attempt": 3, + "outcome": "passed", + "scope": "same exact profile + separately labelled synthetic credential route gateway fixture + manager lifecycle", + "artifact_unchanged": true + }, + { + "attempt": 4, + "outcome": "passed", + "scope": "canonical published rein9b88cad artifact f59959b6, unchanged1.1.1 gateway and separate synthetic route fixture plus manager lifecycle", + "refinement_from_attempt1_retained": true, + "native_acceptance": false + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-runtime-lock.patch b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-runtime-lock.patch new file mode 100644 index 00000000..ed9547e2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-runtime-lock.patch @@ -0,0 +1,13 @@ +diff --git a/deploy/runtime-contract-lock.json b/deploy/runtime-contract-lock.json +index ee804ca..2ef3087 100644 +--- a/deploy/runtime-contract-lock.json ++++ b/deploy/runtime-contract-lock.json +@@ -13,7 +13,7 @@ + "version": "0.1.0" + }, + { +- "commit": "30be81808140a3a3d137e731d1fe9aed59ad8581", ++ "commit": "7357115c8311e55ee3d495f5266f501a17ec3c89", + "distribution": "glas-harness", + "source": "../glas-harness", + "version": "0.1.0" diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-tests.txt b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-tests.txt new file mode 100644 index 00000000..8e5b23fa --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/rein-tests.txt @@ -0,0 +1,2 @@ +....... [100%] +7 passed in 0.59s diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/runtime-lock-check.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/runtime-lock-check.json new file mode 100644 index 00000000..c0e5ecfe --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/runtime-lock-check.json @@ -0,0 +1,24 @@ +{ + "dependencies": [ + { + "clean": true, + "commit": "37436bb562a3f20abc3b88defc5bae6508c32e7e", + "distribution": "llm-connect", + "version": "0.1.0" + }, + { + "clean": true, + "commit": "7357115c8311e55ee3d495f5266f501a17ec3c89", + "distribution": "glas-harness", + "version": "0.1.0" + }, + { + "clean": true, + "commit": "221126cb77ea41ec283598a23d628f760a51bdea", + "distribution": "sandboxer", + "version": "0.0.0" + } + ], + "ok": true, + "schema_version": "1" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/source-inputs.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/source-inputs.json new file mode 100644 index 00000000..7c24c1b7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation-next/source-inputs.json @@ -0,0 +1,7 @@ +{ + "rein-aharness": "be5ef400861a5e31a31f29d173990c7a143fb189", + "glas-harness": "7357115c8311e55ee3d495f5266f501a17ec3c89", + "llm-connect": "37436bb562a3f20abc3b88defc5bae6508c32e7e", + "sand-boxer": "221126cb77ea41ec283598a23d628f760a51bdea", + "candidate_rein": "c811fc430f80e214b43cfcca5dc9e7f786d5f559" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/README.md b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/README.md new file mode 100644 index 00000000..a03de441 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/README.md @@ -0,0 +1,69 @@ +# Glas owner implementation — 16 October 2026 + +Prepared a concrete source patch in an isolated copy of current tracked Glas +files, retaining working-tree contents. Canonical Glas/Sand-boxer/rein sources, +profiles, native policies, installed artifacts and supervision authority were +not modified. Dirty Glas `.custodian-brief.md` and `WORK-RECORDS.md` are preserved. + +The gateway previously requested profile credential routes for every owner +command, including executable lookup, task writers/removers, Git helpers and +artifact probes. The patch gives housekeeping an empty-route binding and gives +only explicit rein workload execution the profile routes. Both concrete adapters +use the explicit path. Missing workload binding refuses; an SSH selection with +credential routes refuses instead of silently bypassing owner delivery. This +narrows trusted gateway behavior, not arbitrary caller authorization. + +Selected `harness.agent-dev-local@1.1.1` already has empty credential refs and +uses the rein's task-scoped MessagesOwner route outside bwrap. This patch does +not revoke that route or claim its scoped capability is an admin credential. +It prevents future/raw provider route selections from granting housekeeping +provider values. No generic shell broker or supervisor service was added. + +Validation: + +- All 141 Glas tests pass; regression checks cover exact create-time identity, + timeout and stdin, empty housekeeping routes, explicit workload routes, + missing binding refusal and remote credential refusal. +- Actual disposable bwrap through Glas transport proves a synthetic host + credential file absent, inherited BAO/VAULT/AWS/SSH variables denied including + a child subprocess, provider value only in the explicit workload, output + redaction, no subsequent-command value, and workspace destruction. +- `git apply --check` passes against canonical Glas. No provider/model call, + real credential read, remote live execution or spend occurred. + +Artifacts: [patch](glas-harness.patch), [source hashes](source-manifest.json), +[test result](tests.json), [actual bwrap proof](bwrap-proof.json), +[patch check](patch-check.json). Patch SHA-256: +`83eeeb0017603aabae0ff49a23f96248f677f6c9f3e2c01b22f6d0416fca69c7`. +The reproducible proof source is included in the patch. The isolated source +remains `/tmp/netkingdom-agent-isolation-20261016/glas-harness`. + +## Exact native gates retained + +1. Review/apply this owner patch and reconcile its resulting runtime/source pins. + Do not reuse an admitted artifact digest for changed contents. The patch adds + a dated preparation note to GLAS-WP-0012 without completing a native task. +2. Latest dependency sources supersede historical missing-key/lane accounts: + September 27 native lanes were applied, verified and revoked. The corrected + owner runtime `818b1b7c…` was rebuilt October 15 and passes no-credential + startup; it is not installed/admitted on Railiance (still `b6e4e8a4…`). +3. REINAH-WP-0008-T07 retains queued spend memo lifecycle acceptance (workspace + `2ccb4bda-e318-51d2-a8dd-709db768494f`, review + `review-e2d2e98f-4701-45b0-abc1-4fe9f00bd1a8`), owner artifact installation, + `metered-once --rehearse`, owner renewal and Secrets Engine file-pin updates, + required native approvals, then successful bounded model/tool/commit proof. + Previously consumed approvals are historical evidence, not reusable authority. +4. GLAS-WP-0012-T02–T05 remain subject to accepted runtime/profile and combined + actual rein/model/task cleanup proof. The standalone bwrap proof establishes + none of those model/provider outcomes. +5. CUST-WP-0073-T02/T04 and TSA-06 additionally require admission of this actual + interactive agent assignment: its host files/mounts/env/subprocesses and + external tool paths must lose admin authority, with scoped distinguishable + task identity, revocation/expiry/cancellation and bounded delegation evidence. + The current interactive runtime has not moved into this disposable sandbox. + The assignment supervision record remains supervised, authority_granted false; + this owner worker does not approve a privileged path or promote the agent. + +No task status, profile readiness, CUST-WP-0073 completion or TSA-06 acceptance +is inferred from the patch or synthetic proof. The stated owner/native gates +remain live in existing workplans; source delivery is separate from admission. diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/bwrap-proof.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/bwrap-proof.json new file mode 100644 index 00000000..13890122 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/bwrap-proof.json @@ -0,0 +1,21 @@ +{ + "exit_code": 0, + "result": { + "scope": "disposable bwrap via Glas transport; not interactive assignment", + "model_calls": 0, + "real_credentials_read": false, + "host_file_denied": true, + "inherited_env_and_subprocess_denied": true, + "credential_routes_by_command": [ + [], + [ + "synthetic" + ], + [] + ], + "provider_output_redacted": true, + "subsequent_command_clean": true, + "workspace_removed": true + }, + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/glas-harness.patch b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/glas-harness.patch new file mode 100644 index 00000000..248d5ea5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/glas-harness.patch @@ -0,0 +1,464 @@ +--- a/src/glas_harness/contract.py ++++ b/src/glas_harness/contract.py +@@ -117,6 +117,8 @@ + class SandboxHandle(ContractModel): + # Process-local owner binding; never serialized as reachability or evidence. + _owner_execute: Any = PrivateAttr(default=None) ++ _owner_workload_execute: Any = PrivateAttr(default=None) ++ _credential_route_refs: list[str] = PrivateAttr(default_factory=list) + _timeout_seconds: int = PrivateAttr(default=600) + + sandbox_id: str +--- a/src/glas_harness/gateway.py ++++ b/src/glas_harness/gateway.py +@@ -7,6 +7,7 @@ + from __future__ import annotations + + import time ++from functools import partial + from collections.abc import Callable + import uuid + from datetime import UTC, datetime +@@ -145,19 +146,23 @@ + reachability=reachability, + ) + +- def owner_execute(argv, input_text, timeout): ++ def owner_execute(argv, input_text, timeout, *, credential_routes=()): + return manager.execute( + sandbox.sandbox_id, + SandboxExecRequest( + command=list(argv), + consumer=consumer, +- credential_route_refs=profile.credential_route_refs, ++ credential_route_refs=list(credential_routes), + stdin_text=input_text, + timeout_seconds=timeout, + ), + ) + + sandbox._owner_execute = owner_execute ++ sandbox._owner_workload_execute = partial( ++ owner_execute, credential_routes=tuple(profile.credential_route_refs) ++ ) ++ sandbox._credential_route_refs = list(profile.credential_route_refs) + sandbox._timeout_seconds = profile.limits.timeout_seconds or 600 + + session = selected_rein.start_session( +--- a/src/glas_harness/transport.py ++++ b/src/glas_harness/transport.py +@@ -13,7 +13,7 @@ + import shlex + import subprocess + import uuid +-from dataclasses import dataclass ++from dataclasses import dataclass, replace + from pathlib import Path, PurePosixPath + from typing import Any, Callable, Literal, Sequence + +@@ -36,6 +36,7 @@ + pid: int | None = None + ssh_target: str | None = None + owner_execute: Callable | None = None ++ owner_workload_execute: Callable | None = None + timeout_seconds: int = 600 + + def command(self, argv: Sequence[str]) -> list[str]: +@@ -94,6 +95,26 @@ + timeout=bounded_timeout, + ) + ++ def run_workload( ++ self, ++ argv: Sequence[str], ++ *, ++ input_text: str | None = None, ++ timeout: float | None = None, ++ ) -> subprocess.CompletedProcess[str]: ++ """Run the selected rein; only this path requests provider routes. ++ ++ The gateway is trusted control-plane code. This separation narrows its ++ credential delivery; it is not authentication of arbitrary Python callers. ++ """ ++ if self.kind == "local_namespace": ++ if self.owner_workload_execute is None: ++ raise TransportError("workload requires an owner workload binding") ++ return replace(self, owner_execute=self.owner_workload_execute).run( ++ argv, input_text=input_text, timeout=timeout ++ ) ++ return self.run(argv, input_text=input_text, timeout=timeout) ++ + def resolve_executable(self, name: str, *, timeout: float = 15.0) -> str: + if not name or "/" in name: + raise TransportError("rein executable must be a command name, not a host path") +@@ -169,9 +190,12 @@ + return ExecutionTransport( + kind="local_namespace", workspace=str(workspace), pid=parsed_pid, + owner_execute=sandbox._owner_execute, ++ owner_workload_execute=sandbox._owner_workload_execute, + timeout_seconds=sandbox._timeout_seconds, + ) + if has_remote: ++ if sandbox._credential_route_refs: ++ raise TransportError("SSH transport has no owner credential delivery contract") + if not ssh_target or not remote_dir: + raise TransportError( + "incomplete remote sandbox reachability: ssh and remote_dir are required" +--- a/src/glas_harness/reins/rein_aharness.py ++++ b/src/glas_harness/reins/rein_aharness.py +@@ -109,7 +109,7 @@ + argv += ["--tool-profile", self.tool_profile] + if self.budget_tokens: + argv += ["--budget-tokens", str(self.budget_tokens)] +- proc = transport.run(argv, timeout=session["timeout_seconds"]) ++ proc = transport.run_workload(argv, timeout=session["timeout_seconds"]) + ok = proc.returncode == 0 + output, events = self._split_stream_events(proc.stdout) + last_result = session["last_result"] = parse_json_object(output) +--- a/src/glas_harness/reins/rein_openweights.py ++++ b/src/glas_harness/reins/rein_openweights.py +@@ -91,7 +91,7 @@ + argv += ["--budget-tokens", str(self.budget_tokens)] + if self.tool_profile: + argv += ["--tool-profile", self.tool_profile] +- proc = transport.run(argv, timeout=session["timeout_seconds"]) ++ proc = transport.run_workload(argv, timeout=session["timeout_seconds"]) + ok = proc.returncode == 0 + last_result = session["last_result"] = parse_json_object(proc.stdout) + return ToolResult( +--- a/tests/test_gateway.py ++++ b/tests/test_gateway.py +@@ -150,6 +150,7 @@ + assert "_owner_execute" not in sandbox.model_dump() + transport = transport_from_sandbox(sandbox) + transport.run(["printf", "%s", "non-secret"], timeout=30) ++ transport.run_workload(["rein-aharness", "run"], timeout=30) + path = transport.write_task_file({"title": "private prompt"}) + transport.remove_file(path) + return super().start_session(profile, inputs, sandbox) +@@ -164,14 +165,14 @@ + assert create_consumer.project == "exact-project" + assert create_consumer.run_id == result.evidence.request_id + assert create_consumer.session_id is None +- assert manager.execute.call_count == 3 ++ assert manager.execute.call_count == 4 + for call in manager.execute.call_args_list: + sandbox_id, command = call.args + assert sandbox_id == "sbx1" + assert command.consumer == create_consumer +- assert command.credential_route_refs == refs ++ assert command.credential_route_refs == (refs if command.command[0] == "rein-aharness" else []) + assert command.timeout_seconds == 7 +- writer = manager.execute.call_args_list[1].args[1] ++ writer = manager.execute.call_args_list[2].args[1] + assert writer.stdin_text == '{"title": "private prompt"}' + assert "private prompt" not in str(writer.command) + manager.destroy.assert_called_once_with("sbx1") +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -173,3 +173,16 @@ + ): + with pytest.raises(TransportError, match="inside the selected sandbox"): + transport.git_head() ++ ++ ++def test_workload_refuses_missing_dedicated_owner_binding(): ++ transport = ExecutionTransport(kind="local_namespace", workspace="/work", owner_execute=lambda *args: None) ++ with pytest.raises(TransportError, match="workload binding"): ++ transport.run_workload(["true"]) ++ ++ ++def test_remote_transport_refuses_requested_credential_routes(): ++ sandbox = SandboxHandle(sandbox_id="remote", host="remote", reachability={"ssh":"agent@host", "remote_dir":"/work"}) ++ sandbox._credential_route_refs = ["provider-route"] ++ with pytest.raises(TransportError, match="credential delivery"): ++ transport_from_sandbox(sandbox) +--- a/tests/test_rein_aharness.py ++++ b/tests/test_rein_aharness.py +@@ -80,18 +80,18 @@ + budget_tokens=1234, + ) + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="") +- session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} +- +- with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): +- result = rein.dispatch_tool(session, ToolCall(name="run_task")) +- +- argv = transport.run.call_args.args[0] ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout="ok", stderr="") ++ session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} ++ ++ with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): ++ result = rein.dispatch_tool(session, ToolCall(name="run_task")) ++ ++ argv = transport.run_workload.call_args.args[0] + assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"] + assert argv[argv.index("--model") + 1] == "claude-sonnet-4-6" + assert argv[argv.index("--tool-profile") + 1] == "green-commit-only" + assert argv[argv.index("--budget-tokens") + 1] == "1234" +- assert transport.run.call_args.kwargs["timeout"] == 30 ++ assert transport.run_workload.call_args.kwargs["timeout"] == 30 + assert result.ok is True + assert result.output == "ok" + +@@ -99,7 +99,7 @@ + def test_dispatch_tool_reports_failure() -> None: + rein = ReinAharness() + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=1, stdout="", stderr="boom") ++ transport.run_workload.return_value = MagicMock(returncode=1, stdout="", stderr="boom") + session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} + + with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): +@@ -167,14 +167,14 @@ + json.dumps({"stream_event": {"type": "system", "subtype": "hook_started"}}), + json.dumps({"ok": True, "committed": True}, indent=2), + ] +- transport.run.return_value = MagicMock( ++ transport.run_workload.return_value = MagicMock( + returncode=0, stdout="\n".join(stdout_lines), stderr="" + ) + + with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): + result = rein.dispatch_tool(session, ToolCall(name="run_task")) + +- argv = transport.run.call_args.args[0] ++ argv = transport.run_workload.call_args.args[0] + assert "--stream-tool-events" in argv + assert len(result.events) == 2 + assert result.events[0]["message"]["content"][0]["name"] == "Read" +@@ -186,13 +186,13 @@ + def test_dispatch_tool_no_stream_flag_by_default() -> None: + rein = ReinAharness() + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="") +- session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} +- +- with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): +- result = rein.dispatch_tool(session, ToolCall(name="run_task")) +- +- argv = transport.run.call_args.args[0] ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout="ok", stderr="") ++ session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} ++ ++ with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"): ++ result = rein.dispatch_tool(session, ToolCall(name="run_task")) ++ ++ argv = transport.run_workload.call_args.args[0] + assert "--stream-tool-events" not in argv + assert result.events == [] + +@@ -235,7 +235,7 @@ + payload = {"ok": True} + if reported is not None: + payload["model"] = reported +- transport.run.return_value = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="") ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="") + transport.git_head.return_value = "new-head" + session = {"task_file": "/tmp/task.json", "transport": transport, + "timeout_seconds": 30, "head_before": "old-head"} +--- a/tests/test_rein_openweights.py ++++ b/tests/test_rein_openweights.py +@@ -76,13 +76,13 @@ + def test_dispatch_tool_invokes_rein_openweights_cli() -> None: + rein = ReinOpenWeights() + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="") ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout="ok", stderr="") + session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} + + with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"): + result = rein.dispatch_tool(session, ToolCall(name="run_task")) + +- argv = transport.run.call_args.args[0] ++ argv = transport.run_workload.call_args.args[0] + assert argv[:3] == ["/usr/bin/rein-openweights", "run", "--task-file"] + assert "--no-hub" in argv + assert result.ok is True +@@ -97,13 +97,13 @@ + tool_profile="green-commit-only", + ) + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="") ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout="ok", stderr="") + session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} + + with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"): + rein.dispatch_tool(session, ToolCall(name="run_task")) + +- argv = transport.run.call_args.args[0] ++ argv = transport.run_workload.call_args.args[0] + assert "--model" in argv + assert "meta-llama/llama-3.1-70b-instruct" in argv + assert argv[argv.index("--max-turns") + 1] == "8" +@@ -114,7 +114,7 @@ + def test_dispatch_tool_reports_failure() -> None: + rein = ReinOpenWeights() + transport = MagicMock(spec=ExecutionTransport) +- transport.run.return_value = MagicMock(returncode=1, stdout="", stderr="boom") ++ transport.run_workload.return_value = MagicMock(returncode=1, stdout="", stderr="boom") + session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30} + + with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"): +@@ -127,7 +127,7 @@ + def test_dispatch_tool_enforces_outer_timeout() -> None: + rein = ReinOpenWeights() + transport = MagicMock(spec=ExecutionTransport) +- transport.run.side_effect = subprocess.TimeoutExpired("rein-openweights", 30) ++ transport.run_workload.side_effect = subprocess.TimeoutExpired("rein-openweights", 30) + session = { + "task_file": "/tmp/task.json", + "transport": transport, +@@ -138,7 +138,7 @@ + with pytest.raises(subprocess.TimeoutExpired): + rein.dispatch_tool(session, ToolCall(name="run_task")) + +- assert transport.run.call_args.kwargs["timeout"] == 30 ++ assert transport.run_workload.call_args.kwargs["timeout"] == 30 + + + def test_end_session_detects_new_commit(tmp_path) -> None: +@@ -183,7 +183,7 @@ + payload = {"ok": True} + if reported is not None: + payload["model"] = reported +- transport.run.return_value = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="") ++ transport.run_workload.return_value = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="") + transport.git_head.return_value = "new-head" + session = {"task_file": "/tmp/task.json", "transport": transport, + "timeout_seconds": 30, "head_before": "old-head"} +--- a/tests/test_rein_session_isolation.py ++++ b/tests/test_rein_session_isolation.py +@@ -19,7 +19,7 @@ + transport = MagicMock(spec=ExecutionTransport) + transport.resolve_executable.return_value = "/runtime/rein" + transport.git_head.return_value = f"head-{index}" +- transport.run.return_value = MagicMock(returncode=0 if ok else 1, stderr="", ++ transport.run_workload.return_value = MagicMock(returncode=0 if ok else 1, stderr="", + stdout=json.dumps({"ok": ok, "tokens_spent": 10 + index, + "model": f"model-{index}", "reason": None if ok else "failed"})) + sessions.append({"transport": transport, "task_file": f"/workspace-{index}/task", +--- /dev/null ++++ b/scripts/prove-credential-command-scope.py +@@ -0,0 +1,55 @@ ++"""No model/provider: actual bwrap transport proof with synthetic credentials.""" ++import json ++import os ++import sys ++import tempfile ++from pathlib import Path ++from sandboxer.extensions.bwrap import BwrapExtension ++from sandboxer.models import Profile ++from glas_harness.contract import SandboxHandle ++from glas_harness.transport import transport_from_sandbox ++ ++ ++def main(): ++ with tempfile.TemporaryDirectory(prefix="glas-command-scope-") as tmp: ++ root = Path(tmp) ++ sentinel = root / "admin-sentinel" ++ sentinel.write_text("synthetic-file-only") ++ provider = root / "provider.py" ++ provider.write_text("import os,subprocess,sys\nenv=os.environ.copy()\nenv['ANTHROPIC_API_KEY']='synthetic-provider-only'\nraise SystemExit(subprocess.call(sys.argv[sys.argv.index('--')+1:],env=env))\n") ++ ext = BwrapExtension({"base_dir": str(root / "sandboxes"), "credential_routes": { ++ "synthetic": {"profiles":["profile.scope-proof"], "projects":["scope-proof"], "actors":["agt"], "exec_argv":[sys.executable,str(provider),"--"]}}}) ++ context = {"actor":"agt", "project":"scope-proof", "run_id":"scope-proof-1", "profile_id":"profile.scope-proof"} ++ # Inherited values are synthetic. Real owner credentials are never read. ++ before = {key:os.environ.get(key) for key in ("BAO_TOKEN","VAULT_TOKEN","AWS_SECRET_ACCESS_KEY","SSH_AUTH_SOCK")} ++ for key in before: ++ os.environ[key] = "synthetic-inherited-only" ++ handle = None ++ try: ++ handle = ext.provision(Profile(id="profile.scope-proof",version="1",extension="ext.bwrap"), {}, "localhost") ++ ext.wait_ready(handle) ++ sandbox = SandboxHandle(sandbox_id=handle["sandbox_id"],host="localhost",reachability={"pid":handle["pid"],"workspace_dir":handle["workspace_dir"]}) ++ calls=[] ++ def execute(argv, input_text, timeout, routes=()): ++ calls.append(list(routes)) ++ raw=ext.execute(handle,argv,credential_route_refs=list(routes),execution_context=context,timeout_seconds=timeout,max_output_bytes=4096,stdin_text=input_text) ++ return type("Result",(),raw)() ++ sandbox._owner_execute=execute ++ sandbox._owner_workload_execute=lambda argv,stdin,timeout: execute(argv,stdin,timeout,["synthetic"]) ++ transport=transport_from_sandbox(sandbox) ++ probe = "import os,pathlib,subprocess; assert not pathlib.Path("+repr(str(sentinel))+").exists(); assert all(k not in os.environ for k in "+repr(list(before))+"); assert subprocess.check_output(['python3','-c',\"import os; print(os.environ.get('BAO_TOKEN','absent'))\"]).strip()==b'absent'; " ++ for workload in (False,True,False): ++ check = probe + ("assert os.environ['ANTHROPIC_API_KEY']=='synthetic-provider-only'; print(os.environ['ANTHROPIC_API_KEY'])" if workload else "assert 'ANTHROPIC_API_KEY' not in os.environ; print('clean')") ++ result=(transport.run_workload if workload else transport.run)(["python3","-c",check],timeout=10) ++ assert result.returncode==0, result.stderr ++ assert result.stdout == ("[REDACTED]\n" if workload else "clean\n") ++ assert calls==[[],["synthetic"],[]] ++ finally: ++ teardown=ext.teardown(handle) if handle else {} ++ for key,value in before.items(): ++ if value is None: os.environ.pop(key,None) ++ else: os.environ[key]=value ++ assert teardown.get("workspace_removed")=="True" ++ print(json.dumps({"scope":"disposable bwrap via Glas transport; not interactive assignment", "model_calls":0,"real_credentials_read":False,"host_file_denied":True,"inherited_env_and_subprocess_denied":True,"credential_routes_by_command":calls,"provider_output_redacted":True,"subsequent_command_clean":True,"workspace_removed":True})) ++ ++if __name__ == "__main__": main() +--- a/docs/harness-contract.md ++++ b/docs/harness-contract.md +@@ -80,9 +80,17 @@ + not treated as sandbox availability. + + The gateway binds local execution to the exact create-time consumer: actor, +-project, and resolved request id as `run_id`. Every owner command forwards the +-selected profile's value-free `credential_route_refs` and a timeout capped by +-its limit. The binding is process-local and excluded from serialized handles. ++project, and resolved request id as `run_id`. Housekeeping `run` commands ++(executable lookup, task creation/removal, Git inspection and artifact capture) ++request no credential routes. Only the selected rein's explicit `run_workload` ++command forwards the selected profile's value-free `credential_route_refs`. ++Both bindings cap timeouts by the profile limit and remain process-local, ++excluded from serialized handles. An absent workload binding refuses rather ++than using the housekeeping binding. SSH profiles with credential routes refuse ++because SSH has no owner-mediated credential delivery contract. These bindings ++narrow trusted gateway behavior; they do not authenticate arbitrary Python ++callers, remove task-scoped MessagesOwner routes or establish isolation of ++the caller's interactive runtime. + An unbound local descriptor refuses execution; no consumer namespace entry or + host execution fallback exists. Owner timeouts and truncated output fail closed. + Generated task specs travel over stdin to an exclusive mode-0600 writer inside +@@ -187,3 +195,10 @@ + Unknown remains null, including failure before provider execution. A reported + alias does not attest an exact provider revision or conversation identifier. + See `docs/evidence/2026-10-03-emergent-session-readiness-review.md`. ++ ++ ++`scripts/prove-credential-command-scope.py` exercises the Glas transport against ++a disposable real bwrap namespace with synthetic material. It checks host-file ++absence, inherited credential-variable denial through a subprocess, explicit ++workload-only provider delivery, output redaction, subsequent-command absence ++and teardown. It uses no provider or model and cannot promote a runtime profile. +--- a/workplans/GLAS-WP-0012-first-local-profile-production-proof.md ++++ b/workplans/GLAS-WP-0012-first-local-profile-production-proof.md +@@ -423,3 +423,28 @@ + available model/session metadata. The current lifecycle exposes no continuity + event API. Existing owner-delivery blockers and all task/readiness states remain; + local deterministic tests are not governed provider or isolation acceptance. ++ ++ ++## 2026-10-16 NetKingdom isolated integration preparation ++ ++Prepared, without owner deployment or readiness promotion, a Glas change that ++separates credential-free housekeeping commands from explicit rein workload ++execution. Both concrete adapters use the workload path; SSH selections with ++credential routes refuse because that transport has no owner delivery contract. ++Full regression: 141 passed. Real disposable bwrap/Glas transport proof passes ++synthetic host-file denial, inherited credential environment and subprocess ++denial, workload-only delivery, redaction, subsequent-command absence and ++teardown. No provider/model call or real credential read occurred. ++ ++Latest owner dependency sources supersede the stale missing-lane account: ++September 27 native lanes were activated and revoked; corrected immutable ++owner runtime is `818b1b7c…` (October 15 startup proof), not yet installed or ++admitted on Railiance. REINAH-WP-0008-T07 retains memo lifecycle acceptance, ++installation, rehearsal, renewal/pin updates, native approvals and bounded ++successful model/tool/commit acceptance. Any subsequent build containing this ++patch has a different source/artifact pin requiring owner admission. ++ ++This is a reviewable patch in Custodian's NetKingdom change bundle; no canonical ++source synchronization or workplan task completion is implied. CUST-WP-0073 ++continues to require admission of the actual interactive assignment, including ++its existing host credential and external tool paths, not a disposable fixture. diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/patch-check.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/patch-check.json new file mode 100644 index 00000000..e998d35d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/patch-check.json @@ -0,0 +1,5 @@ +{ + "exit_code": 0, + "stdout": "", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/source-manifest.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/source-manifest.json new file mode 100644 index 00000000..563a4175 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/source-manifest.json @@ -0,0 +1,120 @@ +{ + "owner": "glas-harness", + "canonical_source_modified": false, + "isolated_tree": "/tmp/netkingdom-agent-isolation-20261016/glas-harness", + "sources": [ + { + "repo": "glas-harness", + "head": "dc88f302044c7c2b41795958527e64be8a83e3ab", + "status": "M .custodian-brief.md\n M WORK-RECORDS.md\n" + }, + { + "repo": "sand-boxer", + "head": "53f800c4f7a9c91469834d04ef6f58e906b0bd49", + "status": "" + }, + { + "repo": "rein-aharness", + "head": "be5ef400861a5e31a31f29d173990c7a143fb189", + "status": "" + } + ], + "changes": [ + { + "path": "src/glas_harness/contract.py", + "baseline_sha256": "ad5f63b4a22ff65a7e9bf956ea65b04d55fe3d7268bbbdf6956aabd118ce4238", + "candidate_sha256": "db9a3540221229de9e4a6ec04216f7bea9e99136f26931418a994c37b6eb21df" + }, + { + "path": "src/glas_harness/gateway.py", + "baseline_sha256": "d6c7636b7d1fb1d16fa66f7105ff18a769414eb4c99f21e62557c8b04c3fe9a8", + "candidate_sha256": "5e998bfa53ade5dafbae799dc04b77031b6414e2af49797f45727d59d7397d42" + }, + { + "path": "src/glas_harness/transport.py", + "baseline_sha256": "91f5a5adeb7a27d0a828eb0c801fd2381f7d272a521cd5f563d04d5fa022d0f2", + "candidate_sha256": "fa1686298b7403390396fa2786a2b6711609980a4a0fca5f2e38f250ae640b5c" + }, + { + "path": "src/glas_harness/reins/rein_aharness.py", + "baseline_sha256": "67bf8369501c96227cc5f89158ad4c41cf2c86bd7f4235f162e7da8cadcc8f05", + "candidate_sha256": "2f725a4dbda311bf2626959dd439ab9d70e82021496fecf61d9319091873c7a7" + }, + { + "path": "src/glas_harness/reins/rein_openweights.py", + "baseline_sha256": "7da551d0bfc42588897ecae6145bdd90813dd64611ff818a03ea5144ebd8dd90", + "candidate_sha256": "e0f7261cafed7f4fe67d34bf27cf431dfeca18536c1cbd84626fa25017cd44ad" + }, + { + "path": "tests/test_gateway.py", + "baseline_sha256": "0197543fda862a1c6cb1e2c974ef4ea7969017fe8ca3f212e5c0d7e8d13308de", + "candidate_sha256": "8ecae8010b1c6182591b12b335af8f2f6e6e08366b646eb138a4ef08679b6634" + }, + { + "path": "tests/test_transport.py", + "baseline_sha256": "27b3d0e2d78e128e2265467835d92d7ab9dcc06637e4ffc236bdc791488703c9", + "candidate_sha256": "50c86170d9dc9cecbef49886a733f6ca78b5a8be5d67701b521d98bd81dfaabe" + }, + { + "path": "tests/test_rein_aharness.py", + "baseline_sha256": "3d34a6dfdb8962263d254e5f40f64147af7da15f6d1ac377854ff688cd7a9032", + "candidate_sha256": "5dc802830f902d4e4a502c981e7626a8282b6dbd4e9829659f9efedfcf1dd16e" + }, + { + "path": "tests/test_rein_openweights.py", + "baseline_sha256": "1b0c4d16e0c885f6dd0f25dac3de3372a93ab3d65c1432b0eebac045625b4665", + "candidate_sha256": "c71c66976982e5375dd2513e2f392e8884305be1690856a37fa5a78075aee222" + }, + { + "path": "tests/test_rein_session_isolation.py", + "baseline_sha256": "0c6a875cd8529b3f084a3b055a1edd86af0703489ced4669671dff817c296a84", + "candidate_sha256": "699b554f5889df5afee2b556e50602876f849ef7fde1dd612408a7acfc597dc1" + }, + { + "path": "scripts/prove-credential-command-scope.py", + "baseline_sha256": null, + "candidate_sha256": "acc7ca6e29b3ace36c734ac3478f48ba901805e9a9acca98bd1ee8820f6fa1a8" + }, + { + "path": "docs/harness-contract.md", + "baseline_sha256": "96e274ea547844e5062eab96c05231bc4d9ffb81306f1e9c5e3cc1ee2c8c87f9", + "candidate_sha256": "0bc3a43ff9a1b72c4668d6ca8e936163f865c2b302c920f7ede6c5e00d59a39d" + }, + { + "path": "workplans/GLAS-WP-0012-first-local-profile-production-proof.md", + "baseline_sha256": "83978dc6cfdc836cca7b5cd12b03b878c944d67598f9505a196701fa534f6cfc", + "candidate_sha256": "881ab0f5ba3bb43f5404c8f4cd23f9628e014992ba88fc13f541f0fa8b7b1eaa" + } + ], + "patch_sha256": "83eeeb0017603aabae0ff49a23f96248f677f6c9f3e2c01b22f6d0416fca69c7", + "dependency_sources": [ + { + "path": "/home/worsch/sand-boxer/src/sandboxer/extensions/bwrap.py", + "sha256": "eff580a498f3609d3e07e41440f926016fd46a7b0c6273c37445921ea6e03cb0" + }, + { + "path": "/home/worsch/sand-boxer/src/sandboxer/extensions/bwrap_runner.py", + "sha256": "c83cdd2758bae58cd5d5694f5d6752736f59d479cfc6e9f0f8841c1f5596cba6" + }, + { + "path": "/home/worsch/sand-boxer/workplans/SAND-WP-0015-bwrap-runtime-and-private-state.md", + "sha256": "230d1ef37aa9c2023335a617492a72678c4671a8bc82d5577e9d7a7cea6e8fe4" + }, + { + "path": "/home/worsch/rein-aharness/rein_aharness/adapter.py", + "sha256": "de55aec9a19c5e17c59febb7e0481400ab4a47880f54bf8ca46b6dd8ec8fcf17" + }, + { + "path": "/home/worsch/rein-aharness/rein_aharness/messages_owner.py", + "sha256": "14129bc0ae9611d5b2e04e277a292b2ee09b32f18d896b94741038fdab867abd" + }, + { + "path": "/home/worsch/rein-aharness/workplans/REINAH-WP-0008-simpler-metered-retry.md", + "sha256": "3f46cf1f278e35be994772cb2d7e6d95f570fb5bfba28c6b55dc89b663d25f89" + }, + { + "path": "/home/worsch/the-custodian/.kaizen/agents/custodian-codex/supervision.json", + "sha256": "407340493ffe584e9a245911d01f60e8f9ec1478f84478388f0210fd0c8b1a6d" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/agent-isolation/tests.json b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/tests.json new file mode 100644 index 00000000..376a4560 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/agent-isolation/tests.json @@ -0,0 +1,7 @@ +{ + "command": "PYTHONPATH=/tmp/netkingdom-agent-isolation-20261016/glas-harness/src:/tmp/netkingdom-agent-isolation-20261016/sand-boxer/src /home/worsch/glas-harness/.venv/bin/python -m pytest /tmp/netkingdom-agent-isolation-20261016/glas-harness/tests -q", + "exit_code": 0, + "stdout": "141 passed in 10.89s", + "python": "/home/worsch/glas-harness/.venv/bin/python", + "native_provider_requests": 0 +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/README.md b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/README.md new file mode 100644 index 00000000..a4eae8c4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/README.md @@ -0,0 +1,44 @@ +# Approval exact-binding confirmation repair + +Approval owner source preparation from clean source commit +`477c683991fda3c01e2af98a18f5a8719ee9dbc9`, in an isolated temporary checkout. +Pending canonical WP0004 closure changes, generated UUIDs and unrelated dirt +remain untouched. `owner-dirty-baseline.patch` preserves that observed state; +`implementation.patch` applies cleanly on top of it. + +Reproduction on unchanged production client source: canonical binding dict +equality accepts nested True/1, False/0, 1/1.0 and 1/True substitutions. Missing, +wrong and boolean served native digests also went unchecked. Seven regression +cases failed their expected refusal, while a valid nested/key-order case passed. +This is client confirmation ambiguity, not proof of a production exploit or +native acceptance failure. + +The minimal fix reuses Approval's existing native `binding_digest`: recompute +the served five-field act digest, compare it with the expected act digest, and +require the served digest field to match. No new digest, API schema, authority, +service or approval contract is introduced. Native binding, approval-free PDP +and full consumed request digests retain their separate meanings. + +Twelve added cases cover four Python-equal/JSON-distinct substitutions with +stale and recomputed served digests, three missing/invalid digest cases, and valid +nested types plus reordered keys. The final confirmation suite passes 36 tests. +Final focused command including consume provenance and the unavailable-server +case passes 44 tests. A broader earlier run passed 101 and failed one existing +200ms transport test because isolated child startup reached the total deadline +instead of returning its expected `unreachable` text; both outcomes fail closed. +Its focused retry passed. That transient failure is retained in validation.json. + +New bounded low-risk source task: APPROVAL-WP-ADHOC-2026-10-16-T01. WP0004 closure +is not reopened, and existing WP0002 native residuals remain open. This source +preparation is not formal delegated-owner assent, signed approval or native +acceptance. No live credential, approval service, OpenBao or protected operation +was used. Coordinator review/integration/publication/sync remain pending. + +After integration, Platform must pin the actual integrated confirmation client +source. Prepared `approval_engine/execution_confirmation.py` SHA256 is +`afd4383d6785b12be9650d1cb3ef84ccbb1b57cd271d38d55e75875c46e85a79`. +Its actual mediator must retain independently authenticated provider transport, +exact fresh claim/signed decision/body/operation checks and the same live one-shot +journal admission. This historical confirmation fact never grants execution or +retries uncertain protected work. Native validation must use the final pinned +client, and prior disposable proof must state which earlier client it exercised. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/implementation.patch new file mode 100644 index 00000000..3eac1ce8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/implementation.patch @@ -0,0 +1,140 @@ +diff --git a/approval_engine/execution_confirmation.py b/approval_engine/execution_confirmation.py +index 2fef8b9..fa7daaa 100644 +--- a/approval_engine/execution_confirmation.py ++++ b/approval_engine/execution_confirmation.py +@@ -8,7 +8,7 @@ from datetime import datetime + from typing import Any + from urllib.parse import quote + +-from .binding import DIGEST_RE, canonical_binding ++from .binding import DIGEST_RE, binding_digest + from .pep import ApprovalHTTPClient, ApprovalProtocolError, _ID_RE + + +@@ -71,7 +71,7 @@ class ExecutionConfirmationClient(ApprovalHTTPClient): + if not isinstance(expected_pdp_digest, str) or not DIGEST_RE.fullmatch(expected_pdp_digest): + raise ApprovalProtocolError("exact approval-free PDP digest is required") + try: +- expected = canonical_binding(expected_binding) ++ expected = binding_digest(expected_binding) + except (TypeError, ValueError, KeyError): + raise ApprovalProtocolError("expected act binding is invalid") from None + obj = self._request("GET", f"/v1/approvals/{quote(approval_id, safe='')}") +@@ -81,7 +81,11 @@ class ExecutionConfirmationClient(ApprovalHTTPClient): + raise ApprovalProtocolError("consumption object is not a decision") + served = obj.get("binding") + try: +- same_act = isinstance(served, dict) and canonical_binding(served) == expected ++ same_act = ( ++ isinstance(served, dict) ++ and binding_digest(served) == expected ++ and served.get("digest") == expected ++ ) + except (TypeError, ValueError, KeyError): + same_act = False + if not same_act or served.get("pdp_digest") != expected_pdp_digest: +diff --git a/tests/test_execution_confirmation.py b/tests/test_execution_confirmation.py +index fed3f83..9892ff3 100644 +--- a/tests/test_execution_confirmation.py ++++ b/tests/test_execution_confirmation.py +@@ -127,3 +127,50 @@ def test_denied_read_never_represents_confirmation(engine, app, tmp_path): + raise HTTPError(req.full_url, status, "refused", {}, None) + with pytest.raises(ApprovalProtocolError, match="unauthorized"): + confirmer(client(tmp_path, opener), obj) ++ ++ ++@pytest.mark.parametrize("original,substitute", [(True, 1), (False, 0), (1, 1.0), (1, True)]) ++@pytest.mark.parametrize("recompute_served_digest", [False, True]) ++def test_confirmation_refuses_python_equal_json_distinct_target( ++ engine, tmp_path, original, substitute, recompute_served_digest ++): ++ expected = binding(target={"nested": [{"value": original}]}) ++ obj = engine.create(expected, validity(), pdp_digest=FREE, pdp_path=True, ++ human_control=True) ++ engine.add_entry(obj.id, "human:founder", principal_type="human") ++ engine.consume(obj.id, FULL, decision_id="decision:original") ++ result = engine.get(obj.id).as_dict() ++ result["binding"]["target"] = {"nested": [{"value": substitute}]} ++ if recompute_served_digest: ++ from approval_engine.binding import binding_digest ++ result["binding"]["digest"] = binding_digest(result["binding"]) ++ assert result["binding"]["target"] == expected["target"] ++ with pytest.raises(ApprovalProtocolError, match="binding does not match"): ++ client(tmp_path, lambda *_a, **_k: Response(result)).confirm_consumption( ++ obj.id, FULL, "decision:original", expected_binding=expected, ++ expected_pdp_digest=FREE) ++ ++ ++@pytest.mark.parametrize("digest", [None, "sha256:" + "00" * 32, True]) ++def test_confirmation_requires_served_native_digest_correspondence(engine, tmp_path, digest): ++ obj = consumed(engine) ++ result = engine.get(obj.id).as_dict() ++ if digest is None: ++ result["binding"].pop("digest") ++ else: ++ result["binding"]["digest"] = digest ++ with pytest.raises(ApprovalProtocolError, match="binding does not match"): ++ confirmer(client(tmp_path, lambda *_a, **_k: Response(result)), obj) ++ ++ ++def test_confirmation_accepts_nested_json_types_and_key_order(engine, tmp_path): ++ expected = binding(target={"b": [True, False, 1, 1.0], "a": {"unicode": "ä"}}) ++ obj = engine.create(expected, validity(), pdp_digest=FREE, pdp_path=True, ++ human_control=True) ++ engine.add_entry(obj.id, "human:founder", principal_type="human") ++ engine.consume(obj.id, FULL, decision_id="decision:original") ++ result = engine.get(obj.id).as_dict() ++ result["binding"]["target"] = dict(reversed(list(expected["target"].items()))) ++ assert client(tmp_path, lambda *_a, **_k: Response(result)).confirm_consumption( ++ obj.id, FULL, "decision:original", expected_binding=expected, ++ expected_pdp_digest=FREE)["approval_id"] == obj.id +diff --git a/workplans/ADHOC-2026-10-16.md b/workplans/ADHOC-2026-10-16.md +new file mode 100644 +index 0000000..cd19170 +--- /dev/null ++++ b/workplans/ADHOC-2026-10-16.md +@@ -0,0 +1,44 @@ ++--- ++id: APPROVAL-WP-ADHOC-2026-10-16 ++type: workplan ++title: "Repair exact consume-confirmation native binding comparison" ++domain: infotech ++repo: approval-engine ++status: finished ++flavor: implementation ++owner: codex ++topic_slug: netkingdom ++created: "2026-10-16" ++updated: "2026-10-16" ++--- ++ ++## Refuse Python-equal JSON-distinct consumed act bindings ++ ++```task ++id: APPROVAL-WP-ADHOC-2026-10-16-T01 ++status: done ++priority: high ++``` ++ ++Bounded source repair demanded by the CUST-WP-0080 strict execution-seam proof. ++INTENT basis: exact binding and authenticated consumption evidence; no authority ++or native grant is added. Existing canonical native-five-field digest is reused. ++ExecutionConfirmationClient now recomputes the served and expected digests and ++requires the served digest field to correspond, replacing Python dict equality ++which accepted nested True/1, False/0 and integer/float substitutions. ++ ++Before repair seven regression cases wrongly confirmed (four target type ++substitutions and three absent/wrong/type-invalid served digest fields); valid ++nested types with reordered keys passed. The repair retains this failure history. ++Twelve added regression cases cover exact native JSON correspondence, including ++substitutions with both retained and recomputed served digests, without ++changing the native digest algorithm, request/PDP digest contract or API schema. ++Canonical integration/publication and the Platform mediator's final provider ++client source pin remain with the coordinator; preparation does not claim them. ++ ++Existing APPROVAL-WP-0004 closure and generated identifiers are untouched. ++APPROVAL-WP-0002-T01/T04/T05 remain existing live native acceptance obligations; ++none is promoted or completed here. No production auth, approval mutation, ++credential, deployment or backend operation was used. This completed low-risk ++local source repair introduces no residual native obligation beyond those ++existing records and the coordinator's integration of this exact artifact. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/owner-dirty-baseline.patch new file mode 100644 index 00000000..3064dd13 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/owner-dirty-baseline.patch @@ -0,0 +1,43 @@ +diff --git a/.custodian-brief.md b/.custodian-brief.md +index 4e8f8d3..ee8ca96 100644 +--- a/.custodian-brief.md ++++ b/.custodian-brief.md +@@ -2,7 +2,7 @@ + # Custodian Brief — approval-engine + + **Domain:** infotech +-**Last synced:** 2026-10-11 13:46 UTC ++**Last synced:** 2026-10-16 06:27 UTC + **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + + ## Active Workstreams +@@ -20,7 +20,7 @@ Progress: 3/6 done | workplan_id: `4fa25ad5-f5d0-5592-aa59-085f8ee3edaf` + + ## Inbox Hygiene + +-**Stale unread:** 10 message(s) older than 3 day(s) — triage at session start. ++**Stale unread:** 11 message(s) older than 3 day(s) — triage at session start. + **Missing thread_id:** 9 unread message(s) lack supersession chains. + - ! informed-decision: INFD-WP-0005: service-requested approvals from informed-decision intake (two small asks) `59c5ee82` + - ! flex-auth: FLEX-WP-0020 handoff request (consumer) `2523510b` +diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md +index 2110857..f7cd8df 100644 +--- a/WORK-RECORDS.md ++++ b/WORK-RECORDS.md +@@ -13,6 +13,7 @@ + | workplan | APPROVAL-WP-0001 | finished | — | workplans/APPROVAL-WP-0001-v07-alignment-and-engine-spine.md | + | workplan | APPROVAL-WP-0002 | blocked | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | workplan | APPROVAL-WP-0003 | finished | — | workplans/APPROVAL-WP-0003-reissue-expired-approvals.md | ++| workplan | APPROVAL-WP-0004 | finished | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | + | task | APPROVAL-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | + | task | APPROVAL-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | + | task | APPROVAL-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | +@@ -32,6 +33,8 @@ + | task | APPROVAL-WP-0002-T05 | wait | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | task | APPROVAL-WP-0002-T06 | done | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | task | APPROVAL-WP-0003-T01 | done | — | workplans/APPROVAL-WP-0003-reissue-expired-approvals.md | ++| task | APPROVAL-WP-0004-T01 | done | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | ++| task | APPROVAL-WP-0004-T02 | done | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | + | intake | APPROVAL-IN-0001 | open | blue | intakes/intakes.md | + | intake | APPROVAL-IN-0002 | closed | blue | intakes/intakes.md | + | intake | APPROVAL-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/patch-check.json b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/patch-check.json new file mode 100644 index 00000000..9fd208f6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/patch-check.json @@ -0,0 +1,5 @@ +{ + "exit_code": 0, + "stderr": "", + "pending_WP0004_closure_and_UUIDs_preserved": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/progress-receipt.json new file mode 100644 index 00000000..6d2603a3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf", + "id": "03ffe09e-b794-43c8-8010-9a55425a4351", + "topic_id": null, + "task_id": null, + "decision_id": null, + "event_type": "note", + "summary": "Approval owner bounded exact-confirmation source repair: reproduced Python-equal JSON-distinct target acceptance and unchecked served native digest, then reused native binding_digest recomputation and exact field correspondence.12 new cases;36confirmation tests and44focused tests passed. Intermediate200ms transport timing failure retained, focused retry passed. Patch/manifest under approval-confirmation; pending canonical integration/final Platform pin. Existing WP0004 closure/UUIDs and native WP0002 obligations unchanged; no live auth/effect.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T06:29:09.227024Z", + "workstream_id": "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/consumer-contract.md b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/consumer-contract.md new file mode 100644 index 00000000..40f70160 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/consumer-contract.md @@ -0,0 +1,135 @@ +# Thin Access Approval fact reader — owner source contract proposal + +Owner scope: Approval Engine / PIP. Work: APPROVAL-WP-0005-T01, selected source +subset of APPROVAL-WP-0002-T01/T05 under CUST-WP-0080. This interprets the existing +claim schema 0.1, not a new API/schema/signature or policy grant. Access's B2 +implementation stays DENY until its separately reviewed complete successor. + +## Authoritative source and guarantee + +Only server-installed, independently admitted provider origin and transport may +GET `/v1/approvals/{id}/claim`, with least-scope `approval:read` credential and +exact admitted store tenant. No caller URL/opener/key/verified flag, redirects, +environment proxy, caller JSON or public same-channel key bootstrap can supply +this provenance. Owner transport must enforce total deadline/size, exact 200 +application/json UTF-8, no encoding, duplicate-key/nonfinite/malformed JSON +refusal, no retry and bounded errors without bearer/body leakage. Provider +unavailability refuses this source join. Never invoke a credential-carrying +Python subprocess as the Go adapter; reuse Go's existing transport boundary. + +At the provider observation, `valid_now: true` AND `binding.human_control: true` +means the configured required count of distinct entries is met and every entry +has verified `principal_type=human`. `/entries` obtains identities/types only from +its authenticator, rejects non-human binds for this declaration, and persists +UNIQUE(approval_id, subject_id); `_evaluate` repeats the threshold/human check. +It does NOT mean required_count is two, requester/actor/principal is human, +directory-derived tenant membership, a particular MFA/assurance level, policy +eligibility, uncompromised source, native issuer admission, or continuing +validity after the observation. Consumer trusts this owner evaluation instead +of fetching identities/counting approvers or revalidating human JWTs. Native +issuer/profile acceptance is still T01, not established by synthetic vectors. + +## Minimal typed fact validation + +The submitted claim and fetched observation must each satisfy existing schema +0.1 with exact JSON types; `human_control` must be present and exactly true on +this lane despite being historically optional in the generic schema. Require +schema_version=0.1, kind=approval-claim, issuer=approval-engine, exact expected +approval UUID, state=valid, valid_now=true, consumed=false, reason_code=ok, +pdp_path=true and non-null lowercase sha256 pdp_digest. Reject decision-shaped +keys and unknown schema members (except optional informational yields_to). +The signed/requesting service must never become a human approver by inference. + +At one captured UTC validation instant now, require zoned RFC3339 timestamps, +positive integral ttl_seconds, observed_at<=now&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}", + "binding": { + "action": "secrets.kv.destroy", + "target": { + "html": "<>&", + "unicode": "é雪😀", + "separator": "

" + }, + "actor": "agt-secrets-engine", + "principal": "bernd", + "purpose": "rotate-exposed-key" + }, + "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}", + "native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea" + }, + { + "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", + "binding": { + "action": "secrets.kv.destroy", + "target": { + "b": [ + true, + 1, + 1.0, + null, + { + "z": "x", + "a": "y" + } + ], + "a": {} + }, + "actor": "agt-secrets-engine", + "principal": "bernd", + "purpose": "rotate-exposed-key" + }, + "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}", + "native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51" + } + ], + "transport_cases": [ + { + "name": "duplicate_valid_now", + "status": 200, + "content_type": "application/json", + "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\",\"valid_now\":false}", + "expected_accept": false + }, + { + "name": "nonfinite_json", + "status": 200, + "content_type": "application/json", + "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":NaN,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", + "expected_accept": false + }, + { + "name": "redirect", + "status": 302, + "location": "https://foreign.example/claim", + "expected_accept": false, + "required_secondary_requests": 0 + }, + { + "name": "wrong_media_type", + "status": 200, + "content_type": "text/plain", + "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", + "expected_accept": false + }, + { + "name": "provider_unavailable", + "status": 503, + "expected_accept": false + }, + { + "name": "declared_body_over_limit", + "status": 200, + "content_length": 1048577, + "consumer_limit_bytes": 1048576, + "expected_accept": false + }, + { + "name": "environment_proxy_configured", + "required_environment_proxy_requests": 0, + "expected_accept": false, + "note": "If fixed origin is unavailable, proxy cannot supply a replacement fact." + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/implementation.patch new file mode 100644 index 00000000..88a03ad7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/implementation.patch @@ -0,0 +1,2593 @@ +diff --git a/docs/access-claim-fact-reader.md b/docs/access-claim-fact-reader.md +new file mode 100644 +index 0000000..40f7016 +--- /dev/null ++++ b/docs/access-claim-fact-reader.md +@@ -0,0 +1,135 @@ ++# Thin Access Approval fact reader — owner source contract proposal ++ ++Owner scope: Approval Engine / PIP. Work: APPROVAL-WP-0005-T01, selected source ++subset of APPROVAL-WP-0002-T01/T05 under CUST-WP-0080. This interprets the existing ++claim schema 0.1, not a new API/schema/signature or policy grant. Access's B2 ++implementation stays DENY until its separately reviewed complete successor. ++ ++## Authoritative source and guarantee ++ ++Only server-installed, independently admitted provider origin and transport may ++GET `/v1/approvals/{id}/claim`, with least-scope `approval:read` credential and ++exact admitted store tenant. No caller URL/opener/key/verified flag, redirects, ++environment proxy, caller JSON or public same-channel key bootstrap can supply ++this provenance. Owner transport must enforce total deadline/size, exact 200 ++application/json UTF-8, no encoding, duplicate-key/nonfinite/malformed JSON ++refusal, no retry and bounded errors without bearer/body leakage. Provider ++unavailability refuses this source join. Never invoke a credential-carrying ++Python subprocess as the Go adapter; reuse Go's existing transport boundary. ++ ++At the provider observation, `valid_now: true` AND `binding.human_control: true` ++means the configured required count of distinct entries is met and every entry ++has verified `principal_type=human`. `/entries` obtains identities/types only from ++its authenticator, rejects non-human binds for this declaration, and persists ++UNIQUE(approval_id, subject_id); `_evaluate` repeats the threshold/human check. ++It does NOT mean required_count is two, requester/actor/principal is human, ++directory-derived tenant membership, a particular MFA/assurance level, policy ++eligibility, uncompromised source, native issuer admission, or continuing ++validity after the observation. Consumer trusts this owner evaluation instead ++of fetching identities/counting approvers or revalidating human JWTs. Native ++issuer/profile acceptance is still T01, not established by synthetic vectors. ++ ++## Minimal typed fact validation ++ ++The submitted claim and fetched observation must each satisfy existing schema ++0.1 with exact JSON types; `human_control` must be present and exactly true on ++this lane despite being historically optional in the generic schema. Require ++schema_version=0.1, kind=approval-claim, issuer=approval-engine, exact expected ++approval UUID, state=valid, valid_now=true, consumed=false, reason_code=ok, ++pdp_path=true and non-null lowercase sha256 pdp_digest. Reject decision-shaped ++keys and unknown schema members (except optional informational yields_to). ++The signed/requesting service must never become a human approver by inference. ++ ++At one captured UTC validation instant now, require zoned RFC3339 timestamps, ++positive integral ttl_seconds, observed_at<=now&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "html": "<>&", ++ "unicode": "é雪😀", ++ "separator": "

" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}", ++ "native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea" ++ }, ++ { ++ "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "b": [ ++ true, ++ 1, ++ 1.0, ++ null, ++ { ++ "z": "x", ++ "a": "y" ++ } ++ ], ++ "a": {} ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}", ++ "native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51" ++ } ++ ], ++ "transport_cases": [ ++ { ++ "name": "duplicate_valid_now", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\",\"valid_now\":false}", ++ "expected_accept": false ++ }, ++ { ++ "name": "nonfinite_json", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":NaN,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "redirect", ++ "status": 302, ++ "location": "https://foreign.example/claim", ++ "expected_accept": false, ++ "required_secondary_requests": 0 ++ }, ++ { ++ "name": "wrong_media_type", ++ "status": 200, ++ "content_type": "text/plain", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "provider_unavailable", ++ "status": 503, ++ "expected_accept": false ++ }, ++ { ++ "name": "declared_body_over_limit", ++ "status": 200, ++ "content_length": 1048577, ++ "consumer_limit_bytes": 1048576, ++ "expected_accept": false ++ }, ++ { ++ "name": "environment_proxy_configured", ++ "required_environment_proxy_requests": 0, ++ "expected_accept": false, ++ "note": "If fixed origin is unavailable, proxy cannot supply a replacement fact." ++ } ++ ] ++} +diff --git a/tests/test_access_claim_vectors.py b/tests/test_access_claim_vectors.py +new file mode 100644 +index 0000000..72e5191 +--- /dev/null ++++ b/tests/test_access_claim_vectors.py +@@ -0,0 +1,42 @@ ++"""Producer facts/canonical vectors only; not a Go consumer acceptance test.""" ++import json ++from pathlib import Path ++ ++import jsonschema ++ ++from approval_engine.binding import binding_digest, canonical_binding, canonical_json ++from approval_engine.store import Engine ++from tests.conftest import FROZEN, binding, validity ++ ++ROOT = Path(__file__).resolve().parents[1] ++VECTORS = json.loads((ROOT / "examples/access-claim-reader-vectors.json").read_text()) ++ ++ ++def test_native_vectors_match_actual_producer_bytes_and_digest(): ++ for case in VECTORS["native_binding_vectors"]: ++ act = binding(target=json.loads(case["input_target_json"])) ++ assert act == case["binding"] ++ assert canonical_json(canonical_binding(act)).decode() == case["canonical_binding_utf8"] ++ assert binding_digest(act) == case["native_digest"] ++ digests = [v["native_digest"] for v in VECTORS["native_binding_vectors"]] ++ assert len(set(digests)) == len(digests) ++ ++ ++def test_positive_claim_vector_matches_actual_provider_and_schema(): ++ case = VECTORS["claim_cases"][0] ++ engine = Engine(clock=lambda: FROZEN) ++ obj = engine.create(binding(), validity(), approval_id=case["submitted_claim"]["approval_id"], ++ required_count=2, pdp_digest=case["expected_pdp_digest"], ++ pdp_path=True, human_control=True) ++ engine.add_entry(obj.id, "human:one", principal_type="human") ++ assert engine.claim(obj.id)["valid_now"] is False ++ engine.add_entry(obj.id, "human:two", principal_type="human") ++ assert engine.claim(obj.id) == case["submitted_claim"] ++ from datetime import timedelta ++ engine.clock = lambda: FROZEN + timedelta(seconds=5) ++ assert engine.claim(obj.id) == case["observed_claim"] ++ assert case["submitted_claim"]["freshness"] != case["observed_claim"]["freshness"] ++ schema = json.loads((ROOT / "schemas/approval_claim.schema.json").read_text()) ++ jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker()).validate( ++ case["observed_claim"]) ++ engine.close() +diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +index 856ebc7..2a95d0d 100644 +--- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md ++++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +@@ -26,6 +26,12 @@ state_hub_workstream_id: "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf" + + # APPROVAL-WP-0002 — Production readiness and consumer adoption + ++2026-10-16 additional scoped demand before Access B2 implementation: ++CUST-WP-0080 selects T01/T05's authoritative claim/human-control/freshness and ++exact-source consumer contract review and producer fixtures only, tracked in ++APPROVAL-WP-0005. Existing residual/native statuses and authority gates remain; ++this does not activate other residual work, a consumer policy or live transport. ++ + Move the completed first-cut engine spine into an authenticated, durable, + observable production service and prove one PEP integration end to end. This is + the residual production scope deliberately excluded from APPROVAL-WP-0001. +diff --git a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +new file mode 100644 +index 0000000..777a9e6 +--- /dev/null ++++ b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +@@ -0,0 +1,49 @@ ++--- ++id: APPROVAL-WP-0005 ++type: workplan ++title: "Review thin Access claim fact reader contract and producer vectors" ++domain: infotech ++repo: approval-engine ++status: active ++flavor: planning ++owner: codex ++topic_slug: netkingdom ++created: "2026-10-16" ++updated: "2026-10-16" ++depends_on: [] ++--- ++ ++CUST-WP-0080 explicitly selects the necessary Approval-WP-0002-T01/T05 source ++contract subset for the Access service HTTP B2 join. Scope is owner review and ++producer fixtures only, before any consumer implementation. No broad residual ++activation, runtime client, trust admission, policy eligibility or live action. ++ ++## Review authoritative claim guarantees and publish thin consumer vectors ++ ++```task ++id: APPROVAL-WP-0005-T01 ++status: done ++priority: high ++``` ++ ++INTENT basis: factual exact-bound approval claims supplied to Access, never a ++second PDP. Source contract documents verified human-control semantics and their ++issuer admission limit, both original/subsequent observation bounds, exact ++native/PDP correspondence and immutable original submitted context. Producer ++vectors cover current lifecycle and canonical types. The superseded proposal's ++request-refresh design requires change; fresh facts are verified out-of-band. ++ ++## Independently integrate owner contract and coordinate Access reader conformance ++ ++```task ++id: APPROVAL-WP-0005-T02 ++status: todo ++priority: high ++``` ++ ++Coordinator reviews/integrates these source records. Access owns the thin Go ++reader/normalization and conformance implementation under FLEX-WP-0024-T06; ++Secrets owns strict immutable handoff and Platform owns provider origin/custody ++and final client pin. Native issuer/profile and served-PEP obligations remain ++live under APPROVAL-WP-0002-T01/T05, with audit under T04. No new native residual ++is created, promoted or marked complete by these source-only fixtures. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/owner-review.json b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/owner-review.json new file mode 100644 index 00000000..bc5bad13 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/owner-review.json @@ -0,0 +1,15 @@ +{ + "reviewer": "approval-engine-owner (agent approval-engine, codex)", + "decision": "changes_requested", + "comment": "INTENT Purpose and exact binding/claim boundary: B2 can reuse existing GET claim schema0.1 and verified human-control predicate without republishing approver identities. Superseded B3 refreshed evaluated-request/context design cannot satisfy the exact original strict handoff; retain original context/full and approval-free digests and verify fresh provider observation out-of-band. Work APPROVAL-WP-0005-T01; source base eece796b8bbee19551f6aa07ad389cbf83cabcdb. No policy activation, issuer admission, foreign owner assent or native acceptance.", + "reviewed_artifact": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-source/service-http-join-proposal-r1.json", + "reviewed_sha256": "e0f8f12c48faf2331cf366591028577beffe617250d7005dcaf2a8b7396535fc", + "owner_contract": "docs/access-claim-fact-reader.md", + "owner_contract_sha256": "3dfb7987a4d9fe461b917cfa5bae7beef61bc4d9a4561fdded3e90128f5966de", + "required_returns": [ + "Access own B2 contract/conformance review and exact native canonicalization parity; original submitted context/digests retained", + "Owner-admitted read identity/store tenant and trusted provider transport separate from source fixture tests", + "Actual runtime source integration and original consumer/journal/cas path before any policy eligibility successor" + ], + "native_accepted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/progress-receipt.json new file mode 100644 index 00000000..21d08403 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf", + "id": "c60f783b-47af-4d98-9495-686b59bc6619", + "topic_id": null, + "task_id": null, + "decision_id": null, + "event_type": "note", + "summary": "Approval owner reviewed authoritative Access B2 claim contract under scoped WP0002 T01/T05 demand, prepared WP0005 source review and producer vectors. Original submitted context and freshness remain intact; fresh source checked out-of-band. Distinct verified human guarantee and native issuer limits documented. 44 source tests passed; 24 claim cases, 14 native Python canonical vectors and 7 transport refusal vectors delivered; Go conformance and canonical integration pending. No live auth, policy activation or native acceptance. Artifacts approval-fact-contract.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T07:44:18.335488Z", + "workstream_id": "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/validation.json b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/validation.json new file mode 100644 index 00000000..21b9d69f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-fact-contract/validation.json @@ -0,0 +1,13 @@ +{ + "command": "python3 -m pytest -q tests/test_access_claim_vectors.py tests/test_human_control.py tests/test_claim_contract.py tests/test_tenant_provenance.py", + "passed": 44, + "claim_cases": 24, + "native_canonical_vectors": 14, + "transport_refusal_vectors": 7, + "native_canonical_vector_test": "actual existing producer bytes/digests verified", + "claim_vector_test": "positive differential observations reproduced against actual Engine; signed human-control/provenance regressions passed", + "consumer_go_conformance": "pending Access implementation; negative fixture expectations are contract, not executed Go acceptance", + "patch_check_exit": 0, + "canonical_source_mutated": false, + "native_accepted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/README.md b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/README.md new file mode 100644 index 00000000..b5dd78c1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/README.md @@ -0,0 +1,52 @@ +# Approval-owned Go fact data adapter + +Source proposal under APPROVAL-WP-0005-T03. Base owner revision: +`eece796b8bbee19551f6aa07ad389cbf83cabcdb`. Worktree: +`/tmp/netkingdom-approval-go-fact-20261016`. Canonical source/dirt is untouched. + +Access agreed the exact module/API before implementation: +`github.com/netkingdom/approval-engine/adapters/go/approvalfact` exports +CanonicalBinding(raw JSON), ValidateClaim(raw JSON, Expected, now), and +MatchObserved(original, fetched). Fact exposes detached bytes, observation and +deadline/native digest only; it is validated data, never authenticated origin. +No credential transport, subprocess runtime, evaluator, consume or service was +added. Existing Access Go1.26.7 is used. + +The Python owner primitive remains authoritative. The Go adapter reproduces +its native five-field JSON bytes, including bool/int/float identity, finite +float exponent thresholds/padding, negative floating zero, arbitrary integer +magnitudes, sorted Unicode keys, HTML and U+2028/U+2029 without Go escaping. +Strict parsing rejects duplicates, invalid Unicode/surrogates and nonfinite +values. Explicit 1MiB/128-depth/nanosecond-time bounds fail closed; no numeric +domain restriction was invented to simplify conformance. + +Validation: 14 native vectors and 24 claim cases pass; Go race and vet pass. +The actual Python source differential corpus matches 14,114 exact canonical +byte/digest results, including random finite IEEE patterns, float boundaries, +subnormals, extrema, integer and Unicode cases. Existing Python human-control, +claim and provenance checks: 44 passed. This is tested source correspondence, +not exhaustive formal proof, authenticated origin or native acceptance. +Initial toolchain-selection/cache failures are retained in validation.json. + +Two patch forms are provided because the initial fact-contract package was +still pending integration when this source was prepared: + +- `implementation.patch` contains the pending fact-contract package plus this + Go adapter, and applies directly to the current canonical owner revision. +- `incremental-after-fact-contract.patch` contains only this next source slice; + apply after approval-fact-contract/implementation.patch. Do not apply both. + +Both apply-checks pass; exact hashes and every source file hash are in +manifest.json. The local pending-contract commit is only a temporary preparation +anchor, not a published owner commit. Workplan T02 remains open for coordinator +review/integration/handoff, and native WP0002 T01/T04/T05 gates remain unchanged. + +Access owns source transport authentication and the wrapper inaccessible to HTTP +callers. Expected native raw act must come from authoritative owner/compiler and +actual normalization. Access's existing float64-decoded maps cannot supply it. +The owner identified a current pilot route preserving raw request.resource only +when it proves no active target registry enrichment and applies exact existing +tenant normalization. Future overrides need authoritative raw enrichment source; +unsupported correspondence refuses. Helper success must not activate policy or +substitute a supplied digest for recomputation. Independently review and pin the +final module sources before Access reader/engine integration and native proof. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/implementation.patch new file mode 100644 index 00000000..4064b925 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/implementation.patch @@ -0,0 +1,5744 @@ +diff --git a/adapters/go/approvalfact/README.md b/adapters/go/approvalfact/README.md +new file mode 100644 +index 0000000..fdffd1f +--- /dev/null ++++ b/adapters/go/approvalfact/README.md +@@ -0,0 +1,55 @@ ++# Approval fact data adapter ++ ++Provider-owned pure Go module for Access's Approval B2 source reader. No network, ++credentials, subprocess, policy evaluator, server or consume mutation exists here. ++Python `approval_engine.binding` remains the authoritative native format. ++ ++API: ++ ++- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes ++ and sha256 digest; extra binding metadata is excluded. ++- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path ++ claim against independently owner-derived `Expected.NativeBindingJSON`, exact ++ approval ID and actual Access approval-free PDP digest. ++- `MatchObserved(submitted, observed)` requires two validated facts at the same ++ captured instant and identical nonfreshness authority facts. No request is ++ rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines ++ preserve each input's own freshness and approval expiry. ++ ++`Fact` means validated data, never authenticated provider origin. Access must ++fetch provider bytes through its independently admitted fixed origin/identity, ++then wrap this data in its own unforgeable source handle. A caller-supplied claim ++cannot become source truth merely by passing this helper. Access remains the ++single evaluator and must bound decision/operation/caller lifetimes separately. ++PEP consume CAS and live one-shot journal remain required before any effect. ++ ++Expected native act bytes must preserve the actual owner/compiler/normalization ++representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and ++large integers. Raw submitted resource is sufficient only when Access proves ++the active registry cannot override that target and applies its exact existing ++normalization. Otherwise authoritative raw enrichment/compiler evidence is ++needed. This helper supplies no fallback or invented restricted numeric domain. ++ ++The strict parser retains integer/floating lexical types, arbitrary integer ++magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values. ++Canonical float/string formatting follows Python source, including negative ++floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate ++object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. ++Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at ++most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++These explicit parser/resource limits fail closed, never normalize authority. ++ ++`go test -race ./...` checks the portable producer vectors, typed fact pairing, ++strict JSON/Unicode and exact deadline semantics. From the owner repo root, ++`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case ++development-only differential corpus against the actual Python producer, ++including random finite IEEE bit patterns, exponent boundaries/subnormals and ++Unicode. It uses a test-only local oracle file; production imports no oracle or ++Python runtime. Sample conformance is evidence, not a formal proof over all JSON. ++ ++The module pins Go 1.26.7 to match the existing Access consumer toolchain. ++`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner ++fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`. ++Transport cases requiring HTTP remain the Access adapter's tests; this module ++checks their duplicate/nonfinite bodies only. Import/deployment/native identity ++admission and positive policy gates require separate owner evidence. +diff --git a/adapters/go/approvalfact/canonical.go b/adapters/go/approvalfact/canonical.go +new file mode 100644 +index 0000000..06f14b5 +--- /dev/null ++++ b/adapters/go/approvalfact/canonical.go +@@ -0,0 +1,295 @@ ++// Package approvalfact provides data-only Approval claim correspondence. ++// It neither authenticates a provider nor decides permission or consumes objects. ++package approvalfact ++ ++import ( ++ "bytes" ++ "crypto/sha256" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "math" ++ "math/big" ++ "sort" ++ "strconv" ++ "strings" ++ "unicode/utf8" ++) ++ ++const MaxJSONBytes = 1 << 20 ++ ++var errJSON = errors.New("invalid Approval JSON") ++ ++// DecodeJSON retains numeric lexical types, refuses duplicates and invalid ++// Unicode, and bounds work. A Go map previously decoded into float64 cannot ++// recover the producer's integer-versus-floating identity; pass source bytes. ++func DecodeJSON(raw []byte) (any, error) { ++ if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) { ++ return nil, errJSON ++ } ++ d := json.NewDecoder(bytes.NewReader(raw)) ++ d.UseNumber() ++ var read func(int) (any, error) ++ read = func(depth int) (any, error) { ++ if depth > 128 { ++ return nil, errJSON ++ } ++ t, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ switch x := t.(type) { ++ case json.Delim: ++ if x == '{' { ++ m := map[string]any{} ++ for d.More() { ++ k, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ s, ok := k.(string) ++ if !ok { ++ return nil, errJSON ++ } ++ if _, ok = m[s]; ok { ++ return nil, errJSON ++ } ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ m[s] = v ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim('}') { ++ return nil, errJSON ++ } ++ return m, nil ++ } ++ if x == '[' { ++ a := []any{} ++ for d.More() { ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ a = append(a, v) ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim(']') { ++ return nil, errJSON ++ } ++ return a, nil ++ } ++ return nil, errJSON ++ case json.Number: ++ if _, e := pythonNumber(x); e != nil { ++ return nil, e ++ } ++ return x, nil ++ case string, bool, nil: ++ return x, nil ++ default: ++ return nil, errJSON ++ } ++ } ++ v, e := read(0) ++ if e != nil { ++ return nil, e ++ } ++ if _, e = d.Token(); e != io.EOF { ++ return nil, errJSON ++ } ++ return v, nil ++} ++ ++// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer ++// cannot encode them as UTF-8. Reject before the decoder can erase that fact. ++func validSurrogates(raw []byte) bool { ++ in := false ++ for i := 0; i < len(raw); i++ { ++ if raw[i] == '"' { ++ in = !in ++ continue ++ } ++ if !in || raw[i] != '\\' { ++ continue ++ } ++ i++ ++ if i >= len(raw) { ++ return false ++ } ++ if raw[i] != 'u' { ++ continue ++ } ++ if i+4 >= len(raw) { ++ return false ++ } ++ n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16) ++ if e != nil { ++ return false ++ } ++ i += 4 ++ if n >= 0xdc00 && n <= 0xdfff { ++ return false ++ } ++ if n < 0xd800 || n > 0xdbff { ++ continue ++ } ++ if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' { ++ return false ++ } ++ low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16) ++ if e != nil || low < 0xdc00 || low > 0xdfff { ++ return false ++ } ++ i += 6 ++ } ++ return !in ++} ++ ++func pythonNumber(n json.Number) (string, error) { ++ s := string(n) ++ if !strings.ContainsAny(s, ".eE") { ++ i, ok := new(big.Int).SetString(s, 10) ++ if !ok { ++ return "", errJSON ++ } ++ return i.String(), nil ++ } ++ f, e := strconv.ParseFloat(s, 64) ++ if e != nil || math.IsInf(f, 0) || math.IsNaN(f) { ++ return "", errJSON ++ } ++ // Python repr uses shortest round-trip digits, fixed form for exponents ++ // -4..15, a mandatory .0 for integral floats, and two exponent digits. ++ sci := strconv.FormatFloat(f, 'e', -1, 64) ++ at := strings.IndexByte(sci, 'e') ++ exp, _ := strconv.Atoi(sci[at+1:]) ++ if exp >= -4 && exp < 16 { ++ s = strconv.FormatFloat(f, 'f', -1, 64) ++ if !strings.Contains(s, ".") { ++ s += ".0" ++ } ++ return s, nil ++ } ++ sign := "+" ++ if exp < 0 { ++ sign = "-" ++ exp = -exp ++ } ++ digits := strconv.Itoa(exp) ++ if len(digits) < 2 { ++ digits = "0" + digits ++ } ++ return sci[:at] + "e" + sign + digits, nil ++} ++ ++func quote(s string) string { ++ var b strings.Builder ++ b.WriteByte('"') ++ for _, r := range s { ++ switch r { ++ case '"', '\\': ++ b.WriteByte('\\') ++ b.WriteRune(r) ++ case '\b': ++ b.WriteString(`\b`) ++ case '\f': ++ b.WriteString(`\f`) ++ case '\n': ++ b.WriteString(`\n`) ++ case '\r': ++ b.WriteString(`\r`) ++ case '\t': ++ b.WriteString(`\t`) ++ default: ++ if r < 32 { ++ b.WriteString(`\u00`) ++ const h = "0123456789abcdef" ++ b.WriteByte(h[byte(r)>>4]) ++ b.WriteByte(h[byte(r)&15]) ++ } else { ++ b.WriteRune(r) ++ } ++ } ++ } ++ b.WriteByte('"') ++ return b.String() ++} ++ ++func canonical(v any) (string, error) { ++ switch x := v.(type) { ++ case nil: ++ return "null", nil ++ case bool: ++ if x { ++ return "true", nil ++ } ++ return "false", nil ++ case string: ++ return quote(x), nil ++ case json.Number: ++ return pythonNumber(x) ++ case []any: ++ a := make([]string, len(x)) ++ for i, v := range x { ++ s, e := canonical(v) ++ if e != nil { ++ return "", e ++ } ++ a[i] = s ++ } ++ return "[" + strings.Join(a, ",") + "]", nil ++ case map[string]any: ++ keys := make([]string, 0, len(x)) ++ for k := range x { ++ keys = append(keys, k) ++ } ++ sort.Strings(keys) ++ a := make([]string, 0, len(keys)) ++ for _, k := range keys { ++ s, e := canonical(x[k]) ++ if e != nil { ++ return "", e ++ } ++ a = append(a, quote(k)+":"+s) ++ } ++ return "{" + strings.Join(a, ",") + "}", nil ++ default: ++ return "", errJSON ++ } ++} ++ ++// CanonicalBinding reproduces approval_engine.binding.binding_digest over the ++// five native act fields. Extra binding metadata is excluded by that contract. ++func CanonicalBinding(raw []byte) ([]byte, string, error) { ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return nil, "", e ++ } ++ m, ok := v.(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act := map[string]any{} ++ for _, k := range []string{"action", "actor", "principal", "purpose"} { ++ s, ok := m[k].(string) ++ if !ok || s == "" { ++ return nil, "", errJSON ++ } ++ act[k] = s ++ } ++ target, ok := m["target"].(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act["target"] = target ++ s, e := canonical(act) ++ if e != nil { ++ return nil, "", e ++ } ++ sum := sha256.Sum256([]byte(s)) ++ return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil ++} +diff --git a/adapters/go/approvalfact/claim.go b/adapters/go/approvalfact/claim.go +new file mode 100644 +index 0000000..169643f +--- /dev/null ++++ b/adapters/go/approvalfact/claim.go +@@ -0,0 +1,170 @@ ++package approvalfact ++ ++import ( ++ "encoding/json" ++ "errors" ++ "regexp" ++ "strconv" ++ "time" ++) ++ ++var errClaim = errors.New("Approval fact does not match required claim") ++var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) ++var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) ++var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`) ++ ++// Expected contains independently owner-derived native act bytes and the actual ++// Access approval-free digest. Marshaling an already float64-decoded Go request ++// is not an authoritative reconstruction of the native act. ++type Expected struct { ++ ApprovalID string ++ NativeBindingJSON []byte ++ PDPDigest string ++} ++ ++// Fact is validated data shape/correspondence, NOT authenticated provider origin ++// or execution authority. Access must wrap it only after its own source admission. ++type Fact struct { ++ raw []byte ++ stable string ++ digest string ++ observed, timeLimit, validatedAt time.Time ++} ++ ++func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) } ++func (f Fact) ObservedAt() time.Time { return f.observed } ++func (f Fact) Deadline() time.Time { return f.timeLimit } ++func (f Fact) NativeDigest() string { return f.digest } ++ ++func members(m map[string]any, required, optional []string) bool { ++ allowed := map[string]bool{} ++ for _, k := range required { ++ allowed[k] = true ++ if _, ok := m[k]; !ok { ++ return false ++ } ++ } ++ for _, k := range optional { ++ allowed[k] = true ++ } ++ for k := range m { ++ if !allowed[k] { ++ return false ++ } ++ } ++ return true ++} ++func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok } ++func stamp(v any) (time.Time, error) { ++ s, ok := v.(string) ++ if !ok || !timePattern.MatchString(s) { ++ return time.Time{}, errClaim ++ } ++ t, e := time.Parse(time.RFC3339Nano, s) ++ if e != nil { ++ return time.Time{}, errClaim ++ } ++ return t, nil ++} ++ ++// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim ++// at a caller-captured instant, using the existing Approval producer semantics. ++// It has no I/O, policy evaluator, credentials, cache, or consume mutation. ++func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) { ++ if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) { ++ return Fact{}, errClaim ++ } ++ _, want, e := CanonicalBinding(expected.NativeBindingJSON) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ m, ok := object(v) ++ if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) { ++ return Fact{}, errClaim ++ } ++ if y, ok := m["yields_to"]; ok { ++ if _, ok = y.(string); !ok { ++ return Fact{}, errClaim ++ } ++ } ++ if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" { ++ return Fact{}, errClaim ++ } ++ b, ok := object(m["binding"]) ++ if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) { ++ return Fact{}, errClaim ++ } ++ if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true { ++ return Fact{}, errClaim ++ } ++ text, e := canonical(b) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ _, got, e := CanonicalBinding([]byte(text)) ++ if e != nil || got != want { ++ return Fact{}, errClaim ++ } ++ f, ok := object(m["freshness"]) ++ if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) { ++ return Fact{}, errClaim ++ } ++ observed, e := stamp(f["observed_at"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ notAfter, e := stamp(f["not_after"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ ttl, ok := f["ttl_seconds"].(json.Number) ++ if !ok { ++ return Fact{}, errClaim ++ } ++ seconds, e := strconv.ParseInt(string(ttl), 10, 64) ++ if e != nil || seconds < 1 { ++ return Fact{}, errClaim ++ } ++ // Bound arithmetic to time.Duration's representable seconds, not a new ++ // semantic freshness grant or configured TTL. Unsupported extremes refuse. ++ if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) { ++ return Fact{}, errClaim ++ } ++ validity, ok := object(m["validity"]) ++ if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) { ++ return Fact{}, errClaim ++ } ++ before, e := stamp(validity["not_before"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ expires, e := stamp(validity["expires_at"]) ++ if e != nil || now.Before(before) || !now.Before(expires) { ++ return Fact{}, errClaim ++ } ++ limit := notAfter ++ if expires.Before(limit) { ++ limit = expires ++ } ++ delete(m, "freshness") ++ delete(m, "yields_to") ++ stable, e := canonical(m) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil ++} ++ ++// MatchObserved requires independently validated original/fetched facts at the ++// same captured instant and exact nonfreshness authority correspondence. It ++// never rewrites the original request or extends its original deadline. ++func MatchObserved(submitted, observed Fact) error { ++ if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable { ++ return errClaim ++ } ++ return nil ++} +diff --git a/adapters/go/approvalfact/claim_test.go b/adapters/go/approvalfact/claim_test.go +new file mode 100644 +index 0000000..26623c8 +--- /dev/null ++++ b/adapters/go/approvalfact/claim_test.go +@@ -0,0 +1,144 @@ ++package approvalfact ++ ++import ( ++ "bytes" ++ "encoding/json" ++ "os" ++ "strings" ++ "testing" ++ "time" ++) ++ ++type vectors struct { ++ Native []struct { ++ Binding json.RawMessage `json:"binding"` ++ Canonical string `json:"canonical_binding_utf8"` ++ Digest string `json:"native_digest"` ++ } `json:"native_binding_vectors"` ++ Claims []struct { ++ Name string `json:"name"` ++ Now time.Time `json:"validation_time"` ++ Submitted json.RawMessage `json:"submitted_claim"` ++ Observed json.RawMessage `json:"observed_claim"` ++ Binding json.RawMessage `json:"expected_binding"` ++ PDP string `json:"expected_pdp_digest"` ++ Accept bool `json:"expected_accept"` ++ } `json:"claim_cases"` ++ Transport []struct { ++ Name string `json:"name"` ++ Body string `json:"body_utf8"` ++ } `json:"transport_cases"` ++} ++ ++func load(t *testing.T) vectors { ++ t.Helper() ++ raw, e := os.ReadFile("testdata/owner-vectors.json") ++ if e != nil { ++ t.Fatal(e) ++ } ++ var v vectors ++ if e = json.Unmarshal(raw, &v); e != nil { ++ t.Fatal(e) ++ } ++ return v ++} ++ ++func TestProducerCanonicalVectors(t *testing.T) { ++ for _, v := range load(t).Native { ++ t.Run(v.Digest, func(t *testing.T) { ++ raw, d, e := CanonicalBinding(v.Binding) ++ if e != nil || string(raw) != v.Canonical || d != v.Digest { ++ t.Fatalf("producer parity: %q %s %v, want %q %s", raw, d, e, v.Canonical, v.Digest) ++ } ++ }) ++ } ++} ++ ++func TestProducerClaimVectors(t *testing.T) { ++ for _, v := range load(t).Claims { ++ t.Run(v.Name, func(t *testing.T) { ++ var id struct { ++ ID string `json:"approval_id"` ++ } ++ if e := json.Unmarshal(v.Submitted, &id); e != nil { ++ t.Fatal(e) ++ } ++ expected := Expected{id.ID, v.Binding, v.PDP} ++ old, e1 := ValidateClaim(v.Submitted, expected, v.Now) ++ fresh, e2 := ValidateClaim(v.Observed, expected, v.Now) ++ accepted := e1 == nil && e2 == nil && MatchObserved(old, fresh) == nil ++ if accepted != v.Accept { ++ t.Fatalf("accepted=%v want=%v original=%v observed=%v", accepted, v.Accept, e1, e2) ++ } ++ if accepted { ++ raw := old.RawJSON() ++ if !bytes.Equal(raw, v.Submitted) { ++ t.Fatal("original bytes changed") ++ } ++ raw[0] = 'x' ++ if old.RawJSON()[0] != '{' { ++ t.Fatal("mutable fact storage") ++ } ++ if !old.Deadline().Before(fresh.Deadline()) { ++ t.Fatal("refresh must not extend original deadline") ++ } ++ } ++ }) ++ } ++} ++ ++func TestStrictJSONRefusals(t *testing.T) { ++ for _, raw := range []string{`{"a":1,"a":2}`, `{"a":{"x":1,"x":2}}`, `{"a":NaN}`, `{"a":1e999}`, `{"a":"\ud800"}`, `{"a":"\udfff"}`, `{"a":"\ud800x"}`, `{"a":1} {}`, `{"a":"` + string([]byte{0xff}) + `"}`, strings.Repeat("[", 130) + strings.Repeat("]", 130), strings.Repeat(" ", MaxJSONBytes+1)} { ++ if _, e := DecodeJSON([]byte(raw)); e == nil { ++ t.Fatalf("accepted malformed JSON %.80q", raw) ++ } ++ } ++} ++ ++func TestUnicodeAndControlEncoding(t *testing.T) { ++ for _, raw := range []string{`{"v":"\ud83d\ude00"}`, `{"v":"escaped \\\" quote"}`, `{"v":"\u0000\b\f\n\r\t"}`} { ++ if _, e := DecodeJSON([]byte(raw)); e != nil { ++ t.Fatalf("refused valid Unicode %q", raw) ++ } ++ } ++} ++ ++func TestGoldenDuplicateAndNonfiniteBodies(t *testing.T) { ++ for _, v := range load(t).Transport { ++ if v.Name == "duplicate_valid_now" || v.Name == "nonfinite_json" { ++ if _, e := DecodeJSON([]byte(v.Body)); e == nil { ++ t.Fatal(v.Name) ++ } ++ } ++ } ++} ++ ++func TestInvalidExpectedAndZeroFacts(t *testing.T) { ++ v := load(t).Claims[0] ++ if _, e := ValidateClaim(v.Submitted, Expected{}, v.Now); e == nil { ++ t.Fatal("missing expected act accepted") ++ } ++ if MatchObserved(Fact{}, Fact{}) == nil { ++ t.Fatal("zero facts accepted") ++ } ++ expected := Expected{"11111111-1111-4111-8111-111111111111", v.Binding, v.PDP} ++ a, e := ValidateClaim(v.Submitted, expected, v.Now) ++ if e != nil { ++ t.Fatal(e) ++ } ++ b, e := ValidateClaim(v.Observed, expected, v.Now.Add(time.Second)) ++ if e != nil { ++ t.Fatal(e) ++ } ++ if MatchObserved(a, b) == nil { ++ t.Fatal("different validation instants accepted") ++ } ++} ++ ++func TestTimestampPrecisionAndOffsetRefusals(t *testing.T) { ++ for _, value := range []string{"2026-08-29T12:00:00,1Z", "2026-08-29T12:00:00+24:00", "2026-08-29T12:00:00+00:60", "2026-08-29T12:00:00.0000000001Z", "2026-08-29T12:00:00"} { ++ if _, err := stamp(value); err == nil { ++ t.Fatalf("accepted unsupported timestamp %q", value) ++ } ++ } ++} +diff --git a/adapters/go/approvalfact/go.mod b/adapters/go/approvalfact/go.mod +new file mode 100644 +index 0000000..bcb1783 +--- /dev/null ++++ b/adapters/go/approvalfact/go.mod +@@ -0,0 +1,3 @@ ++module github.com/netkingdom/approval-engine/adapters/go/approvalfact ++ ++go 1.26.7 +diff --git a/adapters/go/approvalfact/oracle_test.go b/adapters/go/approvalfact/oracle_test.go +new file mode 100644 +index 0000000..b845388 +--- /dev/null ++++ b/adapters/go/approvalfact/oracle_test.go +@@ -0,0 +1,42 @@ ++package approvalfact ++ ++import ( ++ "bufio" ++ "encoding/json" ++ "os" ++ "testing" ++) ++ ++// This test-only input is produced by scripts/check_go_approvalfact.py using ++// the actual Python owner primitive. Production has no subprocess or oracle. ++func TestPythonDifferential(t *testing.T) { ++ path := os.Getenv("APPROVAL_TEST_ORACLE") ++ if path == "" { ++ t.Skip("run scripts/check_go_approvalfact.py for Python differential corpus") ++ } ++ f, e := os.Open(path) ++ if e != nil { ++ t.Fatal(e) ++ } ++ defer f.Close() ++ s := bufio.NewScanner(f) ++ count := 0 ++ for s.Scan() { ++ var v struct{ Raw, Canonical, Digest string } ++ if e = json.Unmarshal(s.Bytes(), &v); e != nil { ++ t.Fatal(e) ++ } ++ raw, d, e := CanonicalBinding([]byte(v.Raw)) ++ if e != nil || string(raw) != v.Canonical || d != v.Digest { ++ t.Fatalf("Python differential row %d: got %q %s %v want %q %s", count, raw, d, e, v.Canonical, v.Digest) ++ } ++ count++ ++ } ++ if e = s.Err(); e != nil { ++ t.Fatal(e) ++ } ++ if count < 10000 { ++ t.Fatalf("incomplete differential corpus: %d", count) ++ } ++ t.Logf("matched %d Python source canonical bytes and digests", count) ++} +diff --git a/adapters/go/approvalfact/testdata/owner-vectors.json b/adapters/go/approvalfact/testdata/owner-vectors.json +new file mode 100644 +index 0000000..81caf34 +--- /dev/null ++++ b/adapters/go/approvalfact/testdata/owner-vectors.json +@@ -0,0 +1,2305 @@ ++{ ++ "producer_revision": "eece796b8bbee19551f6aa07ad389cbf83cabcdb", ++ "fixture_only": true, ++ "source": "Engine.claim with fixed synthetic identities; mutated consumer refusal cases explicitly labeled; no native identity admission", ++ "schema_version": "0.1", ++ "claim_cases": [ ++ { ++ "name": "fresh_out_of_band_observation_unchanged_original", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": true, ++ "reason": "matching_valid_facts_original_digest_unchanged" ++ }, ++ { ++ "name": "requested", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "requested", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "requested" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "insufficient_distinct_humans", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "requested", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "requested" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "foreign_issuer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "foreign", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "foreign_id", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "22222222-2222-4222-8222-222222222222", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "nonhuman_control", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": false ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "legacy_missing_human_control", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "human_control_integer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": 1 ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_pdp_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:abababababababababababababababababababababababababababababababab", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "null_pdp_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": null, ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "undeclared_pdp_path", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": false, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "valid_now_integer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": 1, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_native_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:abababababababababababababababababababababababababababababababab", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_act", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "other", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "future_observation", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:06+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "inconsistent_ttl", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 31, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "zero_ttl", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 0, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "naive_observation", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "original_observation_expired_even_when_provider_fresh", ++ "validation_time": "2026-08-29T12:00:30+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "refetch_cannot_extend_original_freshness", ++ "validation_time": "2026-08-29T12:00:30+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:30+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:01:00+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "consumed_source", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "consumed", ++ "valid_now": false, ++ "consumed": true, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "consumed" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "expired_after_consumption_preserves_consumed_true", ++ "validation_time": "2026-08-30T12:00:00+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "expired", ++ "valid_now": false, ++ "consumed": true, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-30T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-30T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "expired" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "source_revoked", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "6302eb08-215e-4a17-8e6f-66717874c6af", ++ "state": "revoked", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "revoked" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "6302eb08-215e-4a17-8e6f-66717874c6af", ++ "state": "revoked", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "revoked" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "terminal_source_refusal" ++ }, ++ { ++ "name": "source_superseded", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "8c8f9c0f-b2dd-4a1f-964a-54ac7022803b", ++ "state": "superseded", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "superseded" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "8c8f9c0f-b2dd-4a1f-964a-54ac7022803b", ++ "state": "superseded", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "superseded" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "terminal_source_refusal" ++ } ++ ], ++ "native_binding_vectors": [ ++ { ++ "input_target_json": "{\"value\":true}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": true ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":true}}", ++ "native_digest": "sha256:e1f537daa849e5da023efb5f3d62cc33d94cce20f9ca9c5a708c76e225fd702f" ++ }, ++ { ++ "input_target_json": "{\"value\":1}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1}}", ++ "native_digest": "sha256:3a3d2da6267e4b917505a7c2247e6ce36fd8ee074eef37251bcc66839b602332" ++ }, ++ { ++ "input_target_json": "{\"value\":1.0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1.0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1.0}}", ++ "native_digest": "sha256:21c2cdb9b161071ebfe361e50703511b2abf6c6f327f153282d247aa92c3898e" ++ }, ++ { ++ "input_target_json": "{\"value\":false}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": false ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":false}}", ++ "native_digest": "sha256:278cbcd013f7923db5258dd860f911d688505a8259f15ccb77983b044f312c74" ++ }, ++ { ++ "input_target_json": "{\"value\":0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":0}}", ++ "native_digest": "sha256:e691b8fb55d4bf82a310dee37c045331100c8031e26ea6f51bdcceeeb3b1f20e" ++ }, ++ { ++ "input_target_json": "{\"value\":-0.0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": -0.0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":-0.0}}", ++ "native_digest": "sha256:eccde458c686bff67506bb2d94c750e648a311bf23967cb22b37f11e84b6844f" ++ }, ++ { ++ "input_target_json": "{\"value\":1e-7}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e-07 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-07}}", ++ "native_digest": "sha256:779042b6c0e675f6aacdcd3a02199aab66f534bb84dd410a95c18657afae563c" ++ }, ++ { ++ "input_target_json": "{\"value\":1e-6}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e-06 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-06}}", ++ "native_digest": "sha256:d021f402231fcd59e764b31e6f08a5298ebb328dfd93da58ecde8feb11fde0b5" ++ }, ++ { ++ "input_target_json": "{\"value\":1e20}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e+20 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+20}}", ++ "native_digest": "sha256:14270ffca4ab10e9d9302d52e988b4aa3e2facd494c12a60e2edafef70277a20" ++ }, ++ { ++ "input_target_json": "{\"value\":1e21}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e+21 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+21}}", ++ "native_digest": "sha256:75a60a595b2578430b0be466b6bf9a46b62ddc99bbbcebd7fa0e8d22e3078c0a" ++ }, ++ { ++ "input_target_json": "{\"value\":2.5e-7}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 2.5e-07 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":2.5e-07}}", ++ "native_digest": "sha256:50af37ff75a3e7a22629e746f814995c0c2cd96adb7f1ed5b26999b71d0ac498" ++ }, ++ { ++ "input_target_json": "{\"value\":9007199254740993}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 9007199254740993 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":9007199254740993}}", ++ "native_digest": "sha256:765360dc5a1d949bd114c6b749d83069123d8b4b6b1d939f831428a800f50dab" ++ }, ++ { ++ "input_target_json": "{\"html\":\"<>&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "html": "<>&", ++ "unicode": "é雪😀", ++ "separator": "

" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}", ++ "native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea" ++ }, ++ { ++ "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "b": [ ++ true, ++ 1, ++ 1.0, ++ null, ++ { ++ "z": "x", ++ "a": "y" ++ } ++ ], ++ "a": {} ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}", ++ "native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51" ++ } ++ ], ++ "transport_cases": [ ++ { ++ "name": "duplicate_valid_now", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\",\"valid_now\":false}", ++ "expected_accept": false ++ }, ++ { ++ "name": "nonfinite_json", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":NaN,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "redirect", ++ "status": 302, ++ "location": "https://foreign.example/claim", ++ "expected_accept": false, ++ "required_secondary_requests": 0 ++ }, ++ { ++ "name": "wrong_media_type", ++ "status": 200, ++ "content_type": "text/plain", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "provider_unavailable", ++ "status": 503, ++ "expected_accept": false ++ }, ++ { ++ "name": "declared_body_over_limit", ++ "status": 200, ++ "content_length": 1048577, ++ "consumer_limit_bytes": 1048576, ++ "expected_accept": false ++ }, ++ { ++ "name": "environment_proxy_configured", ++ "required_environment_proxy_requests": 0, ++ "expected_accept": false, ++ "note": "If fixed origin is unavailable, proxy cannot supply a replacement fact." ++ } ++ ] ++} +diff --git a/docs/access-claim-fact-reader.md b/docs/access-claim-fact-reader.md +new file mode 100644 +index 0000000..4669f64 +--- /dev/null ++++ b/docs/access-claim-fact-reader.md +@@ -0,0 +1,137 @@ ++# Thin Access Approval fact reader — owner source contract proposal ++ ++Owner scope: Approval Engine / PIP. Work: APPROVAL-WP-0005-T01, selected source ++subset of APPROVAL-WP-0002-T01/T05 under CUST-WP-0080. This interprets the existing ++claim schema 0.1, not a new API/schema/signature or policy grant. Access's B2 ++implementation stays DENY until its separately reviewed complete successor. ++ ++## Authoritative source and guarantee ++ ++Only server-installed, independently admitted provider origin and transport may ++GET `/v1/approvals/{id}/claim`, with least-scope `approval:read` credential and ++exact admitted store tenant. No caller URL/opener/key/verified flag, redirects, ++environment proxy, caller JSON or public same-channel key bootstrap can supply ++this provenance. Owner transport must enforce total deadline/size, exact 200 ++application/json UTF-8, no encoding, duplicate-key/nonfinite/malformed JSON ++refusal, no retry and bounded errors without bearer/body leakage. Provider ++unavailability refuses this source join. Never invoke a credential-carrying ++Python subprocess as the Go adapter; reuse Go's existing transport boundary. ++ ++At the provider observation, `valid_now: true` AND `binding.human_control: true` ++means the configured required count of distinct entries is met and every entry ++has verified `principal_type=human`. `/entries` obtains identities/types only from ++its authenticator, rejects non-human binds for this declaration, and persists ++UNIQUE(approval_id, subject_id); `_evaluate` repeats the threshold/human check. ++It does NOT mean required_count is two, requester/actor/principal is human, ++directory-derived tenant membership, a particular MFA/assurance level, policy ++eligibility, uncompromised source, native issuer admission, or continuing ++validity after the observation. Consumer trusts this owner evaluation instead ++of fetching identities/counting approvers or revalidating human JWTs. Native ++issuer/profile acceptance is still T01, not established by synthetic vectors. ++ ++## Minimal typed fact validation ++ ++The submitted claim and fetched observation must each satisfy existing schema ++0.1 with exact JSON types; `human_control` must be present and exactly true on ++this lane despite being historically optional in the generic schema. Require ++schema_version=0.1, kind=approval-claim, issuer=approval-engine, exact expected ++approval UUID, state=valid, valid_now=true, consumed=false, reason_code=ok, ++pdp_path=true and non-null lowercase sha256 pdp_digest. Reject decision-shaped ++keys and unknown schema members (except optional informational yields_to). ++The signed/requesting service must never become a human approver by inference. ++ ++At one captured UTC validation instant now, require zoned RFC3339 timestamps, ++positive integral ttl_seconds, observed_at<=now&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "html": "<>&", ++ "unicode": "é雪😀", ++ "separator": "

" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}", ++ "native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea" ++ }, ++ { ++ "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "b": [ ++ true, ++ 1, ++ 1.0, ++ null, ++ { ++ "z": "x", ++ "a": "y" ++ } ++ ], ++ "a": {} ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}", ++ "native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51" ++ } ++ ], ++ "transport_cases": [ ++ { ++ "name": "duplicate_valid_now", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\",\"valid_now\":false}", ++ "expected_accept": false ++ }, ++ { ++ "name": "nonfinite_json", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":NaN,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "redirect", ++ "status": 302, ++ "location": "https://foreign.example/claim", ++ "expected_accept": false, ++ "required_secondary_requests": 0 ++ }, ++ { ++ "name": "wrong_media_type", ++ "status": 200, ++ "content_type": "text/plain", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "provider_unavailable", ++ "status": 503, ++ "expected_accept": false ++ }, ++ { ++ "name": "declared_body_over_limit", ++ "status": 200, ++ "content_length": 1048577, ++ "consumer_limit_bytes": 1048576, ++ "expected_accept": false ++ }, ++ { ++ "name": "environment_proxy_configured", ++ "required_environment_proxy_requests": 0, ++ "expected_accept": false, ++ "note": "If fixed origin is unavailable, proxy cannot supply a replacement fact." ++ } ++ ] ++} +diff --git a/scripts/check_go_approvalfact.py b/scripts/check_go_approvalfact.py +new file mode 100644 +index 0000000..c71f576 +--- /dev/null ++++ b/scripts/check_go_approvalfact.py +@@ -0,0 +1,53 @@ ++"""Development-only Go parity check against the actual Python producer.""" ++from pathlib import Path ++import json ++import math ++import os ++import random ++import struct ++import subprocess ++import sys ++import tempfile ++ ++ROOT = Path(__file__).resolve().parents[1] ++sys.path.insert(0, str(ROOT)) ++from approval_engine.binding import binding_digest, canonical_binding, canonical_json ++ ++ ++def main(): ++ rng = random.Random(20261016) ++ values = [0.0, -0.0, 5e-324, -5e-324, sys.float_info.max, ++ sys.float_info.min, 1.0, 1e-4, 1e-5, 1e15, 1e16, 1e20, 1e21] ++ for _ in range(12000): ++ value = struct.unpack(">d", rng.getrandbits(64).to_bytes(8, "big"))[0] ++ if math.isfinite(value): ++ values.append(value) ++ for power in range(-323, 309): ++ value = float("1e" + str(power)) ++ values.extend([value, math.nextafter(value, 0.0), math.nextafter(value, math.inf)]) ++ targets = [{"value": value} for value in values if math.isfinite(value)] ++ targets += [{"value": n} for n in [0, -1, 1, 2**53+1, 10**100, -(10**100)]] ++ targets += [json.loads('{"value":' + token + '}') for token in ++ ["-0", "-0.0", "1E0", "1.000", "100e-2", "1e-07", "1e+20"]] ++ targets += [{"text": "".join(chr(i) for i in range(32)) + '<>&é雪😀\u2028\u2029'}] ++ for _ in range(200): ++ points = [rng.randrange(0x110000) for _ in range(20)] ++ targets.append({"text": "".join(chr(c) for c in points if not 0xD800 <= c <= 0xDFFF)}) ++ with tempfile.TemporaryDirectory(prefix="approval-go-oracle-") as tmp: ++ path = Path(tmp) / "oracle.jsonl" ++ with path.open("w") as stream: ++ for target in targets: ++ act = dict(action="execute-ccr", actor="service:secrets-engine", ++ principal="principal", purpose="differential", target=target) ++ row = {"Raw": json.dumps(act, ensure_ascii=False, allow_nan=False), ++ "Canonical": canonical_json(canonical_binding(act)).decode(), ++ "Digest": binding_digest(act)} ++ stream.write(json.dumps(row, ensure_ascii=True, allow_nan=False) + "\n") ++ env = dict(os.environ, APPROVAL_TEST_ORACLE=str(path)) ++ subprocess.run(["go", "test", "-count=1", "-run", "TestPythonDifferential", "-v", "./..."], ++ cwd=ROOT / "adapters/go/approvalfact", env=env, check=True) ++ print(f"Python producer corpus: {len(targets)} cases") ++ ++ ++if __name__ == "__main__": ++ main() +diff --git a/tests/test_access_claim_vectors.py b/tests/test_access_claim_vectors.py +new file mode 100644 +index 0000000..72e5191 +--- /dev/null ++++ b/tests/test_access_claim_vectors.py +@@ -0,0 +1,42 @@ ++"""Producer facts/canonical vectors only; not a Go consumer acceptance test.""" ++import json ++from pathlib import Path ++ ++import jsonschema ++ ++from approval_engine.binding import binding_digest, canonical_binding, canonical_json ++from approval_engine.store import Engine ++from tests.conftest import FROZEN, binding, validity ++ ++ROOT = Path(__file__).resolve().parents[1] ++VECTORS = json.loads((ROOT / "examples/access-claim-reader-vectors.json").read_text()) ++ ++ ++def test_native_vectors_match_actual_producer_bytes_and_digest(): ++ for case in VECTORS["native_binding_vectors"]: ++ act = binding(target=json.loads(case["input_target_json"])) ++ assert act == case["binding"] ++ assert canonical_json(canonical_binding(act)).decode() == case["canonical_binding_utf8"] ++ assert binding_digest(act) == case["native_digest"] ++ digests = [v["native_digest"] for v in VECTORS["native_binding_vectors"]] ++ assert len(set(digests)) == len(digests) ++ ++ ++def test_positive_claim_vector_matches_actual_provider_and_schema(): ++ case = VECTORS["claim_cases"][0] ++ engine = Engine(clock=lambda: FROZEN) ++ obj = engine.create(binding(), validity(), approval_id=case["submitted_claim"]["approval_id"], ++ required_count=2, pdp_digest=case["expected_pdp_digest"], ++ pdp_path=True, human_control=True) ++ engine.add_entry(obj.id, "human:one", principal_type="human") ++ assert engine.claim(obj.id)["valid_now"] is False ++ engine.add_entry(obj.id, "human:two", principal_type="human") ++ assert engine.claim(obj.id) == case["submitted_claim"] ++ from datetime import timedelta ++ engine.clock = lambda: FROZEN + timedelta(seconds=5) ++ assert engine.claim(obj.id) == case["observed_claim"] ++ assert case["submitted_claim"]["freshness"] != case["observed_claim"]["freshness"] ++ schema = json.loads((ROOT / "schemas/approval_claim.schema.json").read_text()) ++ jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker()).validate( ++ case["observed_claim"]) ++ engine.close() +diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +index 856ebc7..2a95d0d 100644 +--- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md ++++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +@@ -26,6 +26,12 @@ state_hub_workstream_id: "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf" + + # APPROVAL-WP-0002 — Production readiness and consumer adoption + ++2026-10-16 additional scoped demand before Access B2 implementation: ++CUST-WP-0080 selects T01/T05's authoritative claim/human-control/freshness and ++exact-source consumer contract review and producer fixtures only, tracked in ++APPROVAL-WP-0005. Existing residual/native statuses and authority gates remain; ++this does not activate other residual work, a consumer policy or live transport. ++ + Move the completed first-cut engine spine into an authenticated, durable, + observable production service and prove one PEP integration end to end. This is + the residual production scope deliberately excluded from APPROVAL-WP-0001. +diff --git a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +new file mode 100644 +index 0000000..abdb945 +--- /dev/null ++++ b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +@@ -0,0 +1,83 @@ ++--- ++id: APPROVAL-WP-0005 ++type: workplan ++title: "Approval fact contract and reusable Go data adapter" ++domain: infotech ++repo: approval-engine ++status: active ++flavor: implementation ++owner: codex ++topic_slug: netkingdom ++created: "2026-10-16" ++updated: "2026-10-16" ++depends_on: [] ++--- ++ ++CUST-WP-0080 explicitly selects the necessary Approval-WP-0002-T01/T05 source ++contract subset for the Access service HTTP B2 join. Initial scope is owner ++review and producer fixtures; subsequent explicit CUST-WP-0080 demand selects ++T03's pure reusable data adapter, agreed with Access before implementation. ++No broad residual activation, credential transport runtime, trust admission, ++policy eligibility or live action is included. ++ ++## Review authoritative claim guarantees and publish thin consumer vectors ++ ++```task ++id: APPROVAL-WP-0005-T01 ++status: done ++priority: high ++``` ++ ++INTENT basis: factual exact-bound approval claims supplied to Access, never a ++second PDP. Source contract documents verified human-control semantics and their ++issuer admission limit, both original/subsequent observation bounds, exact ++native/PDP correspondence and immutable original submitted context. Producer ++vectors cover current lifecycle and canonical types. The superseded proposal's ++request-refresh design requires change; fresh facts are verified out-of-band. ++ ++## Independently integrate owner contract and coordinate Access reader conformance ++ ++```task ++id: APPROVAL-WP-0005-T02 ++status: todo ++priority: high ++``` ++ ++Coordinator reviews/integrates these source records. Access owns the thin Go ++reader/normalization and conformance implementation under FLEX-WP-0024-T06; ++Secrets owns strict immutable handoff and Platform owns provider origin/custody ++and final client pin. Native issuer/profile and served-PEP obligations remain ++live under APPROVAL-WP-0002-T01/T05, with audit under T04. No new native residual ++is created, promoted or marked complete by these source-only fixtures. ++ ++## Supply producer-owned reusable Go claim and native binding data adapter ++ ++```task ++id: APPROVAL-WP-0005-T03 ++status: done ++priority: high ++``` ++ ++Explicit bounded implementation demand after Access confirmed the exact module ++and raw-JSON API: adapters/go/approvalfact contains the existing Python-native ++five-field canonicalization port and strict human/PDP-path typed claim validation ++plus original/fresh fact matching. It is pure data code, not a source transport, ++credential runtime, policy evaluator or consume service. Fact means validated ++shape/correspondence, never authenticated provider origin. ++ ++Fourteen pinned native vectors and 24 claim cases pass in Go, with strict ++duplicate/Unicode/type/expiry defenses and detached original bytes. A 14,114-case ++deterministic differential corpus matches actual Python producer canonical bytes ++and digests; Go race tests pass. This is source sample evidence, not exhaustive ++proof or native acceptance. An initial module toolchain selection failure and ++shared cache sandbox refusal are retained in preparation evidence; final module ++uses Access's existing Go1.26.7 and completed tests. ++ ++Access explicitly retains the expected-native-raw-source correspondence gate: ++its current decoded float64 maps cannot recover Python float/int identity. ++The current pilot's raw resource may be used only with proof of no active target ++registry override plus exact existing tenant normalization. Other enrichment ++needs authoritative raw source/compiler representation. No numeric restriction, ++digest substitution, foreign review, runtime enable or live proof is invented. ++T02 still owns independent source integration/handoff; existing T01/T05 native ++residual obligations and actual Access conformance/integration remain unchanged. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/incremental-after-fact-contract.patch b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/incremental-after-fact-contract.patch new file mode 100644 index 00000000..60d908b3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/incremental-after-fact-contract.patch @@ -0,0 +1,3199 @@ +diff --git a/adapters/go/approvalfact/README.md b/adapters/go/approvalfact/README.md +new file mode 100644 +index 0000000..fdffd1f +--- /dev/null ++++ b/adapters/go/approvalfact/README.md +@@ -0,0 +1,55 @@ ++# Approval fact data adapter ++ ++Provider-owned pure Go module for Access's Approval B2 source reader. No network, ++credentials, subprocess, policy evaluator, server or consume mutation exists here. ++Python `approval_engine.binding` remains the authoritative native format. ++ ++API: ++ ++- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes ++ and sha256 digest; extra binding metadata is excluded. ++- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path ++ claim against independently owner-derived `Expected.NativeBindingJSON`, exact ++ approval ID and actual Access approval-free PDP digest. ++- `MatchObserved(submitted, observed)` requires two validated facts at the same ++ captured instant and identical nonfreshness authority facts. No request is ++ rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines ++ preserve each input's own freshness and approval expiry. ++ ++`Fact` means validated data, never authenticated provider origin. Access must ++fetch provider bytes through its independently admitted fixed origin/identity, ++then wrap this data in its own unforgeable source handle. A caller-supplied claim ++cannot become source truth merely by passing this helper. Access remains the ++single evaluator and must bound decision/operation/caller lifetimes separately. ++PEP consume CAS and live one-shot journal remain required before any effect. ++ ++Expected native act bytes must preserve the actual owner/compiler/normalization ++representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and ++large integers. Raw submitted resource is sufficient only when Access proves ++the active registry cannot override that target and applies its exact existing ++normalization. Otherwise authoritative raw enrichment/compiler evidence is ++needed. This helper supplies no fallback or invented restricted numeric domain. ++ ++The strict parser retains integer/floating lexical types, arbitrary integer ++magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values. ++Canonical float/string formatting follows Python source, including negative ++floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate ++object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. ++Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at ++most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++These explicit parser/resource limits fail closed, never normalize authority. ++ ++`go test -race ./...` checks the portable producer vectors, typed fact pairing, ++strict JSON/Unicode and exact deadline semantics. From the owner repo root, ++`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case ++development-only differential corpus against the actual Python producer, ++including random finite IEEE bit patterns, exponent boundaries/subnormals and ++Unicode. It uses a test-only local oracle file; production imports no oracle or ++Python runtime. Sample conformance is evidence, not a formal proof over all JSON. ++ ++The module pins Go 1.26.7 to match the existing Access consumer toolchain. ++`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner ++fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`. ++Transport cases requiring HTTP remain the Access adapter's tests; this module ++checks their duplicate/nonfinite bodies only. Import/deployment/native identity ++admission and positive policy gates require separate owner evidence. +diff --git a/adapters/go/approvalfact/canonical.go b/adapters/go/approvalfact/canonical.go +new file mode 100644 +index 0000000..06f14b5 +--- /dev/null ++++ b/adapters/go/approvalfact/canonical.go +@@ -0,0 +1,295 @@ ++// Package approvalfact provides data-only Approval claim correspondence. ++// It neither authenticates a provider nor decides permission or consumes objects. ++package approvalfact ++ ++import ( ++ "bytes" ++ "crypto/sha256" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "math" ++ "math/big" ++ "sort" ++ "strconv" ++ "strings" ++ "unicode/utf8" ++) ++ ++const MaxJSONBytes = 1 << 20 ++ ++var errJSON = errors.New("invalid Approval JSON") ++ ++// DecodeJSON retains numeric lexical types, refuses duplicates and invalid ++// Unicode, and bounds work. A Go map previously decoded into float64 cannot ++// recover the producer's integer-versus-floating identity; pass source bytes. ++func DecodeJSON(raw []byte) (any, error) { ++ if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) { ++ return nil, errJSON ++ } ++ d := json.NewDecoder(bytes.NewReader(raw)) ++ d.UseNumber() ++ var read func(int) (any, error) ++ read = func(depth int) (any, error) { ++ if depth > 128 { ++ return nil, errJSON ++ } ++ t, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ switch x := t.(type) { ++ case json.Delim: ++ if x == '{' { ++ m := map[string]any{} ++ for d.More() { ++ k, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ s, ok := k.(string) ++ if !ok { ++ return nil, errJSON ++ } ++ if _, ok = m[s]; ok { ++ return nil, errJSON ++ } ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ m[s] = v ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim('}') { ++ return nil, errJSON ++ } ++ return m, nil ++ } ++ if x == '[' { ++ a := []any{} ++ for d.More() { ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ a = append(a, v) ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim(']') { ++ return nil, errJSON ++ } ++ return a, nil ++ } ++ return nil, errJSON ++ case json.Number: ++ if _, e := pythonNumber(x); e != nil { ++ return nil, e ++ } ++ return x, nil ++ case string, bool, nil: ++ return x, nil ++ default: ++ return nil, errJSON ++ } ++ } ++ v, e := read(0) ++ if e != nil { ++ return nil, e ++ } ++ if _, e = d.Token(); e != io.EOF { ++ return nil, errJSON ++ } ++ return v, nil ++} ++ ++// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer ++// cannot encode them as UTF-8. Reject before the decoder can erase that fact. ++func validSurrogates(raw []byte) bool { ++ in := false ++ for i := 0; i < len(raw); i++ { ++ if raw[i] == '"' { ++ in = !in ++ continue ++ } ++ if !in || raw[i] != '\\' { ++ continue ++ } ++ i++ ++ if i >= len(raw) { ++ return false ++ } ++ if raw[i] != 'u' { ++ continue ++ } ++ if i+4 >= len(raw) { ++ return false ++ } ++ n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16) ++ if e != nil { ++ return false ++ } ++ i += 4 ++ if n >= 0xdc00 && n <= 0xdfff { ++ return false ++ } ++ if n < 0xd800 || n > 0xdbff { ++ continue ++ } ++ if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' { ++ return false ++ } ++ low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16) ++ if e != nil || low < 0xdc00 || low > 0xdfff { ++ return false ++ } ++ i += 6 ++ } ++ return !in ++} ++ ++func pythonNumber(n json.Number) (string, error) { ++ s := string(n) ++ if !strings.ContainsAny(s, ".eE") { ++ i, ok := new(big.Int).SetString(s, 10) ++ if !ok { ++ return "", errJSON ++ } ++ return i.String(), nil ++ } ++ f, e := strconv.ParseFloat(s, 64) ++ if e != nil || math.IsInf(f, 0) || math.IsNaN(f) { ++ return "", errJSON ++ } ++ // Python repr uses shortest round-trip digits, fixed form for exponents ++ // -4..15, a mandatory .0 for integral floats, and two exponent digits. ++ sci := strconv.FormatFloat(f, 'e', -1, 64) ++ at := strings.IndexByte(sci, 'e') ++ exp, _ := strconv.Atoi(sci[at+1:]) ++ if exp >= -4 && exp < 16 { ++ s = strconv.FormatFloat(f, 'f', -1, 64) ++ if !strings.Contains(s, ".") { ++ s += ".0" ++ } ++ return s, nil ++ } ++ sign := "+" ++ if exp < 0 { ++ sign = "-" ++ exp = -exp ++ } ++ digits := strconv.Itoa(exp) ++ if len(digits) < 2 { ++ digits = "0" + digits ++ } ++ return sci[:at] + "e" + sign + digits, nil ++} ++ ++func quote(s string) string { ++ var b strings.Builder ++ b.WriteByte('"') ++ for _, r := range s { ++ switch r { ++ case '"', '\\': ++ b.WriteByte('\\') ++ b.WriteRune(r) ++ case '\b': ++ b.WriteString(`\b`) ++ case '\f': ++ b.WriteString(`\f`) ++ case '\n': ++ b.WriteString(`\n`) ++ case '\r': ++ b.WriteString(`\r`) ++ case '\t': ++ b.WriteString(`\t`) ++ default: ++ if r < 32 { ++ b.WriteString(`\u00`) ++ const h = "0123456789abcdef" ++ b.WriteByte(h[byte(r)>>4]) ++ b.WriteByte(h[byte(r)&15]) ++ } else { ++ b.WriteRune(r) ++ } ++ } ++ } ++ b.WriteByte('"') ++ return b.String() ++} ++ ++func canonical(v any) (string, error) { ++ switch x := v.(type) { ++ case nil: ++ return "null", nil ++ case bool: ++ if x { ++ return "true", nil ++ } ++ return "false", nil ++ case string: ++ return quote(x), nil ++ case json.Number: ++ return pythonNumber(x) ++ case []any: ++ a := make([]string, len(x)) ++ for i, v := range x { ++ s, e := canonical(v) ++ if e != nil { ++ return "", e ++ } ++ a[i] = s ++ } ++ return "[" + strings.Join(a, ",") + "]", nil ++ case map[string]any: ++ keys := make([]string, 0, len(x)) ++ for k := range x { ++ keys = append(keys, k) ++ } ++ sort.Strings(keys) ++ a := make([]string, 0, len(keys)) ++ for _, k := range keys { ++ s, e := canonical(x[k]) ++ if e != nil { ++ return "", e ++ } ++ a = append(a, quote(k)+":"+s) ++ } ++ return "{" + strings.Join(a, ",") + "}", nil ++ default: ++ return "", errJSON ++ } ++} ++ ++// CanonicalBinding reproduces approval_engine.binding.binding_digest over the ++// five native act fields. Extra binding metadata is excluded by that contract. ++func CanonicalBinding(raw []byte) ([]byte, string, error) { ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return nil, "", e ++ } ++ m, ok := v.(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act := map[string]any{} ++ for _, k := range []string{"action", "actor", "principal", "purpose"} { ++ s, ok := m[k].(string) ++ if !ok || s == "" { ++ return nil, "", errJSON ++ } ++ act[k] = s ++ } ++ target, ok := m["target"].(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act["target"] = target ++ s, e := canonical(act) ++ if e != nil { ++ return nil, "", e ++ } ++ sum := sha256.Sum256([]byte(s)) ++ return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil ++} +diff --git a/adapters/go/approvalfact/claim.go b/adapters/go/approvalfact/claim.go +new file mode 100644 +index 0000000..169643f +--- /dev/null ++++ b/adapters/go/approvalfact/claim.go +@@ -0,0 +1,170 @@ ++package approvalfact ++ ++import ( ++ "encoding/json" ++ "errors" ++ "regexp" ++ "strconv" ++ "time" ++) ++ ++var errClaim = errors.New("Approval fact does not match required claim") ++var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) ++var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) ++var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`) ++ ++// Expected contains independently owner-derived native act bytes and the actual ++// Access approval-free digest. Marshaling an already float64-decoded Go request ++// is not an authoritative reconstruction of the native act. ++type Expected struct { ++ ApprovalID string ++ NativeBindingJSON []byte ++ PDPDigest string ++} ++ ++// Fact is validated data shape/correspondence, NOT authenticated provider origin ++// or execution authority. Access must wrap it only after its own source admission. ++type Fact struct { ++ raw []byte ++ stable string ++ digest string ++ observed, timeLimit, validatedAt time.Time ++} ++ ++func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) } ++func (f Fact) ObservedAt() time.Time { return f.observed } ++func (f Fact) Deadline() time.Time { return f.timeLimit } ++func (f Fact) NativeDigest() string { return f.digest } ++ ++func members(m map[string]any, required, optional []string) bool { ++ allowed := map[string]bool{} ++ for _, k := range required { ++ allowed[k] = true ++ if _, ok := m[k]; !ok { ++ return false ++ } ++ } ++ for _, k := range optional { ++ allowed[k] = true ++ } ++ for k := range m { ++ if !allowed[k] { ++ return false ++ } ++ } ++ return true ++} ++func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok } ++func stamp(v any) (time.Time, error) { ++ s, ok := v.(string) ++ if !ok || !timePattern.MatchString(s) { ++ return time.Time{}, errClaim ++ } ++ t, e := time.Parse(time.RFC3339Nano, s) ++ if e != nil { ++ return time.Time{}, errClaim ++ } ++ return t, nil ++} ++ ++// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim ++// at a caller-captured instant, using the existing Approval producer semantics. ++// It has no I/O, policy evaluator, credentials, cache, or consume mutation. ++func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) { ++ if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) { ++ return Fact{}, errClaim ++ } ++ _, want, e := CanonicalBinding(expected.NativeBindingJSON) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ m, ok := object(v) ++ if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) { ++ return Fact{}, errClaim ++ } ++ if y, ok := m["yields_to"]; ok { ++ if _, ok = y.(string); !ok { ++ return Fact{}, errClaim ++ } ++ } ++ if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" { ++ return Fact{}, errClaim ++ } ++ b, ok := object(m["binding"]) ++ if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) { ++ return Fact{}, errClaim ++ } ++ if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true { ++ return Fact{}, errClaim ++ } ++ text, e := canonical(b) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ _, got, e := CanonicalBinding([]byte(text)) ++ if e != nil || got != want { ++ return Fact{}, errClaim ++ } ++ f, ok := object(m["freshness"]) ++ if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) { ++ return Fact{}, errClaim ++ } ++ observed, e := stamp(f["observed_at"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ notAfter, e := stamp(f["not_after"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ ttl, ok := f["ttl_seconds"].(json.Number) ++ if !ok { ++ return Fact{}, errClaim ++ } ++ seconds, e := strconv.ParseInt(string(ttl), 10, 64) ++ if e != nil || seconds < 1 { ++ return Fact{}, errClaim ++ } ++ // Bound arithmetic to time.Duration's representable seconds, not a new ++ // semantic freshness grant or configured TTL. Unsupported extremes refuse. ++ if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) { ++ return Fact{}, errClaim ++ } ++ validity, ok := object(m["validity"]) ++ if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) { ++ return Fact{}, errClaim ++ } ++ before, e := stamp(validity["not_before"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ expires, e := stamp(validity["expires_at"]) ++ if e != nil || now.Before(before) || !now.Before(expires) { ++ return Fact{}, errClaim ++ } ++ limit := notAfter ++ if expires.Before(limit) { ++ limit = expires ++ } ++ delete(m, "freshness") ++ delete(m, "yields_to") ++ stable, e := canonical(m) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil ++} ++ ++// MatchObserved requires independently validated original/fetched facts at the ++// same captured instant and exact nonfreshness authority correspondence. It ++// never rewrites the original request or extends its original deadline. ++func MatchObserved(submitted, observed Fact) error { ++ if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable { ++ return errClaim ++ } ++ return nil ++} +diff --git a/adapters/go/approvalfact/claim_test.go b/adapters/go/approvalfact/claim_test.go +new file mode 100644 +index 0000000..26623c8 +--- /dev/null ++++ b/adapters/go/approvalfact/claim_test.go +@@ -0,0 +1,144 @@ ++package approvalfact ++ ++import ( ++ "bytes" ++ "encoding/json" ++ "os" ++ "strings" ++ "testing" ++ "time" ++) ++ ++type vectors struct { ++ Native []struct { ++ Binding json.RawMessage `json:"binding"` ++ Canonical string `json:"canonical_binding_utf8"` ++ Digest string `json:"native_digest"` ++ } `json:"native_binding_vectors"` ++ Claims []struct { ++ Name string `json:"name"` ++ Now time.Time `json:"validation_time"` ++ Submitted json.RawMessage `json:"submitted_claim"` ++ Observed json.RawMessage `json:"observed_claim"` ++ Binding json.RawMessage `json:"expected_binding"` ++ PDP string `json:"expected_pdp_digest"` ++ Accept bool `json:"expected_accept"` ++ } `json:"claim_cases"` ++ Transport []struct { ++ Name string `json:"name"` ++ Body string `json:"body_utf8"` ++ } `json:"transport_cases"` ++} ++ ++func load(t *testing.T) vectors { ++ t.Helper() ++ raw, e := os.ReadFile("testdata/owner-vectors.json") ++ if e != nil { ++ t.Fatal(e) ++ } ++ var v vectors ++ if e = json.Unmarshal(raw, &v); e != nil { ++ t.Fatal(e) ++ } ++ return v ++} ++ ++func TestProducerCanonicalVectors(t *testing.T) { ++ for _, v := range load(t).Native { ++ t.Run(v.Digest, func(t *testing.T) { ++ raw, d, e := CanonicalBinding(v.Binding) ++ if e != nil || string(raw) != v.Canonical || d != v.Digest { ++ t.Fatalf("producer parity: %q %s %v, want %q %s", raw, d, e, v.Canonical, v.Digest) ++ } ++ }) ++ } ++} ++ ++func TestProducerClaimVectors(t *testing.T) { ++ for _, v := range load(t).Claims { ++ t.Run(v.Name, func(t *testing.T) { ++ var id struct { ++ ID string `json:"approval_id"` ++ } ++ if e := json.Unmarshal(v.Submitted, &id); e != nil { ++ t.Fatal(e) ++ } ++ expected := Expected{id.ID, v.Binding, v.PDP} ++ old, e1 := ValidateClaim(v.Submitted, expected, v.Now) ++ fresh, e2 := ValidateClaim(v.Observed, expected, v.Now) ++ accepted := e1 == nil && e2 == nil && MatchObserved(old, fresh) == nil ++ if accepted != v.Accept { ++ t.Fatalf("accepted=%v want=%v original=%v observed=%v", accepted, v.Accept, e1, e2) ++ } ++ if accepted { ++ raw := old.RawJSON() ++ if !bytes.Equal(raw, v.Submitted) { ++ t.Fatal("original bytes changed") ++ } ++ raw[0] = 'x' ++ if old.RawJSON()[0] != '{' { ++ t.Fatal("mutable fact storage") ++ } ++ if !old.Deadline().Before(fresh.Deadline()) { ++ t.Fatal("refresh must not extend original deadline") ++ } ++ } ++ }) ++ } ++} ++ ++func TestStrictJSONRefusals(t *testing.T) { ++ for _, raw := range []string{`{"a":1,"a":2}`, `{"a":{"x":1,"x":2}}`, `{"a":NaN}`, `{"a":1e999}`, `{"a":"\ud800"}`, `{"a":"\udfff"}`, `{"a":"\ud800x"}`, `{"a":1} {}`, `{"a":"` + string([]byte{0xff}) + `"}`, strings.Repeat("[", 130) + strings.Repeat("]", 130), strings.Repeat(" ", MaxJSONBytes+1)} { ++ if _, e := DecodeJSON([]byte(raw)); e == nil { ++ t.Fatalf("accepted malformed JSON %.80q", raw) ++ } ++ } ++} ++ ++func TestUnicodeAndControlEncoding(t *testing.T) { ++ for _, raw := range []string{`{"v":"\ud83d\ude00"}`, `{"v":"escaped \\\" quote"}`, `{"v":"\u0000\b\f\n\r\t"}`} { ++ if _, e := DecodeJSON([]byte(raw)); e != nil { ++ t.Fatalf("refused valid Unicode %q", raw) ++ } ++ } ++} ++ ++func TestGoldenDuplicateAndNonfiniteBodies(t *testing.T) { ++ for _, v := range load(t).Transport { ++ if v.Name == "duplicate_valid_now" || v.Name == "nonfinite_json" { ++ if _, e := DecodeJSON([]byte(v.Body)); e == nil { ++ t.Fatal(v.Name) ++ } ++ } ++ } ++} ++ ++func TestInvalidExpectedAndZeroFacts(t *testing.T) { ++ v := load(t).Claims[0] ++ if _, e := ValidateClaim(v.Submitted, Expected{}, v.Now); e == nil { ++ t.Fatal("missing expected act accepted") ++ } ++ if MatchObserved(Fact{}, Fact{}) == nil { ++ t.Fatal("zero facts accepted") ++ } ++ expected := Expected{"11111111-1111-4111-8111-111111111111", v.Binding, v.PDP} ++ a, e := ValidateClaim(v.Submitted, expected, v.Now) ++ if e != nil { ++ t.Fatal(e) ++ } ++ b, e := ValidateClaim(v.Observed, expected, v.Now.Add(time.Second)) ++ if e != nil { ++ t.Fatal(e) ++ } ++ if MatchObserved(a, b) == nil { ++ t.Fatal("different validation instants accepted") ++ } ++} ++ ++func TestTimestampPrecisionAndOffsetRefusals(t *testing.T) { ++ for _, value := range []string{"2026-08-29T12:00:00,1Z", "2026-08-29T12:00:00+24:00", "2026-08-29T12:00:00+00:60", "2026-08-29T12:00:00.0000000001Z", "2026-08-29T12:00:00"} { ++ if _, err := stamp(value); err == nil { ++ t.Fatalf("accepted unsupported timestamp %q", value) ++ } ++ } ++} +diff --git a/adapters/go/approvalfact/go.mod b/adapters/go/approvalfact/go.mod +new file mode 100644 +index 0000000..bcb1783 +--- /dev/null ++++ b/adapters/go/approvalfact/go.mod +@@ -0,0 +1,3 @@ ++module github.com/netkingdom/approval-engine/adapters/go/approvalfact ++ ++go 1.26.7 +diff --git a/adapters/go/approvalfact/oracle_test.go b/adapters/go/approvalfact/oracle_test.go +new file mode 100644 +index 0000000..b845388 +--- /dev/null ++++ b/adapters/go/approvalfact/oracle_test.go +@@ -0,0 +1,42 @@ ++package approvalfact ++ ++import ( ++ "bufio" ++ "encoding/json" ++ "os" ++ "testing" ++) ++ ++// This test-only input is produced by scripts/check_go_approvalfact.py using ++// the actual Python owner primitive. Production has no subprocess or oracle. ++func TestPythonDifferential(t *testing.T) { ++ path := os.Getenv("APPROVAL_TEST_ORACLE") ++ if path == "" { ++ t.Skip("run scripts/check_go_approvalfact.py for Python differential corpus") ++ } ++ f, e := os.Open(path) ++ if e != nil { ++ t.Fatal(e) ++ } ++ defer f.Close() ++ s := bufio.NewScanner(f) ++ count := 0 ++ for s.Scan() { ++ var v struct{ Raw, Canonical, Digest string } ++ if e = json.Unmarshal(s.Bytes(), &v); e != nil { ++ t.Fatal(e) ++ } ++ raw, d, e := CanonicalBinding([]byte(v.Raw)) ++ if e != nil || string(raw) != v.Canonical || d != v.Digest { ++ t.Fatalf("Python differential row %d: got %q %s %v want %q %s", count, raw, d, e, v.Canonical, v.Digest) ++ } ++ count++ ++ } ++ if e = s.Err(); e != nil { ++ t.Fatal(e) ++ } ++ if count < 10000 { ++ t.Fatalf("incomplete differential corpus: %d", count) ++ } ++ t.Logf("matched %d Python source canonical bytes and digests", count) ++} +diff --git a/adapters/go/approvalfact/testdata/owner-vectors.json b/adapters/go/approvalfact/testdata/owner-vectors.json +new file mode 100644 +index 0000000..81caf34 +--- /dev/null ++++ b/adapters/go/approvalfact/testdata/owner-vectors.json +@@ -0,0 +1,2305 @@ ++{ ++ "producer_revision": "eece796b8bbee19551f6aa07ad389cbf83cabcdb", ++ "fixture_only": true, ++ "source": "Engine.claim with fixed synthetic identities; mutated consumer refusal cases explicitly labeled; no native identity admission", ++ "schema_version": "0.1", ++ "claim_cases": [ ++ { ++ "name": "fresh_out_of_band_observation_unchanged_original", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": true, ++ "reason": "matching_valid_facts_original_digest_unchanged" ++ }, ++ { ++ "name": "requested", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "requested", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "requested" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "insufficient_distinct_humans", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "requested", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "requested" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "foreign_issuer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "foreign", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "foreign_id", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "22222222-2222-4222-8222-222222222222", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "nonhuman_control", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": false ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "legacy_missing_human_control", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "human_control_integer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": 1 ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_pdp_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:abababababababababababababababababababababababababababababababab", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "null_pdp_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": null, ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "undeclared_pdp_path", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": false, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "valid_now_integer", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": 1, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_native_digest", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:abababababababababababababababababababababababababababababababab", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "wrong_act", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "other", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "future_observation", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:06+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "inconsistent_ttl", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 31, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "zero_ttl", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 0, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "naive_observation", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "original_observation_expired_even_when_provider_fresh", ++ "validation_time": "2026-08-29T12:00:30+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "refetch_cannot_extend_original_freshness", ++ "validation_time": "2026-08-29T12:00:30+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:30+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:01:00+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "consumed_source", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "consumed", ++ "valid_now": false, ++ "consumed": true, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:05+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:35+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "consumed" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "expired_after_consumption_preserves_consumed_true", ++ "validation_time": "2026-08-30T12:00:00+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "valid", ++ "valid_now": true, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "ok" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "11111111-1111-4111-8111-111111111111", ++ "state": "expired", ++ "valid_now": false, ++ "consumed": true, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-30T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-30T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "expired" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "source_fact_refusal" ++ }, ++ { ++ "name": "source_revoked", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "6302eb08-215e-4a17-8e6f-66717874c6af", ++ "state": "revoked", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "revoked" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "6302eb08-215e-4a17-8e6f-66717874c6af", ++ "state": "revoked", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "revoked" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "terminal_source_refusal" ++ }, ++ { ++ "name": "source_superseded", ++ "validation_time": "2026-08-29T12:00:05+00:00", ++ "submitted_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "8c8f9c0f-b2dd-4a1f-964a-54ac7022803b", ++ "state": "superseded", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "superseded" ++ }, ++ "observed_claim": { ++ "schema_version": "0.1", ++ "kind": "approval-claim", ++ "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", ++ "issuer": "approval-engine", ++ "approval_id": "8c8f9c0f-b2dd-4a1f-964a-54ac7022803b", ++ "state": "superseded", ++ "valid_now": false, ++ "consumed": false, ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "digest": "sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b", ++ "pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "pdp_path": true, ++ "human_control": true ++ }, ++ "freshness": { ++ "observed_at": "2026-08-29T12:00:00+00:00", ++ "ttl_seconds": 30, ++ "not_after": "2026-08-29T12:00:30+00:00" ++ }, ++ "validity": { ++ "not_before": "2026-08-29T11:00:00+00:00", ++ "expires_at": "2026-08-29T15:00:00+00:00" ++ }, ++ "reason_code": "superseded" ++ }, ++ "expected_binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "id": "lane-openbao-root", ++ "stage": "prod" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "expected_pdp_digest": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", ++ "expected_accept": false, ++ "reason": "terminal_source_refusal" ++ } ++ ], ++ "native_binding_vectors": [ ++ { ++ "input_target_json": "{\"value\":true}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": true ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":true}}", ++ "native_digest": "sha256:e1f537daa849e5da023efb5f3d62cc33d94cce20f9ca9c5a708c76e225fd702f" ++ }, ++ { ++ "input_target_json": "{\"value\":1}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1}}", ++ "native_digest": "sha256:3a3d2da6267e4b917505a7c2247e6ce36fd8ee074eef37251bcc66839b602332" ++ }, ++ { ++ "input_target_json": "{\"value\":1.0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1.0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1.0}}", ++ "native_digest": "sha256:21c2cdb9b161071ebfe361e50703511b2abf6c6f327f153282d247aa92c3898e" ++ }, ++ { ++ "input_target_json": "{\"value\":false}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": false ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":false}}", ++ "native_digest": "sha256:278cbcd013f7923db5258dd860f911d688505a8259f15ccb77983b044f312c74" ++ }, ++ { ++ "input_target_json": "{\"value\":0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":0}}", ++ "native_digest": "sha256:e691b8fb55d4bf82a310dee37c045331100c8031e26ea6f51bdcceeeb3b1f20e" ++ }, ++ { ++ "input_target_json": "{\"value\":-0.0}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": -0.0 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":-0.0}}", ++ "native_digest": "sha256:eccde458c686bff67506bb2d94c750e648a311bf23967cb22b37f11e84b6844f" ++ }, ++ { ++ "input_target_json": "{\"value\":1e-7}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e-07 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-07}}", ++ "native_digest": "sha256:779042b6c0e675f6aacdcd3a02199aab66f534bb84dd410a95c18657afae563c" ++ }, ++ { ++ "input_target_json": "{\"value\":1e-6}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e-06 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-06}}", ++ "native_digest": "sha256:d021f402231fcd59e764b31e6f08a5298ebb328dfd93da58ecde8feb11fde0b5" ++ }, ++ { ++ "input_target_json": "{\"value\":1e20}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e+20 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+20}}", ++ "native_digest": "sha256:14270ffca4ab10e9d9302d52e988b4aa3e2facd494c12a60e2edafef70277a20" ++ }, ++ { ++ "input_target_json": "{\"value\":1e21}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 1e+21 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e+21}}", ++ "native_digest": "sha256:75a60a595b2578430b0be466b6bf9a46b62ddc99bbbcebd7fa0e8d22e3078c0a" ++ }, ++ { ++ "input_target_json": "{\"value\":2.5e-7}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 2.5e-07 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":2.5e-07}}", ++ "native_digest": "sha256:50af37ff75a3e7a22629e746f814995c0c2cd96adb7f1ed5b26999b71d0ac498" ++ }, ++ { ++ "input_target_json": "{\"value\":9007199254740993}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "value": 9007199254740993 ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":9007199254740993}}", ++ "native_digest": "sha256:765360dc5a1d949bd114c6b749d83069123d8b4b6b1d939f831428a800f50dab" ++ }, ++ { ++ "input_target_json": "{\"html\":\"<>&\", \"unicode\":\"é雪😀\", \"separator\":\"\\u2028\\u2029\"}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "html": "<>&", ++ "unicode": "é雪😀", ++ "separator": "

" ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"<>&\",\"separator\":\"

\",\"unicode\":\"é雪😀\"}}", ++ "native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea" ++ }, ++ { ++ "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", ++ "binding": { ++ "action": "secrets.kv.destroy", ++ "target": { ++ "b": [ ++ true, ++ 1, ++ 1.0, ++ null, ++ { ++ "z": "x", ++ "a": "y" ++ } ++ ], ++ "a": {} ++ }, ++ "actor": "agt-secrets-engine", ++ "principal": "bernd", ++ "purpose": "rotate-exposed-key" ++ }, ++ "canonical_binding_utf8": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"a\":{},\"b\":[true,1,1.0,null,{\"a\":\"y\",\"z\":\"x\"}]}}", ++ "native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51" ++ } ++ ], ++ "transport_cases": [ ++ { ++ "name": "duplicate_valid_now", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\",\"valid_now\":false}", ++ "expected_accept": false ++ }, ++ { ++ "name": "nonfinite_json", ++ "status": 200, ++ "content_type": "application/json", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":NaN,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "redirect", ++ "status": 302, ++ "location": "https://foreign.example/claim", ++ "expected_accept": false, ++ "required_secondary_requests": 0 ++ }, ++ { ++ "name": "wrong_media_type", ++ "status": 200, ++ "content_type": "text/plain", ++ "body_utf8": "{\"schema_version\":\"0.1\",\"kind\":\"approval-claim\",\"yields_to\":\"net-kingdom taxonomy request-claim schema (statute §17; unassigned)\",\"issuer\":\"approval-engine\",\"approval_id\":\"11111111-1111-4111-8111-111111111111\",\"state\":\"valid\",\"valid_now\":true,\"consumed\":false,\"binding\":{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"id\":\"lane-openbao-root\",\"stage\":\"prod\"},\"digest\":\"sha256:0982954d4ecbaad0c7368c207ec530e51d7508c3304b67c10885d112175f929b\",\"pdp_digest\":\"sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd\",\"pdp_path\":true,\"human_control\":true},\"freshness\":{\"observed_at\":\"2026-08-29T12:00:05+00:00\",\"ttl_seconds\":30,\"not_after\":\"2026-08-29T12:00:35+00:00\"},\"validity\":{\"not_before\":\"2026-08-29T11:00:00+00:00\",\"expires_at\":\"2026-08-29T15:00:00+00:00\"},\"reason_code\":\"ok\"}", ++ "expected_accept": false ++ }, ++ { ++ "name": "provider_unavailable", ++ "status": 503, ++ "expected_accept": false ++ }, ++ { ++ "name": "declared_body_over_limit", ++ "status": 200, ++ "content_length": 1048577, ++ "consumer_limit_bytes": 1048576, ++ "expected_accept": false ++ }, ++ { ++ "name": "environment_proxy_configured", ++ "required_environment_proxy_requests": 0, ++ "expected_accept": false, ++ "note": "If fixed origin is unavailable, proxy cannot supply a replacement fact." ++ } ++ ] ++} +diff --git a/docs/access-claim-fact-reader.md b/docs/access-claim-fact-reader.md +index 40f7016..4669f64 100644 +--- a/docs/access-claim-fact-reader.md ++++ b/docs/access-claim-fact-reader.md +@@ -125,7 +125,9 @@ restriction was established by this review, so it cannot be presumed. If a + small shared producer-owned Go canonicalization helper becomes necessary, scope + it explicitly to the actual consumer need and test against these producer bytes; + do not invent a second unverified serialization algorithm in the adapter. This +-review provides vectors only, not that runtime implementation. The Go owner must ++initial review provided vectors only. Subsequent scoped WP0005-T03 supplies ++`adapters/go/approvalfact`, a producer-owned pure data implementation agreed with ++Access; it adds no credential transport/runtime. The Go consumer owner must + return exact accepted/refused vector results and transport negatives + (redirect/proxy/duplicate/size/deadline). + +diff --git a/scripts/check_go_approvalfact.py b/scripts/check_go_approvalfact.py +new file mode 100644 +index 0000000..c71f576 +--- /dev/null ++++ b/scripts/check_go_approvalfact.py +@@ -0,0 +1,53 @@ ++"""Development-only Go parity check against the actual Python producer.""" ++from pathlib import Path ++import json ++import math ++import os ++import random ++import struct ++import subprocess ++import sys ++import tempfile ++ ++ROOT = Path(__file__).resolve().parents[1] ++sys.path.insert(0, str(ROOT)) ++from approval_engine.binding import binding_digest, canonical_binding, canonical_json ++ ++ ++def main(): ++ rng = random.Random(20261016) ++ values = [0.0, -0.0, 5e-324, -5e-324, sys.float_info.max, ++ sys.float_info.min, 1.0, 1e-4, 1e-5, 1e15, 1e16, 1e20, 1e21] ++ for _ in range(12000): ++ value = struct.unpack(">d", rng.getrandbits(64).to_bytes(8, "big"))[0] ++ if math.isfinite(value): ++ values.append(value) ++ for power in range(-323, 309): ++ value = float("1e" + str(power)) ++ values.extend([value, math.nextafter(value, 0.0), math.nextafter(value, math.inf)]) ++ targets = [{"value": value} for value in values if math.isfinite(value)] ++ targets += [{"value": n} for n in [0, -1, 1, 2**53+1, 10**100, -(10**100)]] ++ targets += [json.loads('{"value":' + token + '}') for token in ++ ["-0", "-0.0", "1E0", "1.000", "100e-2", "1e-07", "1e+20"]] ++ targets += [{"text": "".join(chr(i) for i in range(32)) + '<>&é雪😀\u2028\u2029'}] ++ for _ in range(200): ++ points = [rng.randrange(0x110000) for _ in range(20)] ++ targets.append({"text": "".join(chr(c) for c in points if not 0xD800 <= c <= 0xDFFF)}) ++ with tempfile.TemporaryDirectory(prefix="approval-go-oracle-") as tmp: ++ path = Path(tmp) / "oracle.jsonl" ++ with path.open("w") as stream: ++ for target in targets: ++ act = dict(action="execute-ccr", actor="service:secrets-engine", ++ principal="principal", purpose="differential", target=target) ++ row = {"Raw": json.dumps(act, ensure_ascii=False, allow_nan=False), ++ "Canonical": canonical_json(canonical_binding(act)).decode(), ++ "Digest": binding_digest(act)} ++ stream.write(json.dumps(row, ensure_ascii=True, allow_nan=False) + "\n") ++ env = dict(os.environ, APPROVAL_TEST_ORACLE=str(path)) ++ subprocess.run(["go", "test", "-count=1", "-run", "TestPythonDifferential", "-v", "./..."], ++ cwd=ROOT / "adapters/go/approvalfact", env=env, check=True) ++ print(f"Python producer corpus: {len(targets)} cases") ++ ++ ++if __name__ == "__main__": ++ main() +diff --git a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +index 777a9e6..abdb945 100644 +--- a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md ++++ b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +@@ -1,11 +1,11 @@ + --- + id: APPROVAL-WP-0005 + type: workplan +-title: "Review thin Access claim fact reader contract and producer vectors" ++title: "Approval fact contract and reusable Go data adapter" + domain: infotech + repo: approval-engine + status: active +-flavor: planning ++flavor: implementation + owner: codex + topic_slug: netkingdom + created: "2026-10-16" +@@ -14,9 +14,11 @@ depends_on: [] + --- + + CUST-WP-0080 explicitly selects the necessary Approval-WP-0002-T01/T05 source +-contract subset for the Access service HTTP B2 join. Scope is owner review and +-producer fixtures only, before any consumer implementation. No broad residual +-activation, runtime client, trust admission, policy eligibility or live action. ++contract subset for the Access service HTTP B2 join. Initial scope is owner ++review and producer fixtures; subsequent explicit CUST-WP-0080 demand selects ++T03's pure reusable data adapter, agreed with Access before implementation. ++No broad residual activation, credential transport runtime, trust admission, ++policy eligibility or live action is included. + + ## Review authoritative claim guarantees and publish thin consumer vectors + +@@ -47,3 +49,35 @@ Secrets owns strict immutable handoff and Platform owns provider origin/custody + and final client pin. Native issuer/profile and served-PEP obligations remain + live under APPROVAL-WP-0002-T01/T05, with audit under T04. No new native residual + is created, promoted or marked complete by these source-only fixtures. ++ ++## Supply producer-owned reusable Go claim and native binding data adapter ++ ++```task ++id: APPROVAL-WP-0005-T03 ++status: done ++priority: high ++``` ++ ++Explicit bounded implementation demand after Access confirmed the exact module ++and raw-JSON API: adapters/go/approvalfact contains the existing Python-native ++five-field canonicalization port and strict human/PDP-path typed claim validation ++plus original/fresh fact matching. It is pure data code, not a source transport, ++credential runtime, policy evaluator or consume service. Fact means validated ++shape/correspondence, never authenticated provider origin. ++ ++Fourteen pinned native vectors and 24 claim cases pass in Go, with strict ++duplicate/Unicode/type/expiry defenses and detached original bytes. A 14,114-case ++deterministic differential corpus matches actual Python producer canonical bytes ++and digests; Go race tests pass. This is source sample evidence, not exhaustive ++proof or native acceptance. An initial module toolchain selection failure and ++shared cache sandbox refusal are retained in preparation evidence; final module ++uses Access's existing Go1.26.7 and completed tests. ++ ++Access explicitly retains the expected-native-raw-source correspondence gate: ++its current decoded float64 maps cannot recover Python float/int identity. ++The current pilot's raw resource may be used only with proof of no active target ++registry override plus exact existing tenant normalization. Other enrichment ++needs authoritative raw source/compiler representation. No numeric restriction, ++digest substitution, foreign review, runtime enable or live proof is invented. ++T02 still owns independent source integration/handoff; existing T01/T05 native ++residual obligations and actual Access conformance/integration remain unchanged. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/normal-sync.json b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/normal-sync.json new file mode 100644 index 00000000..c53386e8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/normal-sync.json @@ -0,0 +1,7 @@ +{ + "chunk_id": "1fd0b5", + "wall_time_seconds": 0.000015916, + "exit_code": 0, + "original_token_count": 858, + "output": "WARN: git commit failed for writeback: Generated-only commits rejected: staged\n{\n \"repo_slug\": \"approval-engine\",\n \"repo_path\": \"/home/worsch/approval-engine\",\n \"source_revision\": null,\n \"source_revision_reason\": \"dirty_workplans\",\n \"issues\": [\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-33\",\n \"message\": \"WORK-RECORDS.md is missing or stale \\u2014 regenerate via fix-consistency (CUST-WP-0061-T04 generated work-record index)\",\n \"file_path\": \"WORK-RECORDS.md\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": true\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-11\",\n \"message\": \"Task 'APPROVAL-WP-ADHOC-2026-10-11-T02' has no state_hub_task_id\",\n \"file_path\": \"workplans/ADHOC-2026-10-11.md#APPROVAL-WP-ADHOC-2026-10-11-T02\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": true\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-06\",\n \"message\": \"Workplan has no state_hub_workstream_id \\u2014 not indexed in DB\",\n \"file_path\": \"workplans/APPROVAL-WP-0005-access-claim-fact-contract.md\",\n \"db_id\": \"\",\n \"file_value\": \"Approval fact contract and reusable Go data adapter\",\n \"db_value\": \"\",\n \"fixable\": true\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-28\",\n \"message\": \"11 unread inbox message(s) older than 3 day(s): key-cape:ffe5aff4, informed-decision:59c5ee82, the-custodian:23172cb2, access-engine:9b8795d2, the-custodian:51e9eace, ...\",\n \"file_path\": \"\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-28\",\n \"message\": \"9 unread message(s) lack thread_id for supersession tracking: key-cape:ffe5aff4, informed-decision:59c5ee82, the-custodian:23172cb2, access-engine:9b8795d2, the-custodian:51e9eace\",\n \"file_path\": \"\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-29\",\n \"message\": \"Unread work request from informed-decision (59c5ee82) may need a workplan file: INFD-WP-0005: service-requested approvals from informed-decision intake (two small asks)\",\n \"file_path\": \"\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n },\n {\n \"severity\": \"WARN\",\n \"check_id\": \"C-29\",\n \"message\": \"Unread work request from flex-auth (2523510b) may need a workplan file: FLEX-WP-0020 handoff request (consumer)\",\n \"file_path\": \"\",\n \"db_id\": \"\",\n \"file_value\": \"\",\n \"db_value\": \"\",\n \"fixable\": false\n }\n ],\n \"fixes_applied\": [\n \"bindings: synced 7/7\",\n \"C-17 cleared: pushed 1 backlogged commit(s)\",\n \"C-33 fixed: regenerated WORK-RECORDS.md\",\n \"C-11 skipped: task 'APPROVAL-WP-ADHOC-2026-10-11-T02' in finished workstream \\u2014 not created\",\n \"C-06 fixed: created workstream 777b17de\\u2026 for APPROVAL-WP-0005, wrote ID to APPROVAL-WP-0005-access-claim-fact-contract.md\",\n \" + task APPROVAL-WP-0005-T01 \\u2192 24bf384a\\u2026\",\n \" + task APPROVAL-WP-0005-T02 \\u2192 3275354d\\u2026\",\n \" + task APPROVAL-WP-0005-T03 \\u2192 362bfe75\\u2026\",\n \"brief: .custodian-brief.md updated\",\n \"push: pushed\"\n ],\n \"summary\": {\n \"fail\": 0,\n \"automation_error\": 0,\n \"warn\": 7,\n \"info\": 0\n },\n \"result\": \"warn\"\n}\n" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/validation.json b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/validation.json new file mode 100644 index 00000000..285e9112 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-go-fact/validation.json @@ -0,0 +1,28 @@ +{ + "go_race": { + "command": "go test -race -count=1 ./...", + "module": "adapters/go/approvalfact", + "result": "passed" + }, + "go_vet": "passed", + "producer_golden_native_vectors": 14, + "producer_claim_cases": 24, + "transport": "only duplicate/nonfinite bodies tested here; HTTP transport negatives remain Access owner", + "python_differential": { + "command": "python3 scripts/check_go_approvalfact.py", + "matched_exact_canonical_bytes_and_digest": 14114, + "seed": 20261016, + "result": "passed" + }, + "python_regressions": { + "command": "python3 -m pytest -q tests/test_access_claim_vectors.py tests/test_human_control.py tests/test_claim_contract.py tests/test_tenant_provenance.py", + "passed": 44 + }, + "retained_preparation_failures": [ + "Initial go1.23 module selector could not find toolchain; module aligned to existing Access Go1.26.7, no added runtime", + "Shared default Go cache sandbox write refusal; authorized source test retry completed" + ], + "combined_patch_check_exit": 0, + "incremental_patch_check_exit": 0, + "native_accepted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/README.md b/docs/changes/netkingdom-target-2026-10-16/approval-reference/README.md new file mode 100644 index 00000000..0de22f15 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/README.md @@ -0,0 +1,65 @@ +# Approval exact-consume source provenance + +Prepared by the approval-engine worker, with its AGENTS.md and INTENT.md loaded, +in `/tmp/netkingdom-approval-reference-20261016`. Canonical owner source remained +unchanged and clean at `9491d1d3a7b6554fa04f8dc06f65d50e3d6ee4e1`. +This is source preparation and owner technical judgement, not a formal delegated +review, native approval or independently accepted deployment. The new owner work +record must be integrated before a formal review is recorded. + +The existing authoritative consume CAS, durable transactional outbox, +ExecutionConfirmationClient and hardened fixed-origin transport already supply +the factual consumer boundary. A signed portable consume receipt is absent, +but adding another signer/service is unnecessary for the independently +authenticated mediator. The reachable source gap was discarded consume caller +identity and insufficient standalone act/PDP correspondence in the use event. + +The patch passes HTTP-verified Identity to consume and atomically retains bounded +consumer metadata, native binding, PDP digest/path alongside existing full +request/decision and threshold evidence. Top-level actor remains the approved +initiator. Caller JSON cannot replace verified consumer provenance. Internal +calls retain null, retries preserve the first event, and historical rows remain +unchanged. Existing response/claim fields and schema version are unchanged. +INTENT basis: authenticated consumable object, exact binding and local atomic +evidence. It supplies facts and creates no new permission decision. + +`implementation.patch`, `manifest.json`, `validation.json` and `patch-check.json` +pin the exact deliverable. Patch SHA256: +`a6bf77fa8b6916f423db7832f475e38747b828dd632a88f9631131e28e3230ee`. +44 focused tests passed. Full suite: 277 passed and one failed deployment-evidence +image pin check; the identical failure was reproduced on an untouched baseline. +Historical release/native evidence was preserved. No real token, approval, +OpenBao, native side effect, deployment or migration was used. + +The scoped demand from CUST-WP-0080 is reconciled into WP0002 without promoting +all residuals. New WP0004 tracks the bounded source delivery and pending canonical +review/integration. The coordinator handles canonical sync after independent +review. Hub progress logging has its request/receipt here; no new workplan was +registered from the temporary checkout. + +Native acceptance requires the existing owner interfaces: + +- Approval T01: exact KeyCape issuer/audience/scope/tenant provenance receipt and + human-authorized release/migration with native verified entry/readback. Current + source identity tests do not prove issuer admission or deployment. +- Approval T04 / Audit: admit the refined source payload, drain actual lifecycle + rows and independently read receiver event ID/content; reconcile counts, + duplicate/lag/boundary handling, silence and scoped bearer revocation. Existing + local atomicity cannot prove omission resistance under source compromise. +- Approval T05 / Platform / Secrets: name actual served PEP revision/route and + actual independently authenticated read/consume client; establish trusted server + transport/custody/ingress. Validate fresh exact claim and signed Access decision, + approved native act, approval-free/full request bindings and immutable operation + plan before CAS. Preserve live one-shot journal admission, deadlines, unknown + outcomes, independently confirmed cleanup and approved recovery. +- Secrets and Access retain strict downstream consumption correspondence, + published policy/action/initiating-principal representation admission and + producer-compatible signed-decision trust. The outbox row does not validate + that a supplied decision ID exists or authorize a protected operation. + +The native proof must bind exact source/image/config, identity, operation/body, +CAS evidence, actual protected effect, event/source/receiver correspondence and +cleanup. Wrong actor/tenant/body/decision, expiry, replay, unavailability and +consume-before-effect failure are required refusals. Fixture success is never +reported as native acceptance. The existing deployment pin/evidence discrepancy +must be reconciled by its owner from real release evidence before rollout. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/closure-owner-workplan-baseline.md b/docs/changes/netkingdom-target-2026-10-16/approval-reference/closure-owner-workplan-baseline.md new file mode 100644 index 00000000..4d8496c4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/closure-owner-workplan-baseline.md @@ -0,0 +1,60 @@ +--- +id: APPROVAL-WP-0004 +type: workplan +title: "Retain exact authenticated consume source provenance" +domain: infotech +repo: approval-engine +status: active +flavor: implementation +owner: codex +topic_slug: netkingdom +created: "2026-10-16" +updated: "2026-10-16" +depends_on: [] +state_hub_workstream_id: "688edc60-a179-5618-90f1-468aadc2af58" +--- + +Explicit NetKingdom target demand under CUST-WP-0080 selects the source evidence +subset of APPROVAL-WP-0002-T01/T04/T05. INTENT basis: authenticated consumable +object, exact binding and atomic local evidence. Existing authoritative CAS, +confirmation transport and audit adapter are reused. No second approval/PDP +service, portable signer, foreign owner review or live action is added. + +## Preserve verified consumer and approved operation in the use source row + +```task +id: APPROVAL-WP-0004-T01 +status: done +priority: high +state_hub_task_id: "b0b0a5a9-f7e7-508b-96d0-4b02457b16fc" +``` + +Source implementation supplies the HTTP-verified Identity to consume and writes +its bounded metadata plus native act/PDP bindings into the existing atomic use +outbox. Body-provided provenance is ignored, internal calls retain null, original +idempotent evidence is immutable, and historical rows remain untouched. No DB +schema change, response/claim widening or identity scope change is needed. +Seven new source tests cover signature-verified HTTP consumption, restart, +adapter preservation, spoofed fields, denied identities, retry immutability and +rollback. Source checks do not establish native acceptance. + +## Integrate independently reviewed source and synchronize work records + +```task +id: APPROVAL-WP-0004-T02 +status: todo +priority: high +state_hub_task_id: "2b932ef8-e143-5bb7-856e-4cba4fb22c09" +``` + +The Custodian coordinator independently reviews the exact patch/manifest, then +integrates and synchronizes the canonical owner record. Temporary-checkout work +is not a canonical synchronized record or formal delegated-owner assent. + +Existing native delivery obligations remain live under APPROVAL-WP-0002: +T01 exact issuer/profile and release/migration, T04 admitted lifecycle source +delivery/reconciliation, T05 named served PEP consumption before protected effect. +Platform owns mediator transport/identity/custody, the one-shot journal and native +effect; Secrets owns its actual strict handoff and task-scoped backend. Access +owns the signed decision and exact request/action/representation binding. No +claim that an isolated source test closes any of these gates is made. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/approval-reference/implementation.patch new file mode 100644 index 00000000..6083b95c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/implementation.patch @@ -0,0 +1,288 @@ +diff --git a/approval_engine/api.py b/approval_engine/api.py +index 607ff19..440e4ac 100644 +--- a/approval_engine/api.py ++++ b/approval_engine/api.py +@@ -176,6 +176,7 @@ class App: + approval_id, + data.get("request_digest"), + decision_id=data.get("decision_id"), ++ consumer=identity, + ) + if "check" in path or path.endswith("/authorize"): + return 404, {"error": "not_found", "message": "no such surface"} +diff --git a/approval_engine/store.py b/approval_engine/store.py +index c30decc..eb02a24 100644 +--- a/approval_engine/store.py ++++ b/approval_engine/store.py +@@ -18,6 +18,7 @@ from pathlib import Path + from typing import Any, Callable + from uuid import uuid4 + ++from .auth import Identity + from .binding import binding_digest, canonical_binding, require_digest + from .errors import ( + Conflict, +@@ -750,6 +751,7 @@ class Engine: + request_digest: str | None, + *, + decision_id: str | None = None, ++ consumer: Identity | None = None, + ) -> dict[str, Any]: + """Atomically spend an approval for one decision-bound request. + +@@ -819,6 +821,19 @@ class Engine: + actor=row["actor"], + extra={ + "binding_digest": row["binding_digest"], ++ "binding": json.loads(row["binding_json"]), ++ "pdp_digest": row["pdp_digest"], ++ "pdp_path": bool(row["pdp_path"]), ++ # Only the HTTP boundary supplies an authenticated identity. ++ # Internal/legacy calls explicitly leave provenance unknown. ++ "consumer": ({ ++ "subject_id": consumer.subject, ++ "issuer": consumer.issuer, ++ "principal_type": consumer.principal_type, ++ "tenant": consumer.tenant, ++ "tenant_source": consumer.tenant_source, ++ "evidence_ref": consumer.evidence_ref, ++ } if consumer is not None else None), + "request_digest": digest, + **({"decision_id": decision_id} if decision_id else {}), + "threshold": self._threshold_evidence( +diff --git a/docs/outbox-contract.md b/docs/outbox-contract.md +index a15bdc8..14295a3 100644 +--- a/docs/outbox-contract.md ++++ b/docs/outbox-contract.md +@@ -130,3 +130,32 @@ where the identities are the point. A consumer that wants the threshold reads + Distinctness itself is a storage invariant rather than a recomputation: + `entries` is UNIQUE on `(approval_id, subject_id)`, so a repeat approver is + refused at insert. ++ ++## Exact use provenance (source refinement, 2026-10-16) ++ ++New `approval.use` rows additionally retain `details.binding` (the native ++five-field act), its existing `binding_digest`, nullable `pdp_digest` and ++explicit `pdp_path`. `request_digest` remains the full decision-bound request, ++and `decision_id` remains caller-supplied correlation, not a verified PDP verdict. ++These distinct digests must never be substituted for each other. ++ ++`details.consumer` records the authenticated consume caller's `subject_id`, ++`issuer`, `principal_type`, exact `tenant`, nullable verified `tenant_source` and ++token-fingerprint `evidence_ref`. It comes solely from the HTTP authenticator; ++body fields cannot override it. Top-level `actor` retains its existing meaning: ++the actor captured on the approved act. Consumer provenance distinguishes the ++service spending the object from that initiating actor. Bearer values, scopes, ++roles and assurance are not copied into this record. ++ ++In-process calls without an authenticated caller record `consumer: null`. ++Existing outbox rows remain unchanged; missing/null consumer evidence cannot be ++backfilled as authenticated. Same-digest retries preserve the first use row and ++never replace its original consumer or decision correspondence. All fields are ++inserted atomically with the consumption CAS, survive restart, and pass unchanged ++through the existing asynchronous audit adapter. No schema migration is needed. ++ ++This source evidence is not a portable signed receipt, execution grant or ++server-identity proof. Consumer confirmation still uses the existing trusted ++authenticated read/consume transport and a separate live execution journal. ++Audit source admission, deployed issuer/profile, lifecycle readback and actual ++served-PEP proof remain APPROVAL-WP-0002-T01/T04/T05 native gates. +diff --git a/tests/test_consume_provenance.py b/tests/test_consume_provenance.py +new file mode 100644 +index 0000000..f2b5d6a +--- /dev/null ++++ b/tests/test_consume_provenance.py +@@ -0,0 +1,110 @@ ++"""Source evidence binds authenticated consumer, approved act and PDP digest.""" ++import json ++ ++import pytest ++ ++from approval_engine.api import App, call ++from approval_engine.audit import audit_envelope ++from approval_engine.store import Engine ++from tests.conftest import FROZEN, binding, validity ++from tests.test_auth import _jwt ++from tests.test_tenant_provenance import authenticator ++ ++FULL = "sha256:" + "ab" * 32 ++FREE = "sha256:" + "cd" * 32 ++ ++ ++def approved(engine): ++ obj = engine.create(binding(), validity(), pdp_digest=FREE, pdp_path=True, ++ human_control=True) ++ engine.add_entry(obj.id, "human:founder", principal_type="human") ++ return obj ++ ++ ++def token(private, **overrides): ++ claims = dict(sub="service:mediator", tenant="tenant:platform", ++ principal_type="service", tenant_source="registration", ++ scope="approval:consume") ++ claims.update(overrides) ++ return _jwt(private, **claims) ++ ++ ++def consume(app, obj, bearer, **body): ++ return call(app, "POST", f"/v1/approvals/{obj.id}/consume", ++ {"request_digest": FULL, "decision_id": "decision:original", **body}, ++ authorization="Bearer " + bearer) ++ ++ ++def uses(engine): ++ return [r for r in engine.undrained() if r["class"] == "use"] ++ ++ ++def test_authenticated_consumer_and_exact_binding_survive_restart_and_drain(tmp_path): ++ private, auth = authenticator() ++ path = tmp_path / "approval.sqlite" ++ engine = Engine(path, clock=lambda: FROZEN) ++ obj = approved(engine) ++ bearer = token(private) ++ status, result = consume(App(engine, auth), obj, bearer, ++ consumer={"subject_id": "spoofed"}, pdp_digest="spoofed") ++ assert status == 200 and result["idempotent"] is False ++ event = uses(engine)[0] ++ details = event["payload"]["details"] ++ assert event["payload"]["actor"] == binding()["actor"] ++ assert details["binding"] == binding() ++ assert details["binding_digest"] == obj.binding_digest ++ assert details["pdp_digest"] == FREE and details["pdp_path"] is True ++ assert details["request_digest"] == FULL ++ assert details["decision_id"] == "decision:original" ++ assert details["consumer"] == { ++ "subject_id": "service:mediator", "issuer": "https://keycape.example", ++ "principal_type": "service", "tenant": "tenant:platform", ++ "tenant_source": "registration", "evidence_ref": auth.authenticate("Bearer " + bearer).evidence_ref, ++ } ++ assert bearer not in json.dumps(event) ++ assert "consumer" not in json.dumps(engine.claim(obj.id)) ++ engine.close() ++ engine = Engine(path, clock=lambda: FROZEN, auto_migrate=False) ++ assert uses(engine) == [event] ++ # A second authenticated caller cannot rewrite the first consumer evidence. ++ status, retry = consume(App(engine, auth), obj, token(private, sub="service:retry"), ++ decision_id="decision:other") ++ assert status == 200 and retry["idempotent"] is True ++ assert retry["decision_id"] == "decision:original" ++ assert uses(engine) == [event] ++ delivered = [] ++ engine.drain(lambda payload: delivered.append(audit_envelope(payload))) ++ use = next(e for e in delivered if e["type"] == "approval.use") ++ assert use["id"] == event["event_id"] ++ assert use["data"]["details"] == details ++ engine.close() ++ ++ ++@pytest.mark.parametrize("overrides,expected", [ ++ ({"tenant": "tenant:foreign"}, 403), ++ ({"principal_type": "human"}, 403), ++ ({"scope": "approval:read"}, 403), ++ ({"tenant_source": "spoofed"}, 401), ++]) ++def test_unadmitted_consumer_cannot_create_evidence(engine, overrides, expected): ++ private, auth = authenticator() ++ obj = approved(engine) ++ assert consume(App(engine, auth), obj, token(private, **overrides))[0] == expected ++ assert uses(engine) == [] and engine.get(obj.id).status == "approved" ++ ++ ++def test_outbox_failure_rolls_back_consumer_and_spend(engine): ++ private, auth = authenticator() ++ obj = approved(engine) ++ engine.fail_outbox = True ++ assert consume(App(engine, auth), obj, token(private))[0] == 503 ++ assert engine.get(obj.id).status == "approved" and uses(engine) == [] ++ engine.fail_outbox = False ++ assert consume(App(engine, auth), obj, token(private, sub="service:recovered"))[0] == 200 ++ assert uses(engine)[0]["payload"]["details"]["consumer"]["subject_id"] == "service:recovered" ++ ++ ++def test_internal_calls_do_not_invent_authenticated_consumer(engine): ++ obj = approved(engine) ++ engine.consume(obj.id, FULL) ++ assert uses(engine)[0]["payload"]["details"]["consumer"] is None +diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +index e3ff5ca..856ebc7 100644 +--- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md ++++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +@@ -34,6 +34,15 @@ Current status: blocked after the 2026-10-04 dependency review. T02, T03 and + T06 are complete. T01, T04 and T05 use `wait` for blocked task status; their + remaining owner receipts are specified in `docs/production-dependencies.md`. + ++2026-10-16 target-architecture demand reconciliation: CUST-WP-0080 explicitly ++selects necessary authenticated-consume/provenance/audit and exact served-PEP ++interfaces from T01/T04/T05. APPROVAL-WP-0004 implements only the reachable ++source use-evidence gap: verified consuming identity and approved act/PDP binding ++in the existing transactional outbox. This scoped implementation demand does ++not activate unrelated residuals or authorize credentials, migration, rollout, ++production approvals or native effects. T01/T04/T05 remain wait for their ++existing independently owned native receipts; this workplan remains residual. ++ + Review completed 2026-09-01. The plan is active. Production mode will verify + KeyCape JWT signatures and exact issuer/audience/scopes; local caller-supplied + identity never becomes authenticated evidence. SQLite remains the first +diff --git a/workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md b/workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md +new file mode 100644 +index 0000000..b9bc3fc +--- /dev/null ++++ b/workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md +@@ -0,0 +1,57 @@ ++--- ++id: APPROVAL-WP-0004 ++type: workplan ++title: "Retain exact authenticated consume source provenance" ++domain: infotech ++repo: approval-engine ++status: active ++flavor: implementation ++owner: codex ++topic_slug: netkingdom ++created: "2026-10-16" ++updated: "2026-10-16" ++depends_on: [] ++--- ++ ++Explicit NetKingdom target demand under CUST-WP-0080 selects the source evidence ++subset of APPROVAL-WP-0002-T01/T04/T05. INTENT basis: authenticated consumable ++object, exact binding and atomic local evidence. Existing authoritative CAS, ++confirmation transport and audit adapter are reused. No second approval/PDP ++service, portable signer, foreign owner review or live action is added. ++ ++## Preserve verified consumer and approved operation in the use source row ++ ++```task ++id: APPROVAL-WP-0004-T01 ++status: done ++priority: high ++``` ++ ++Source implementation supplies the HTTP-verified Identity to consume and writes ++its bounded metadata plus native act/PDP bindings into the existing atomic use ++outbox. Body-provided provenance is ignored, internal calls retain null, original ++idempotent evidence is immutable, and historical rows remain untouched. No DB ++schema change, response/claim widening or identity scope change is needed. ++Seven new source tests cover signature-verified HTTP consumption, restart, ++adapter preservation, spoofed fields, denied identities, retry immutability and ++rollback. Source checks do not establish native acceptance. ++ ++## Integrate independently reviewed source and synchronize work records ++ ++```task ++id: APPROVAL-WP-0004-T02 ++status: todo ++priority: high ++``` ++ ++The Custodian coordinator independently reviews the exact patch/manifest, then ++integrates and synchronizes the canonical owner record. Temporary-checkout work ++is not a canonical synchronized record or formal delegated-owner assent. ++ ++Existing native delivery obligations remain live under APPROVAL-WP-0002: ++T01 exact issuer/profile and release/migration, T04 admitted lifecycle source ++delivery/reconciliation, T05 named served PEP consumption before protected effect. ++Platform owns mediator transport/identity/custody, the one-shot journal and native ++effect; Secrets owns its actual strict handoff and task-scoped backend. Access ++owns the signed decision and exact request/action/representation binding. No ++claim that an isolated source test closes any of these gates is made. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/approval-reference/owner-dirty-baseline.patch new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/patch-check.json b/docs/changes/netkingdom-target-2026-10-16/approval-reference/patch-check.json new file mode 100644 index 00000000..e998d35d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/patch-check.json @@ -0,0 +1,5 @@ +{ + "exit_code": 0, + "stdout": "", + "stderr": "" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-receipt.json new file mode 100644 index 00000000..b0916268 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-receipt.json @@ -0,0 +1 @@ +{"workplan_id":"4fa25ad5-f5d0-5592-aa59-085f8ee3edaf","id":"c6bddc94-a264-4c82-8b28-6507e2019624","topic_id":null,"task_id":null,"decision_id":null,"event_type":"note","summary":"approval-engine owner source preparation under CUST-WP-0080: exact authenticated consuming identity and approved act/PDP binding retained atomically in existing approval.use outbox. Isolated patch/manifest in docs/changes/netkingdom-target-2026-10-16/approval-reference. 44 focused passed; full 277 passed, one unchanged baseline deployment evidence pin mismatch reproduced. Canonical review/integration/sync pending coordinator; APPROVAL-WP-0002 native gates remain open; no credential/deploy/native action.","detail":null,"author":"codex","session_id":null,"created_at":"2026-10-16T00:06:37.193148Z","workstream_id":"4fa25ad5-f5d0-5592-aa59-085f8ee3edaf"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-request.json b/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-request.json new file mode 100644 index 00000000..5c8287e9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/progress-request.json @@ -0,0 +1 @@ +{"summary": "approval-engine owner source preparation under CUST-WP-0080: exact authenticated consuming identity and approved act/PDP binding retained atomically in existing approval.use outbox. Isolated patch/manifest in docs/changes/netkingdom-target-2026-10-16/approval-reference. 44 focused passed; full 277 passed, one unchanged baseline deployment evidence pin mismatch reproduced. Canonical review/integration/sync pending coordinator; APPROVAL-WP-0002 native gates remain open; no credential/deploy/native action.", "event_type": "note", "author": "codex", "workplan_id": "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf"} \ No newline at end of file diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/source-closure.patch b/docs/changes/netkingdom-target-2026-10-16/approval-reference/source-closure.patch new file mode 100644 index 00000000..df716d35 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/source-closure.patch @@ -0,0 +1,57 @@ +--- a/workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md ++++ b/workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md +@@ -4,7 +4,7 @@ + title: "Retain exact authenticated consume source provenance" + domain: infotech + repo: approval-engine +-status: active ++status: finished + flavor: implementation + owner: codex + topic_slug: netkingdom +@@ -42,19 +42,40 @@ + + ```task + id: APPROVAL-WP-0004-T02 +-status: todo ++status: done + priority: high + state_hub_task_id: "2b932ef8-e143-5bb7-856e-4cba4fb22c09" + ``` + +-The Custodian coordinator independently reviews the exact patch/manifest, then +-integrates and synchronizes the canonical owner record. Temporary-checkout work +-is not a canonical synchronized record or formal delegated-owner assent. ++The Custodian coordinator reviewed and integrated the exact source patch ++`a6bf77fa8b6916f423db7832f475e38747b828dd632a88f9631131e28e3230ee` as commit ++`477c683991fda3c01e2af98a18f5a8719ee9dbc9`. Canonical source hashes and integration ++are recorded in the-custodian's ++`docs/changes/netkingdom-target-2026-10-16/approval-reference/canonical-integration.json`. ++Normal State Hub sync/publication is recorded in the same change directory's ++`sync-session-61857.json`: bindings synchronized 5/5, source commit pushed, zero ++failures, zero automation errors and nine warnings. The generated-only writeback ++commit refusal remains recorded in that receipt; generated identifiers are ++retained. Published origin HEAD was independently read back as the exact commit ++above on 2026-10-16. This completes scoped source integration/publication, not ++formal delegated-owner assent or native acceptance. The closure-file update ++itself still follows the coordinator's normal review and synchronization. + + Existing native delivery obligations remain live under APPROVAL-WP-0002: + T01 exact issuer/profile and release/migration, T04 admitted lifecycle source + delivery/reconciliation, T05 named served PEP consumption before protected effect. + Platform owns mediator transport/identity/custody, the one-shot journal and native +-effect; Secrets owns its actual strict handoff and task-scoped backend. Access ++effect; Secrets owns implementation of the strict handoff and task-scoped ++backend. The strict task handoff is not implemented at Secrets canonical ++`c80a944105a00220cd3c1a007709e72d830dd08d` ++and remains its next implementation work. Access + owns the signed decision and exact request/action/representation binding. No + claim that an isolated source test closes any of these gates is made. ++ ++No scoped source work remains in APPROVAL-WP-0004. The native obligations above ++were live in APPROVAL-WP-0002 before this work and remain there under their ++existing identifiers and wait statuses; no new residual record is needed and ++no residual implementation is promoted by this closure. Further implementation ++of a necessary WP0002 residual must be explicitly selected under the continuing ++CUST-WP-0080 target demand and recorded with its precise owner/action scope; ++credential, rollout and native effect steps retain their separate authority gates. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-reference/validation.json b/docs/changes/netkingdom-target-2026-10-16/approval-reference/validation.json new file mode 100644 index 00000000..1d26fc5e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-reference/validation.json @@ -0,0 +1,20 @@ +{ + "focused": { + "command": "python3 -m pytest -q tests/test_consume_provenance.py tests/test_outbox.py tests/test_execution_confirmation.py", + "passed": 44 + }, + "full": { + "command": "python3 -m pytest -q", + "passed": 277, + "failed": 1, + "failure": "tests/test_deploy_manifest.py::test_pinned_image_schema_drift_is_acknowledged", + "reason": "manifest image digest sha256:16e5d222d58e179f4836f649e8f8128321a8b644d29dc426b4d21a65ff97845d differs from historical release evidence digest sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49" + }, + "baseline_failure_reproduced": { + "checkout": "/tmp/netkingdom-approval-baseline-20261016", + "command": "python3 -m pytest -q tests/test_deploy_manifest.py::test_pinned_image_schema_drift_is_acknowledged", + "failed": 1, + "same_mismatch": true + }, + "diff_check": "passed" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/README.md b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/README.md new file mode 100644 index 00000000..a0a68ccc --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/README.md @@ -0,0 +1,55 @@ +# Approval authoritative timestamp precision repair + +Frozen source repair under APPROVAL-WP-0006-T01, base +`3485415b823faf4f3b682079195e186666b0ed2b`. Isolated source: +`/tmp/netkingdom-approval-timestamp-precision-20261016`. +Canonical source, prior WP0005 closure and registrar-generated identifiers remain +untouched. Exact observed owner dirt is preserved in owner-dirty-baseline.patch. + +The coordinator counterexample demonstrates a real validity defect: clock .750 +was rounded to .000 and consumed an approval expiring at .500. The same rounding +made legitimate immediate consumption evidence appear before Access's precise +signed NotBefore. The repair retains actual aware UTC microseconds in utc_now +and ISO serialization. First consumption samples exactly once after SQLite's +write lock is acquired and uses that one instant for validity, immutable receipt, +updated_at and the use outbox's occurrence/creation time. No Access backdating, +consumer tolerance, caller sleep, schema change or new approval protocol exists. + +Historical whole-second rows/readback and same-digest idempotent receipts retain +their original timestamps/decision IDs without fabricated precision. All new +source timestamps, including claims/freshness, preserve clock precision. The +existing Go helper already accepts microseconds; its code is unchanged, and only +its README's old whole-second producer statement is corrected. + +Validation: 89 focused tests pass, including actual SQLite lock waits crossing +expiry, immediate fractional receipt/readback/restart, one sampled clock and +identical receipt/outbox time, unchanged old evidence and failed-outbox rollback. +Full source suite: 298 pass, one known historical release-evidence/image-pin +mismatch remains. Original regressions failed five cases before repair. An +initial repaired test tried issuing an already expired approval and correctly +failed; its fixture now issues before expiry and then advances. These outcomes +remain recorded instead of being rewritten into success. + +`implementation.patch` SHA256: +`74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd`. +`approval_engine/store.py` SHA256: +`866b275ed2d844d0aa9090ce9899ce955c6bfe32a493c2645c86e72efa343e86`. +The original standalone repair is frozen independently of later temporal review. +`owner-review-record.patch` applies after it, recording T02 progress and the +Approval-owned formal review in the owner repo; no canonical write is performed. + +The supplementary review approves Approval facts/time only for exact Access R3 +full patch `46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6`. +All 31 frozen source hashes match; independent temporal/retained-registry HTTP +race checks pass. Signed claim-free DENY supplies authenticated normalized facts +before issue; final source join compares that approved act/PDP binding without +rewriting the original claim. Exact operation lifetime must fit original/fresh +fact bounds. Default freshness30 cannot cover backend30 plus cleanup5; +fixture freshness120 is explicit synthetic configuration, never a native grant. + +Prior R1 review and WP0005 closure remain exact historical records. Immediate +strict CAS and whole-path remeasurement use the final separately pinned repaired +Approval source, Access R3 and Secrets successor; their result is not assumed +here. Native issuer/profile, source/custody/config/image admission, actual audit +receiver and served PEP/CAS/journal/effect/cleanup remain the existing WP0002 +T01/T04/T05 and other-owner obligations. No authority promotion is recorded. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/manifest.json new file mode 100644 index 00000000..ee3d43d0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/manifest.json @@ -0,0 +1,13 @@ +{ + "patch": "review-sha256-prefix.patch", + "patch_sha256": "sha256:7ff222f5a0968d8b580382d21246c987f03990aa152aec6f61edeb3a367963be", + "original_git_blob": "deefaac30c988de61ee26a08f29482ae76b39a0b", + "formatted_git_blob": "4a0b3e3404a36544f7e90e974a76bd0aab946783", + "files_sha256": { + "updated-review.json": "sha256:2bfc6122456737918f5cf5f2c5f62eb30886b7ea911b601589ab08fc6225e98c", + "owner-reaffirmation.json": "sha256:bdaa2bd086d0d93e3d799cab6f7f6b59447572a88418003999fc2aa481cfa4a5", + "review-sha256-prefix.patch": "sha256:7ff222f5a0968d8b580382d21246c987f03990aa152aec6f61edeb3a367963be", + "validation.json": "sha256:ee50e4ca1a5e060e9d227bbe3f4868396e05719286890c75965086de30bac1b6" + }, + "canonical_mutated_by_owner": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/owner-reaffirmation.json b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/owner-reaffirmation.json new file mode 100644 index 00000000..89ee9926 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/owner-reaffirmation.json @@ -0,0 +1,13 @@ +{ + "reviewer": "approval-engine-owner (agent approval-engine, codex)", + "decision": "approved", + "comment": "APPROVAL-WP-0006-T02 supplemental source review formatting only. Seven explicit sha256: prefixes label existing public digest metadata in values and prose. Removing only that prefix restores the exact original review bytes; digest values, technical judgement, reviewed source revisions and authority exclusions are unchanged. The owner reaffirms the original Approval-only fact/time review and no native authority is promoted. This is a new narrow formatting delta; original frozen repair/review artifacts remain historical and immutable.", + "target": "docs/reviews/2026-10-16-fractional-consumption-temporal-approval-owner-review.json", + "original_review_sha256": "sha256:fd5a00c498940fcfeaf188bded42de2f55b2fd04d0f78f294cc00610f5af3a6b", + "formatted_review_sha256": "sha256:2bfc6122456737918f5cf5f2c5f62eb30886b7ea911b601589ab08fc6225e98c", + "format_patch_sha256": "sha256:7ff222f5a0968d8b580382d21246c987f03990aa152aec6f61edeb3a367963be", + "normal_scanner_result": "clean after explicit public digest labels; original blocked blob independently reproduced with unchanged pinned scanner", + "source_implementation_changed": false, + "canonical_mutated_by_owner": false, + "native_accepted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/updated-review.json b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/updated-review.json new file mode 100644 index 00000000..4a0b3e34 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/updated-review.json @@ -0,0 +1,46 @@ +{ + "reviewer": "approval-engine-owner (agent approval-engine, codex)", + "decision": "approved", + "comment": "INTENT Purpose (valid right now), The Claim Shape and Consumption Ordering: approved for bounded Approval source time precision repair and Approval-owned fact/temporal correspondence in exact Access R3 source. Work APPROVAL-WP-0006-T01/T02. Repair patch SHA256 sha256:74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd against 3485415b823faf4f3b682079195e186666b0ed2b; store.py SHA256 sha256:866b275ed2d844d0aa9090ce9899ce955c6bfe32a493c2645c86e72efa343e86. Access R3 full patch SHA256 sha256:46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6, delta sha256:17ecf399c50c07845c545b31cad4022200eb20dfb760595aa76a271e9cee7521; all 31 frozen source hashes matched. This review does not assent to Access policy/other-owner behavior, native credentials/trust, deployment or effects.", + "reviewed_repair_patch_sha256": "sha256:74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd", + "reviewed_access_full_r3_sha256": "sha256:46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6", + "reviewed_access_r2_to_r3_sha256": "sha256:17ecf399c50c07845c545b31cad4022200eb20dfb760595aa76a271e9cee7521", + "approval_time_findings": [ + "utc_now and iso preserve actual aware microseconds; no schema or binding algorithm change", + "First consume samples one authoritative time only after BEGINIMMEDIATE obtains the lock; identical instant checks validity, persists receipt/update and creates use outbox timestamp", + "Expiry remains exclusive; actual clock .750 refuses expiry .500; post-lock expiry cannot borrow pre-lock admission", + "Whole-second old rows, readback and idempotent original decision/timestamp are preserved without fabricated precision; no unconsume, retry authority or tolerance", + "No Access NotBefore backdating, caller wait/sleep acceptance or consumer timing waiver is needed" + ], + "access_fact_temporal_findings": [ + "Signed claim-free DENY normalization receipt supplies existing producer-evaluated native act and approval-free digest before issue, never permission; requester consumer independently authenticates it", + "Actual retained registry normalization is source-owned and number-preserving; final join compares the approved act/PDP digest and refuses drift instead of refreshing old context", + "Original and newly fetched facts are validated separately; no mutation of original claim/context or either request digest", + "Exact immutable operation deadline must fit caller, original/fetched freshness and approval validity; decision is precise post-evaluation NotBefore to exact operation deadline", + "Entire remaining interval must cover declared backendTTL plus 5-second cleanup reserve and source maximum 120; this review does not approve that policy or native budget", + "Default Approval freshness 30 cannot satisfy minimum 30+5; fixture freshness 120 is explicit synthetic configuration, not a producer default flip or native grant" + ], + "validation": { + "repair_focused_passed": 89, + "repair_full_passed": 298, + "repair_full_failed_preexisting": 1, + "baseline_counterexamples_failed": 5, + "access_independent_command": "go test -race -count=1 ./internal/decision ./cmd/access-engine -run TestTrusted", + "access_independent_result": "passed" + }, + "retained_failures": [ + "coordinator-counterexample.json documents canonical expired fractional approval consumed", + "Initial repair expiry test attempted already-expired issuance; repaired engine correctly refused; fixture corrected to issue before expiry then advance", + "Existing deployment pin/historical release evidence mismatch remains unchanged" + ], + "prior_evidence": "WP0005 closure and6079edR1review remain exact historical evidence; this is a new precision task and new exactR3 review", + "remaining_gates": [ + "Coordinator independent source integration/publication/synchronization at exact repair and Access/Secrets successor source pins", + "Actual immediate strict CAS/whole-path remeasurement must use unchanged precise signed NotBefore and no sleep/waiver", + "Native issuer/profile, CA/source/read credential/store tenant admission and schema/image/config rollout remain existing WP0002T01", + "Actual use event/audit receiver reconciliation remains WP0002T04", + "Served PEP original decision/request CAS before effect, live one-shot journal, deadlines and confirmed cleanup remain WP0002T05 and existing Secrets/Platform work" + ], + "native_accepted": false, + "authority_promotion": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/validation.json b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/validation.json new file mode 100644 index 00000000..a575e350 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/formatting-r1/validation.json @@ -0,0 +1,24 @@ +{ + "scanner_command": "python3 /home/worsch/repo-manager/src/repo_manager/secret_scan.py --path /tmp/approval-review-formatting-20261016 --binary /home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks", + "scanner_version": "8.30.1", + "scanner_sha256": "sha256:88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "before": { + "status": "blocked", + "scanned_blobs": 1, + "blocked_blob_ids": [ + "deefaac30c988de61ee26a08f29482ae76b39a0b" + ] + }, + "after": { + "status": "clean", + "scanned_blobs": 1, + "blocked_blob_ids": [], + "formatted_blob_id": "4a0b3e3404a36544f7e90e974a76bd0aab946783" + }, + "prefixes_added": 7, + "all_digest_bytes_preserved": true, + "all_other_bytes_preserved": true, + "JSON_valid": true, + "scanner_config_ignores_bypassed": false, + "canonical_mutated_by_owner": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/implementation.patch new file mode 100644 index 00000000..e47e4851 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/implementation.patch @@ -0,0 +1,303 @@ +diff --git a/adapters/go/approvalfact/README.md b/adapters/go/approvalfact/README.md +index fdffd1f..1b67b64 100644 +--- a/adapters/go/approvalfact/README.md ++++ b/adapters/go/approvalfact/README.md +@@ -36,7 +36,8 @@ Canonical float/string formatting follows Python source, including negative + floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate + object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. + Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at +-most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++most nanoseconds with strict zoned RFC3339; the precision-repaired Python ++producer preserves microseconds, while historical whole-second strings remain valid. + These explicit parser/resource limits fail closed, never normalize authority. + + `go test -race ./...` checks the portable producer vectors, typed fact pairing, +diff --git a/approval_engine/store.py b/approval_engine/store.py +index eb02a24..e9444d9 100644 +--- a/approval_engine/store.py ++++ b/approval_engine/store.py +@@ -103,11 +103,11 @@ def utc_now(clock: Callable[[], datetime] | None = None) -> datetime: + now = datetime.now(timezone.utc) + if now.tzinfo is None: + now = now.replace(tzinfo=timezone.utc) +- return now.astimezone(timezone.utc).replace(microsecond=0) ++ return now.astimezone(timezone.utc) + + + def iso(ts: datetime) -> str: +- return ts.astimezone(timezone.utc).replace(microsecond=0).isoformat() ++ return ts.astimezone(timezone.utc).isoformat() + + + def parse_iso(value: str) -> datetime: +@@ -768,7 +768,6 @@ class Engine: + ): + raise Unprocessable("decision_id must be a non-empty string") + conn = self._conn() +- now = iso(self.now()) + idempotent = False + try: + conn.execute("BEGIN IMMEDIATE") +@@ -794,7 +793,11 @@ class Engine: + if row["status"] != "approved": + conn.rollback() + raise Conflict(f"cannot consume from status {row['status']}") +- if self.now() < parse_iso(row["not_before"]) or self.now() >= parse_iso(row["expires_at"]): ++ # Lock acquisition can wait past a validity boundary. One post-lock ++ # instant is authoritative for both the check and its durable proof. ++ consumed_at = self.now() ++ now = iso(consumed_at) ++ if consumed_at < parse_iso(row["not_before"]) or consumed_at >= parse_iso(row["expires_at"]): + conn.rollback() + raise Conflict("cannot consume outside validity window") + if row["human_control"]: +@@ -819,6 +822,7 @@ class Engine: + "use", + approval_id, + actor=row["actor"], ++ observed_at=consumed_at, + extra={ + "binding_digest": row["binding_digest"], + "binding": json.loads(row["binding_json"]), +@@ -911,13 +915,14 @@ class Engine: + *, + actor: str | None, + extra: dict[str, Any] | None = None, ++ observed_at: datetime | None = None, + ) -> str: + if self.fail_outbox: + raise StoreUnavailable("outbox insert failed") + if event_class not in EVENT_CLASSES: + raise Unprocessable(f"unknown event class {event_class}") + event_id = str(uuid4()) +- created = iso(self.now()) ++ created = iso(observed_at if observed_at is not None else self.now()) + resource = f"approval:{approval_id}" if approval_id else "approval-engine:heartbeat" + details = {"class": event_class, **(extra or {})} + if approval_id: +diff --git a/docs/approval-consumption.md b/docs/approval-consumption.md +index f85ab28..45cbb96 100644 +--- a/docs/approval-consumption.md ++++ b/docs/approval-consumption.md +@@ -42,6 +42,24 @@ after consumption, the approval remains spent and a retry needs a new approval. + The response is mutation evidence, not a permission decision. It contains no + `effect`, `allow`, `deny`, or decision result. + ++## Authoritative time and fractional precision ++ ++New source preserves the actual aware UTC clock's microseconds rather than ++rounding down to whole seconds. The first consume samples one instant after ++`BEGIN IMMEDIATE` acquires its write lock, then uses that instant for the ++not-before/inclusive and expiry/exclusive check, persisted `consumed_at` and ++`updated_at`, and use-event occurrence/creation time. Lock wait cannot borrow ++an earlier validity instant or stamp evidence before actual admission. ++ ++Existing whole-second ISO strings remain valid and are never backfilled with ++invented fractions. Idempotent responses and object readback retain the original ++recorded timestamp/decision exactly, even after expiry; they do not renew ++execution authority. The store schema, API keys, CAS/digest correspondence and ++no-unconsume rule are unchanged. Claim observation/freshness and other new ++source timestamps also retain actual clock precision. Consumers must parse ++aware ISO/RFC3339 timestamps including fractional seconds instead of assuming ++a fixed-width whole-second string. No clock skew or early-use tolerance is added. ++ + ## The claim response is not an `ActionAuthorization` + + `GET /v1/approvals/{id}/claim` serves the **approval-claim** envelope defined in +diff --git a/tests/test_timestamp_precision.py b/tests/test_timestamp_precision.py +new file mode 100644 +index 0000000..42fb5bc +--- /dev/null ++++ b/tests/test_timestamp_precision.py +@@ -0,0 +1,130 @@ ++"""Actual source clock, locking and original immutable receipt precision.""" ++from datetime import timedelta, timezone ++import sqlite3 ++import threading ++from concurrent.futures import ThreadPoolExecutor ++ ++import pytest ++ ++from approval_engine.errors import Conflict, StoreUnavailable ++from approval_engine.store import Engine, iso, utc_now ++from tests.conftest import FROZEN, binding ++ ++DIGEST = "sha256:" + "ab" * 32 ++ ++ ++def issue(engine, start, end): ++ obj = engine.create(binding(), {"not_before": start.isoformat(), "expires_at": end.isoformat()}) ++ return engine.add_entry(obj.id, "human", principal_type="human") ++ ++ ++def uses(engine): ++ return [row for row in engine.undrained() if row["class"] == "use"] ++ ++ ++def test_clock_and_iso_preserve_aware_precision_and_legacy_seconds(): ++ now = FROZEN.replace(microsecond=750123) ++ offset = now.astimezone(timezone(timedelta(hours=2))) ++ assert utc_now(lambda: offset) == now ++ assert iso(offset) == "2026-08-29T12:00:00.750123+00:00" ++ assert iso(FROZEN) == "2026-08-29T12:00:00+00:00" ++ ++ ++def test_fractional_expiry_refuses_actual_canonical_counterexample(): ++ engine = Engine(clock=lambda: FROZEN) ++ obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN.replace(microsecond=500000)) ++ engine.clock = lambda: FROZEN.replace(microsecond=750000) ++ assert engine.claim(obj.id)["valid_now"] is False ++ with pytest.raises(Conflict): ++ engine.consume(obj.id, DIGEST, decision_id="decision:original") ++ assert uses(engine) == [] ++ ++ ++def test_immediate_fractional_receipt_restart_and_outbox_use_one_instant(tmp_path): ++ current = [FROZEN.replace(microsecond=750123)] ++ path = tmp_path/"approval.sqlite" ++ engine = Engine(path, clock=lambda: current[0]) ++ obj = issue(engine, FROZEN.replace(microsecond=700000), FROZEN+timedelta(seconds=1)) ++ samples = [] ++ def clock(): ++ value = current[0] + timedelta(microseconds=len(samples)) ++ samples.append(value) ++ return value ++ engine.clock = clock ++ result = engine.consume(obj.id, DIGEST, decision_id="decision:original") ++ assert samples == [current[0]] ++ assert result["consumed_at"] == current[0].isoformat() ++ assert result["consumed_at"] >= FROZEN.replace(microsecond=700001).isoformat() ++ event = uses(engine)[0] ++ assert event["created_at"] == event["payload"]["observed_at"] == result["consumed_at"] ++ engine.close() ++ engine = Engine(path, clock=lambda: FROZEN+timedelta(days=1), auto_migrate=False) ++ assert engine.get(obj.id).as_dict()["consumption"]["consumed_at"] == result["consumed_at"] ++ retry = engine.consume(obj.id, DIGEST, decision_id="decision:new") ++ assert retry == {**result, "idempotent": True} ++ assert uses(engine) == [event] ++ engine.close() ++ ++ ++@pytest.mark.parametrize("after,accepted", [(450000, True), (750000, False)]) ++def test_consume_samples_after_real_sqlite_lock_wait(tmp_path, after, accepted): ++ current = [FROZEN.replace(microsecond=250000)] ++ path = tmp_path/"approval.sqlite" ++ engine = Engine(path, clock=lambda: current[0]) ++ obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN.replace(microsecond=500000)) ++ entered = threading.Event() ++ initialized = threading.Event() ++ start = threading.Event() ++ samples = [] ++ def consume(): ++ conn = engine._conn() ++ conn.set_trace_callback(lambda statement: entered.set() if statement == "BEGIN IMMEDIATE" else None) ++ initialized.set() ++ assert start.wait(2) ++ def clock(): ++ samples.append(current[0]) ++ return current[0] ++ engine.clock = clock ++ try: ++ return engine.consume(obj.id, DIGEST, decision_id="decision:original") ++ finally: ++ engine.close() ++ with ThreadPoolExecutor(max_workers=1) as pool: ++ future = pool.submit(consume) ++ assert initialized.wait(2) ++ lock = sqlite3.connect(path) ++ lock.execute("BEGIN IMMEDIATE") ++ start.set() ++ assert entered.wait(2) ++ current[0] = FROZEN.replace(microsecond=after) ++ lock.commit() ++ lock.close() ++ if accepted: ++ assert future.result(timeout=2)["consumed_at"] == current[0].isoformat() ++ else: ++ with pytest.raises(Conflict): ++ future.result(timeout=2) ++ assert samples == [current[0]] ++ assert len(uses(engine)) == int(accepted) ++ engine.close() ++ ++ ++def test_old_second_precision_receipt_and_payload_are_never_backfilled(tmp_path): ++ engine = Engine(tmp_path/"old.sqlite", clock=lambda: FROZEN) ++ obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN+timedelta(seconds=1)) ++ original = engine.consume(obj.id, DIGEST, decision_id="decision:old") ++ event = uses(engine)[0] ++ assert original["consumed_at"] == "2026-08-29T12:00:00+00:00" ++ engine.clock = lambda: FROZEN + timedelta(days=1, microseconds=123456) ++ assert engine.consume(obj.id, DIGEST, decision_id="decision:new") == {**original, "idempotent": True} ++ assert uses(engine) == [event] ++ ++ ++def test_fractional_use_outbox_failure_rolls_back(engine): ++ engine.clock = lambda: FROZEN.replace(microsecond=123456) ++ obj = issue(engine, FROZEN, FROZEN+timedelta(seconds=1)) ++ engine.fail_outbox = True ++ with pytest.raises(StoreUnavailable): ++ engine.consume(obj.id, DIGEST) ++ assert engine.get(obj.id).consumed_at is None ++ assert uses(engine) == [] +diff --git a/workplans/APPROVAL-WP-0006-fractional-consumption-time.md b/workplans/APPROVAL-WP-0006-fractional-consumption-time.md +new file mode 100644 +index 0000000..be9ab93 +--- /dev/null ++++ b/workplans/APPROVAL-WP-0006-fractional-consumption-time.md +@@ -0,0 +1,55 @@ ++--- ++id: APPROVAL-WP-0006 ++type: workplan ++title: "Preserve fractional authoritative approval and consumption time" ++domain: infotech ++repo: approval-engine ++status: active ++flavor: implementation ++owner: codex ++topic_slug: netkingdom ++created: "2026-10-16" ++updated: "2026-10-16" ++depends_on: [] ++--- ++ ++Explicit CUST-WP-0080 precision defect demand: actual clock .750 was rounded to ++.000, accepting consume beyond a .500 expiry and recording evidence earlier than ++the actual signed decision. INTENT basis: valid right now, exact-bound atomic ++consumption and faithful source evidence. Fix source time, not Access backdating ++or a consumer waiver. This does not promote native issuer/deployment authority. ++ ++## Preserve clock precision and one post-lock consumption instant ++ ++```task ++id: APPROVAL-WP-0006-T01 ++status: done ++priority: high ++``` ++ ++Preserve aware UTC microseconds through clock/ISO formatting. After acquisition ++of the SQLite consume lock, sample one time for validity, stored consumption ++receipt/update and use-event occurrence. Preserve old second-precision rows, ++original idempotent receipt/decision and local transactional atomicity. ++ ++Seven added regressions: baseline five failed/two passed; repaired 89 focused ++checks pass. Full source suite: 298 passed, one pre-existing deployment pin vs ++historical release-evidence mismatch retained. One initial test attempted issue ++after expiry and was corrected to issue before expiry, then advance the clock; ++the repaired source correctly refused that expired setup. Source change needs no ++schema migration and preserves old rows/idempotent timestamps exactly. ++ ++## Review temporal source successor and hand off independently pinned repair ++ ++```task ++id: APPROVAL-WP-0006-T02 ++status: todo ++priority: high ++``` ++ ++Supply exact patch/test/source review evidence for coordinator integration and ++Access/Secrets immediate strict CAS remeasurement. Review only Approval facts, ++normalized preapproval correspondence and temporal bounds on the exact successor ++source. Existing WP0005 closure/R1 review remain historical; native issuer, ++audit receiver, served-PEP/CAS/effect and cleanup gates remain WP0002 T01/T04/T05 ++and the existing Access/Secrets/Platform records. No native gate is closed here. diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/manifest.json b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/manifest.json new file mode 100644 index 00000000..69c2da60 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/manifest.json @@ -0,0 +1,20 @@ +{ + "owner": "approval-engine", + "source_revision": "3485415b823faf4f3b682079195e186666b0ed2b", + "work_item": "APPROVAL-WP-0006-T01", + "patch_sha256": "74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd", + "owner_dirty_baseline_sha256": "d076c2525b659279bddf1c56c037a77cb67bc0ad59b746cc1c377b4f1a6949e9", + "files": { + "adapters/go/approvalfact/README.md": "ac8f85d83c50a59c74a0d6ffa5f2bba9a414b2aab4e11a49c3dcc16e1b5201fc", + "approval_engine/store.py": "866b275ed2d844d0aa9090ce9899ce955c6bfe32a493c2645c86e72efa343e86", + "docs/approval-consumption.md": "31eee8a1a975bc1ad6a77ca0973208b2fc636c68cb0c748cb8aa666d5618be85", + "tests/test_timestamp_precision.py": "cbffa9265546a685b802702f5baf4e9d9ee822aaacab83c9a15c6c7316ee989b", + "workplans/APPROVAL-WP-0006-fractional-consumption-time.md": "7fe8fddb06894e87a89236eda3a3292d1d1495404b1b6a368bbfb0159d8c17ce" + }, + "canonical_written": false, + "native_accepted": false, + "status": "standalone precision repair source frozen; exact Access R3 Approval fact/temporal review delivered separately", + "supplementary_owner_review_sha256": "fd5a00c498940fcfeaf188bded42de2f55b2fd04d0f78f294cc00610f5af3a6b", + "supplementary_owner_review_record_patch_sha256": "fd69252bfa12825158eb2333d4d3816a1cdde5f67b58e937dd4f8bd321627073", + "owner_review_record_applies_after": "implementation.patch" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/owner-dirty-baseline.patch b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/owner-dirty-baseline.patch new file mode 100644 index 00000000..1ac34242 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/approval-timestamp-precision/owner-dirty-baseline.patch @@ -0,0 +1,121 @@ +diff --git a/.custodian-brief.md b/.custodian-brief.md +index 4e8f8d3..cbefd12 100644 +--- a/.custodian-brief.md ++++ b/.custodian-brief.md +@@ -2,11 +2,17 @@ + # Custodian Brief — approval-engine + + **Domain:** infotech +-**Last synced:** 2026-10-11 13:46 UTC ++**Last synced:** 2026-10-16 14:28 UTC + **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + + ## Active Workstreams + ++### Approval fact contract and reusable Go data adapter ++Progress: 2/3 done | workplan_id: `777b17de-2c1e-5ce5-aa82-c99e5137e956` ++ ++**Open tasks:** ++- · Independently integrate owner contract and coordinate Access reader conformance `3275354d` ++ + ### Production readiness and consumer adoption + Progress: 3/6 done | workplan_id: `4fa25ad5-f5d0-5592-aa59-085f8ee3edaf` + +@@ -20,7 +26,7 @@ Progress: 3/6 done | workplan_id: `4fa25ad5-f5d0-5592-aa59-085f8ee3edaf` + + ## Inbox Hygiene + +-**Stale unread:** 10 message(s) older than 3 day(s) — triage at session start. ++**Stale unread:** 11 message(s) older than 3 day(s) — triage at session start. + **Missing thread_id:** 9 unread message(s) lack supersession chains. + - ! informed-decision: INFD-WP-0005: service-requested approvals from informed-decision intake (two small asks) `59c5ee82` + - ! flex-auth: FLEX-WP-0020 handoff request (consumer) `2523510b` +diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md +index 2110857..11d89d3 100644 +--- a/WORK-RECORDS.md ++++ b/WORK-RECORDS.md +@@ -10,12 +10,16 @@ + | --- | --- | --- | --- | --- | + | workplan | APPROVAL-WP-ADHOC-2026-10-10 | finished | — | workplans/ADHOC-2026-10-10.md | + | workplan | APPROVAL-WP-ADHOC-2026-10-11 | finished | — | workplans/ADHOC-2026-10-11.md | ++| workplan | APPROVAL-WP-ADHOC-2026-10-16 | finished | — | workplans/ADHOC-2026-10-16.md | + | workplan | APPROVAL-WP-0001 | finished | — | workplans/APPROVAL-WP-0001-v07-alignment-and-engine-spine.md | + | workplan | APPROVAL-WP-0002 | blocked | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | workplan | APPROVAL-WP-0003 | finished | — | workplans/APPROVAL-WP-0003-reissue-expired-approvals.md | ++| workplan | APPROVAL-WP-0004 | finished | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | ++| workplan | APPROVAL-WP-0005 | active | — | workplans/APPROVAL-WP-0005-access-claim-fact-contract.md | + | task | APPROVAL-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | + | task | APPROVAL-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | + | task | APPROVAL-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | ++| task | APPROVAL-WP-ADHOC-2026-10-16-T01 | done | — | workplans/ADHOC-2026-10-16.md | + | task | APPROVAL-WP-0001-T01 | done | — | workplans/APPROVAL-WP-0001-v07-alignment-and-engine-spine.md | + | task | APPROVAL-WP-0001-T02 | done | — | workplans/APPROVAL-WP-0001-v07-alignment-and-engine-spine.md | + | task | APPROVAL-WP-0001-T03 | done | — | workplans/APPROVAL-WP-0001-v07-alignment-and-engine-spine.md | +@@ -32,6 +36,11 @@ + | task | APPROVAL-WP-0002-T05 | wait | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | task | APPROVAL-WP-0002-T06 | done | — | workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md | + | task | APPROVAL-WP-0003-T01 | done | — | workplans/APPROVAL-WP-0003-reissue-expired-approvals.md | ++| task | APPROVAL-WP-0004-T01 | done | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | ++| task | APPROVAL-WP-0004-T02 | done | — | workplans/APPROVAL-WP-0004-exact-consume-source-provenance.md | ++| task | APPROVAL-WP-0005-T01 | done | — | workplans/APPROVAL-WP-0005-access-claim-fact-contract.md | ++| task | APPROVAL-WP-0005-T02 | todo | — | workplans/APPROVAL-WP-0005-access-claim-fact-contract.md | ++| task | APPROVAL-WP-0005-T03 | done | — | workplans/APPROVAL-WP-0005-access-claim-fact-contract.md | + | intake | APPROVAL-IN-0001 | open | blue | intakes/intakes.md | + | intake | APPROVAL-IN-0002 | closed | blue | intakes/intakes.md | + | intake | APPROVAL-IN-0003 | closed | — | intakes/intakes.md | +diff --git a/workplans/ADHOC-2026-10-16.md b/workplans/ADHOC-2026-10-16.md +index cd19170..8cda495 100644 +--- a/workplans/ADHOC-2026-10-16.md ++++ b/workplans/ADHOC-2026-10-16.md +@@ -10,6 +10,7 @@ owner: codex + topic_slug: netkingdom + created: "2026-10-16" + updated: "2026-10-16" ++state_hub_workstream_id: "51585baa-f3c3-5203-9f32-3f0a3c94f889" + --- + + ## Refuse Python-equal JSON-distinct consumed act bindings +@@ -18,6 +19,7 @@ updated: "2026-10-16" + id: APPROVAL-WP-ADHOC-2026-10-16-T01 + status: done + priority: high ++state_hub_task_id: "ff45b583-a021-5a06-bb8d-3ba09674d1f8" + ``` + + Bounded source repair demanded by the CUST-WP-0080 strict execution-seam proof. +diff --git a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +index abdb945..ba0b9af 100644 +--- a/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md ++++ b/workplans/APPROVAL-WP-0005-access-claim-fact-contract.md +@@ -11,6 +11,7 @@ topic_slug: netkingdom + created: "2026-10-16" + updated: "2026-10-16" + depends_on: [] ++state_hub_workstream_id: "777b17de-2c1e-5ce5-aa82-c99e5137e956" + --- + + CUST-WP-0080 explicitly selects the necessary Approval-WP-0002-T01/T05 source +@@ -26,6 +27,7 @@ policy eligibility or live action is included. + id: APPROVAL-WP-0005-T01 + status: done + priority: high ++state_hub_task_id: "24bf384a-fdd0-566d-a1d3-5e89023a7521" + ``` + + INTENT basis: factual exact-bound approval claims supplied to Access, never a +@@ -41,6 +43,7 @@ request-refresh design requires change; fresh facts are verified out-of-band. + id: APPROVAL-WP-0005-T02 + status: todo + priority: high ++state_hub_task_id: "3275354d-d781-53cf-aec6-975c0d4a601a" + ``` + + Coordinator reviews/integrates these source records. Access owns the thin Go +@@ -56,6 +59,7 @@ is created, promoted or marked complete by these source-only fixtures. + id: APPROVAL-WP-0005-T03 + status: done + priority: high ++state_hub_task_id: "362bfe75-1cd7-5081-971b-b55ce483c802" + ``` + + Explicit bounded implementation demand after Access confirmed the exact module