docs: preserve reviewed NetKingdom session artifacts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Reject generated-only commits / generated-commit-guard (push) Successful in 8s

This commit is contained in:
codex 2026-11-01 23:00:33 +01:00
parent 0863ba073a
commit 121097524c
46 changed files with 8039 additions and 0 deletions

View file

@ -0,0 +1,21 @@
{
"scope": "disposable synthetic only",
"actual_source": "Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine",
"actual_access": "production serve binary from frozen B2/B3 temporal R2",
"allow": true,
"original_context_unchanged": true,
"published_digest_match": true,
"returned_source_record_match": true,
"actual_source_consumed_refused": true,
"actual_strict_prepare_and_consume": true,
"no_record_after_refusal": true,
"native_admission": false,
"files_sha256": {
"negative-cases.json": "sha256:1bef572773c34f87a3c6587dbc60ebeb2835c01125f690e8d2f5b0d70a6abadd",
"source-record.json": "sha256:e7e9f7a0ecfc86c7cd449de7443f615e9ba19cecab1cf85f67ec03267e949e64",
"handoff.json": "sha256:ef9739fce5d6be84f0a9e5a593499d4289fcc8bda79573c7d21316a45310234a",
"response.json": "sha256:e7e9f7a0ecfc86c7cd449de7443f615e9ba19cecab1cf85f67ec03267e949e64",
"original-approval-claim.json": "sha256:9e62510e1599d9bc61b15ebb24099e48a6992b808451af4fb3ce26a4b3338f1e",
"request.json": "sha256:d65fb473885c9d6fbb089f5b7dcdcecb75cf31cf20e60a9158e562cb0eb3af4d"
}
}

View file

@ -0,0 +1 @@
5a2099f400fb0c97fed2d991832c0e31513bbdfb

View file

@ -0,0 +1,4 @@
........................................................................ [ 33%]
........................................................................ [ 67%]
..................................................................... [100%]
213 passed in 17.26s

View file

@ -0,0 +1,4 @@
........................................................................ [ 36%]
........................................................................ [ 73%]
.................................................... [100%]
196 passed in 24.01s

View file

@ -0,0 +1,4 @@
........................................................................ [ 34%]
........................................................................ [ 69%]
................................................................ [100%]
208 passed in 19.25s

View file

@ -0,0 +1,4 @@
........................................................................ [ 37%]
........................................................................ [ 75%]
................................................ [100%]
192 passed in 25.13s

View file

@ -0,0 +1,24 @@
{
"prior_handoff_failure_file": "docs/changes/netkingdom-target-2026-10-16/decision-handoff/secrets-task-handoff-full.log",
"actual_current_result": "784 passed,16 failed,7 skipped",
"exact_failure_list_matches_prior": true,
"failures": [
"FAILED tests/test_exec_owner.py::test_actual_bound_child_gets_only_selected_secret_and_fixed_environment",
"FAILED tests/test_exec_owner.py::test_config_changed_during_fetch_does_not_launch",
"FAILED tests/test_exec_owner.py::test_binding_changed_after_approval_refuses_before_fetch",
"FAILED tests/test_exec_owner_companions.py::test_companion_value_reaches_bound_child_and_is_redacted",
"FAILED tests/test_exec_owner_companions.py::test_companion_fetch_failure_starts_no_child",
"FAILED tests/test_exec_owner_companions.py::test_each_lane_is_gated_separately_before_backend",
"FAILED tests/test_exec_owner_companions.py::test_unresolvable_companion_refuses_before_any_gate",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[missing]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[extra]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[env]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[field]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[lane]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[consent]",
"FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[stage]",
"FAILED tests/test_human_control.py::test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child",
"FAILED tests/test_integration_companions.py::test_two_lanes_reach_one_bound_owner"
],
"new_failures": []
}

View file

@ -0,0 +1,562 @@
........................................................................ [ 8%]
........................................................................ [ 17%]
............s........................................................... [ 26%]
.................................................................F...... [ 35%]
..FF..................FF....................FF..FFFFFFF................. [ 44%]
........................................................................ [ 53%]
..........................................F............F................ [ 62%]
........................................................................ [ 71%]
............................................ssssss...................... [ 80%]
........................................................................ [ 89%]
........................................................................ [ 98%]
............... [100%]
=================================== FAILURES ===================================
___ test_actual_bound_child_gets_only_selected_secret_and_fixed_environment ____
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...nd_child_gets_o0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_actual_bound_child_gets_o0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756740d72750>
capsys = <_pytest.capture.CaptureFixture object at 0x756740d70da0>
def test_actual_bound_child_gets_only_selected_secret_and_fixed_environment(bound, monkeypatch, capsys):
data, private, _ = bound
entry = validate_entry(data)
for name in ["PARENT_CREDENTIAL", "BAO_TOKEN", "SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE", "PYTHONPATH"]:
monkeypatch.setenv(name, "parent-sensitive-fixture")
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: "synthetic-owner-value")
> assert exec_delivery.exec_with_secret(object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"], mode="exec-env", expected_owner_digest=owner_digest(entry)) == 0
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_exec_owner.py:55:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/secrets_engine/exec_delivery.py:199: in exec_with_secret
binding_digest = validate_delivery_target(entry, field, command, mode)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/secrets_engine/exec_owner.py:171: in validate_delivery_target
_check_path(Path(binding["cwd"]), directory=True, private=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_actual_bound_child_gets_o0/owner')
directory = True, private = True
def _check_path(path: Path, *, directory: bool = False, private: bool = False) -> None:
"""No symlink or untrusted writable ancestor; root-owned sticky /tmp is safe."""
for candidate in [*reversed(path.parents), path]:
info = candidate.lstat()
leaf = candidate == path
is_dir = not leaf or directory
if not (stat.S_ISDIR(info.st_mode) if is_dir else stat.S_ISREG(info.st_mode)):
raise DeliveryError("exec owner path is not a regular file/directory")
if info.st_uid not in {0, os.getuid()}:
> raise DeliveryError("exec owner path has an untrusted owner")
E secrets_engine.errors.DeliveryError: exec owner path has an untrusted owner
src/secrets_engine/exec_owner.py:146: DeliveryError
_______________ test_config_changed_during_fetch_does_not_launch _______________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...nged_during_fet0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_config_changed_during_fet0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756740da8500>
def test_config_changed_during_fetch_does_not_launch(bound, monkeypatch):
data, _, script = bound
entry = validate_entry(data)
def fetch(*a, **k):
script.write_text("changed during fetch")
return "synthetic-owner-value"
monkeypatch.setattr(exec_delivery, "_fetch_value", fetch)
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("must not launch"))
> with pytest.raises(DeliveryError, match="digest"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'digest'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner.py:91: AssertionError
___________ test_binding_changed_after_approval_refuses_before_fetch ___________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...anged_after_app0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_binding_changed_after_app0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756740d5cbf0>
def test_binding_changed_after_approval_refuses_before_fetch(bound, monkeypatch):
data, _, _ = bound
entry = validate_entry(data)
expected = owner_digest(entry)
entry.delivery_config["exec_owner"]["environment"]["LANG"] = "C"
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("must not read"))
> with pytest.raises(DeliveryError, match="after action admission"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'after action admission'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner.py:101: AssertionError
___________ test_companion_value_reaches_bound_child_and_is_redacted ___________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...value_reaches_b0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_companion_value_reaches_b0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741ef8b00>
capsys = <_pytest.capture.CaptureFixture object at 0x756741efa2a0>
def test_companion_value_reaches_bound_child_and_is_redacted(bound, monkeypatch, capsys):
data, _, script = bound
script.write_text('''test "$API_TOKEN" = synthetic-owner-value || exit 10
test "$WORKER_TOKEN" = synthetic-worker-value || exit 11
read ignored && exit 15
printf '%s %s\\n' "$API_TOKEN" "$WORKER_TOKEN"
printf 'companion-child-ok\\n'
''')
import hashlib
data["delivery_config"]["exec_owner"]["files"][str(script)]["sha256"] = hashlib.sha256(script.read_bytes()).hexdigest()
entry = validate_entry(_with_companion(data))
lane = validate_entry(_companion())
companions = resolve_companions(entry, _lookup(lane))
values = {"test-lane": "synthetic-owner-value", "test-worker": "synthetic-worker-value"}
seen = []
def fetch(client, e, field, **kwargs):
seen.append((e.id, field))
return values[e.id]
monkeypatch.setattr(exec_delivery, "_fetch_value", fetch)
sessions = {}
> rc = exec_delivery.exec_with_secret(
object(), entry, "api_token", data["delivery_config"]["exec_owner"]["command"],
mode="exec-env", expected_owner_digest=owner_digest(entry),
companions=companions, companion_sessions=sessions,
)
tests/test_exec_owner_companions.py:55:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/secrets_engine/exec_delivery.py:199: in exec_with_secret
binding_digest = validate_delivery_target(entry, field, command, mode)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/secrets_engine/exec_owner.py:171: in validate_delivery_target
_check_path(Path(binding["cwd"]), directory=True, private=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_companion_value_reaches_b0/owner')
directory = True, private = True
def _check_path(path: Path, *, directory: bool = False, private: bool = False) -> None:
"""No symlink or untrusted writable ancestor; root-owned sticky /tmp is safe."""
for candidate in [*reversed(path.parents), path]:
info = candidate.lstat()
leaf = candidate == path
is_dir = not leaf or directory
if not (stat.S_ISDIR(info.st_mode) if is_dir else stat.S_ISREG(info.st_mode)):
raise DeliveryError("exec owner path is not a regular file/directory")
if info.st_uid not in {0, os.getuid()}:
> raise DeliveryError("exec owner path has an untrusted owner")
E secrets_engine.errors.DeliveryError: exec owner path has an untrusted owner
src/secrets_engine/exec_owner.py:146: DeliveryError
_________________ test_companion_fetch_failure_starts_no_child _________________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...fetch_failure_s0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_companion_fetch_failure_s0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741f37ec0>
def test_companion_fetch_failure_starts_no_child(bound, monkeypatch):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane = validate_entry(_companion())
def fetch(client, e, field, **kwargs):
if e.id == "test-worker":
raise DeliveryError("scoped read failed for lane 'test-worker' (denied or absent)")
return "synthetic-owner-value"
monkeypatch.setattr(exec_delivery, "_fetch_value", fetch)
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("must not launch"))
> with pytest.raises(DeliveryError, match="test-worker"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'test-worker'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:77: AssertionError
______________ test_each_lane_is_gated_separately_before_backend _______________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...is_gated_separa0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_each_lane_is_gated_separa0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741f36990>
tmp_path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_each_lane_is_gated_separa0')
def test_each_lane_is_gated_separately_before_backend(bound, monkeypatch, tmp_path):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane = validate_entry(_companion())
table = {entry.id: entry, lane.id: lane}
monkeypatch.setattr(cli, "get_entry", lambda _dir, cid: table[cid])
gated = []
def gate(cfg, e, action, evidence, *, fields=(), auth_args=None):
assert auth_args is args
gated.append((e.id, action, fields))
if e.id == "test-worker":
raise DeliveryError("companion lane denied")
return SimpleNamespace(status="approved")
monkeypatch.setattr(cli, "_require_lane_approval", gate)
monkeypatch.setattr(cli, "require_delivery_state", lambda *a: None)
monkeypatch.setattr(cli, "_open_backend", lambda *a, **k: pytest.fail("no backend after a lane refusal"))
command = data["delivery_config"]["exec_owner"]["command"]
args = SimpleNamespace(field=None, catalog=entry.id, command=command, mode="exec-env")
> with pytest.raises(DeliveryError, match="companion lane denied"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion lane denied'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:166: AssertionError
_____________ test_unresolvable_companion_refuses_before_any_gate ______________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...le_companion_re0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_unresolvable_companion_re0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741f37bf0>
tmp_path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_unresolvable_companion_re0')
def test_unresolvable_companion_refuses_before_any_gate(bound, monkeypatch, tmp_path):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
def get(_dir, cid):
if cid == entry.id:
return entry
raise KeyError(cid)
monkeypatch.setattr(cli, "get_entry", get)
monkeypatch.setattr(cli, "_require_lane_approval", lambda *a, **k: pytest.fail("no gate"))
monkeypatch.setattr(cli, "_open_backend", lambda *a, **k: pytest.fail("no backend"))
command = data["delivery_config"]["exec_owner"]["command"]
> with pytest.raises(DeliveryError, match="unavailable"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'unavailable'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:182: AssertionError
______ test_delivery_rechecks_companion_contract_before_any_read[missing] ______
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani0/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756740d5c860>
change = 'missing'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
_______ test_delivery_rechecks_companion_contract_before_any_read[extra] _______
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani1/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani1/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756740d5d9a0>
change = 'extra'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
________ test_delivery_rechecks_companion_contract_before_any_read[env] ________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani2/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani2/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741ec4800>
change = 'env'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
_______ test_delivery_rechecks_companion_contract_before_any_read[field] _______
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani3/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani3/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741ec4260>
change = 'field'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
_______ test_delivery_rechecks_companion_contract_before_any_read[lane] ________
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani4/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani4/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741ec7b60>
change = 'lane'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
______ test_delivery_rechecks_companion_contract_before_any_read[consent] ______
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani5/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani5/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741f37b60>
change = 'consent'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
_______ test_delivery_rechecks_companion_contract_before_any_read[stage] _______
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...echecks_compani6/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_delivery_rechecks_compani6/owner/owner.sh'))
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741f34200>
change = 'stage'
@pytest.mark.parametrize("change", ["missing", "extra", "env", "field", "lane", "consent", "stage"])
def test_delivery_rechecks_companion_contract_before_any_read(bound, monkeypatch, change):
data, _, _ = bound
entry = validate_entry(_with_companion(data))
lane_data = _companion()
if change == "consent":
lane_data["delivery_config"] = {}
elif change == "stage":
lane_data.update(stage="build", path="build/team/worker")
elif change == "lane":
lane_data["id"] = "another-worker"
lane = validate_entry(lane_data)
companions = [(lane, "worker_token", "WORKER_TOKEN")]
if change == "missing": companions = []
elif change == "extra": companions *= 2
elif change == "env": companions = [(lane, "worker_token", "API_TOKEN")]
elif change == "field": companions = [(lane, "other_field", "WORKER_TOKEN")]
monkeypatch.setattr(exec_delivery, "_fetch_value", lambda *a, **k: pytest.fail("no read"))
monkeypatch.setattr(exec_delivery, "_spawn", lambda *a, **k: pytest.fail("no child"))
> with pytest.raises(DeliveryError, match="companion"):
E AssertionError: Regex pattern did not match.
E Expected regex: 'companion'
E Actual message: 'exec owner path has an untrusted owner'
tests/test_exec_owner_companions.py:223: AssertionError
_ test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child _
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'prod', ...}, PosixPath('/tmp/pytest-of-worsch/py...handler_rejects0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_real_exec_handler_rejects0/owner/owner.sh'))
tmp_path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_real_exec_handler_rejects0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x756741eef5f0>
def test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child(bound, tmp_path, monkeypatch):
raw, _, _ = bound
raw["stage"] = "prod"
raw["approval"].update(model="decision", authorization_id=APPROVAL_ID,
purpose="contract-test", human_control=True)
entry = validate_entry(raw)
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path), authorization_subject_id="user:alice",
authorization_subject_type="Human")
claim = _served(entry, action="exec")
monkeypatch.setattr(cli, "get_entry", lambda *a: entry)
monkeypatch.setattr(cli, "authorize_action", lambda *a, **k: authorize_action(*a, **k,
opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not check")))
for name in ("require_production_consume", "_open_backend"):
monkeypatch.setattr(cli, name, lambda *a, **k: pytest.fail("must not consume or open backend"))
with pytest.raises(DecisionError, match="human_control"):
> cli.cmd_exec(cfg, SimpleNamespace(catalog=entry.id, field="api_token", mode="exec-env",
command=raw["delivery_config"]["exec_owner"]["command"]))
tests/test_human_control.py:161:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/secrets_engine/cli.py:569: in cmd_exec
owner_digest = validate_delivery_target(entry, field, args.command, args.mode)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/secrets_engine/exec_owner.py:171: in validate_delivery_target
_check_path(Path(binding["cwd"]), directory=True, private=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_real_exec_handler_rejects0/owner')
directory = True, private = True
def _check_path(path: Path, *, directory: bool = False, private: bool = False) -> None:
"""No symlink or untrusted writable ancestor; root-owned sticky /tmp is safe."""
for candidate in [*reversed(path.parents), path]:
info = candidate.lstat()
leaf = candidate == path
is_dir = not leaf or directory
if not (stat.S_ISDIR(info.st_mode) if is_dir else stat.S_ISREG(info.st_mode)):
raise DeliveryError("exec owner path is not a regular file/directory")
if info.st_uid not in {0, os.getuid()}:
> raise DeliveryError("exec owner path has an untrusted owner")
E secrets_engine.errors.DeliveryError: exec owner path has an untrusted owner
src/secrets_engine/exec_owner.py:146: DeliveryError
_____________________ test_two_lanes_reach_one_bound_owner _____________________
bao_dev = OpenBaoClient(addr='http://127.0.0.1:55527', token='se-test-root', bao_bin='/home/worsch/.local/bin/bao')
bound = ({'id': 'test-lane', 'org': 'coulomb', 'repo': 'team-repo', 'stage': 'test', ...}, PosixPath('/tmp/pytest-of-worsch/py...reach_one_bound0/owner'), PosixPath('/tmp/pytest-of-worsch/pytest-207/test_two_lanes_reach_one_bound0/owner/owner.sh'))
tmp_path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_two_lanes_reach_one_bound0')
capsys = <_pytest.capture.CaptureFixture object at 0x756740d5fd70>
def test_two_lanes_reach_one_bound_owner(bao_dev, bound, tmp_path, capsys):
data, _, script = bound
script.write_text('''test "$API_TOKEN" = primary-integration-value || exit 10
test "$WORKER_TOKEN" = worker-integration-value || exit 11
test -z "$BAO_TOKEN" || exit 12
read ignored && exit 15
printf '%s %s\\n' "$API_TOKEN" "$WORKER_TOKEN"
printf 'two-lane-child-ok\\n'
''')
data["delivery_config"]["exec_owner"]["files"][str(script)]["sha256"] = hashlib.sha256(
script.read_bytes()
).hexdigest()
entry = validate_entry(_with_companion(copy.deepcopy(data)))
lane = validate_entry(_companion())
_provision(bao_dev, entry, "api_token", "primary-integration-value", tmp_path)
_provision(bao_dev, lane, "worker_token", "worker-integration-value", tmp_path)
sessions, primary_session = {}, {}
> rc = exec_with_secret(
bao_dev, entry, "api_token", data["delivery_config"]["exec_owner"]["command"],
mode="exec-env", session_evidence=primary_session,
expected_owner_digest=owner_digest(entry),
companions=resolve_companions(entry, _lookup(lane)), companion_sessions=sessions,
)
tests/test_integration_companions.py:47:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
src/secrets_engine/exec_delivery.py:199: in exec_with_secret
binding_digest = validate_delivery_target(entry, field, command, mode)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/secrets_engine/exec_owner.py:171: in validate_delivery_target
_check_path(Path(binding["cwd"]), directory=True, private=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
path = PosixPath('/tmp/pytest-of-worsch/pytest-207/test_two_lanes_reach_one_bound0/owner')
directory = True, private = True
def _check_path(path: Path, *, directory: bool = False, private: bool = False) -> None:
"""No symlink or untrusted writable ancestor; root-owned sticky /tmp is safe."""
for candidate in [*reversed(path.parents), path]:
info = candidate.lstat()
leaf = candidate == path
is_dir = not leaf or directory
if not (stat.S_ISDIR(info.st_mode) if is_dir else stat.S_ISREG(info.st_mode)):
raise DeliveryError("exec owner path is not a regular file/directory")
if info.st_uid not in {0, os.getuid()}:
> raise DeliveryError("exec owner path has an untrusted owner")
E secrets_engine.errors.DeliveryError: exec owner path has an untrusted owner
src/secrets_engine/exec_owner.py:146: DeliveryError
=========================== short test summary info ============================
FAILED tests/test_exec_owner.py::test_actual_bound_child_gets_only_selected_secret_and_fixed_environment
FAILED tests/test_exec_owner.py::test_config_changed_during_fetch_does_not_launch
FAILED tests/test_exec_owner.py::test_binding_changed_after_approval_refuses_before_fetch
FAILED tests/test_exec_owner_companions.py::test_companion_value_reaches_bound_child_and_is_redacted
FAILED tests/test_exec_owner_companions.py::test_companion_fetch_failure_starts_no_child
FAILED tests/test_exec_owner_companions.py::test_each_lane_is_gated_separately_before_backend
FAILED tests/test_exec_owner_companions.py::test_unresolvable_companion_refuses_before_any_gate
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[missing]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[extra]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[env]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[field]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[lane]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[consent]
FAILED tests/test_exec_owner_companions.py::test_delivery_rechecks_companion_contract_before_any_read[stage]
FAILED tests/test_human_control.py::test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child
FAILED tests/test_integration_companions.py::test_two_lanes_reach_one_bound_owner
16 failed, 784 passed, 7 skipped in 55.47s

View file

@ -0,0 +1,96 @@
diff --git a/.custodian-brief.md b/.custodian-brief.md
index 0cb912a..ba794c8 100644
--- a/.custodian-brief.md
+++ b/.custodian-brief.md
@@ -2,7 +2,7 @@
# Custodian Brief — secrets-engine
**Domain:** infotech
-**Last synced:** 2026-10-11 17:52 UTC
+**Last synced:** 2026-10-16 05:55 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
@@ -11,8 +11,24 @@
Progress: 1/3 done | workplan_id: `5e38aecc-85ca-5f55-94c6-9fbb08604819`
**Open tasks:**
+- ! Define the exact mediated execution contract `b2f0d462`
+ *(wait: Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred.)*
- ! Implement and prove one native metadata action `5f8f7c41`
-- ► Define the exact mediated execution contract `b2f0d462`
+
+### Constrain PDP transport and authenticate decision responses
+Progress: 1/3 done | workplan_id: `7252469d-d7a5-5c18-8911-b511b8c6a736`
+
+**Open tasks:**
+- ! Verify the admitted native consumer and rotation path `85aa4c78`
+ *(wait: Requires reviewed verification-key admission, source enforcement and exact native rollout receipt; no production trust change is proposed yet.)*
+- ► Implement independently anchored responder authentication `877782e5`
+
+### Production-safe provisioning, authorization, and lifecycle hardening
+Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
+
+**Open tasks:**
+- ! Resume native production lane adoption `a0a1dd92`
+ *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
### Activate native Claude credential delivery for Glas
Progress: 2/3 done | workplan_id: `40ccc3b4-d046-5a58-8649-e7935f45c974`
@@ -26,27 +42,20 @@ Progress: 4/6 done | workplan_id: `31f7f8ea-7f73-516c-8877-f03a13f1db82`
**Open tasks:**
- ! Stage live apply and verification `fb103f1e`
- *(wait: OpenRouter key-check native acceptance is recorded in SECRETS-WP-0010-T03. Other lanes still need exact approvals, scoped attended apply and per-lane positive/negative/health/revocation evidence.)*
+ *(wait: Owner work first: publish a reviewed scoped-apply command per lane. Per-lane approvals are no longer needed (D1 standing rule); each lane with a command enters the attended window.)*
- ! Reconcile routing ownership and retire interim proxies `1431edae`
*(wait: Needs owner-agreed routing/proxy retirement for each verified lane and custody disposition of the legacy npm pointer; a single approved OpenRouter key-check does not authorize broader routing cutover.)*
-### Production-safe provisioning, authorization, and lifecycle hardening
-Progress: 6/7 done | workplan_id: `68a39be1-bd9c-5133-ad64-e7bca892aaf3`
-
-**Open tasks:**
-- ! Resume native production lane adoption `a0a1dd92`
- *(wait: Engine approval hardening is complete. Remaining per-lane cutover includes native routing/proxy retirement with ops-warden under SECRETS-WP-0006-T05/T06; the exact OpenRouter key-check receipt does not establish broader recipient readiness.)*
-
### Evolve the Lifecycle engine to the accepted security layer model
Progress: 5/6 done | workplan_id: `9c9e5164-b2f5-5ea2-a557-5368d65e9fe0`
**Open tasks:**
- ! No standing engine credential `d7bc8bdc`
- *(wait: Positive service-JWT login proof passed 2026-10-10 (SECRETS-WP-0012-T03, docs/evidence/2026-10-10-keycape-openbao-probe.json). Waits on railiance-platform closing RPF-WP-0035-T02 with the CCR-2026-0038 negative checks, then on routing live engine actions through service-jwt instead of bootstrap/env.)*
+ *(wait: Published JWT contract and positive login/revocation are delivered. Native KEY-WP-0039-T02 claim negatives, CCR-2026-0038 effective-policy/capability/expiry/use checks and MASON-WP-0009-T05 remain. Login-self-only grants no protected-operation authority: agree and implement separately reviewed task-scoped backend execution entitlement, then prove functional steady-state service-identity adoption without bootstrap/env fallback.)*
## Inbox Hygiene
-**Stale unread:** 2 message(s) older than 3 day(s) — triage at session start.
+**Stale unread:** 3 message(s) older than 3 day(s) — triage at session start.
**Missing thread_id:** 2 unread message(s) lack supersession chains.
---
diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 70e5831..4d1cfb6 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -22,6 +22,7 @@
| workplan | SECRETS-WP-0011 | finished | — | workplans/SECRETS-WP-0011-multi-lane-exec-owner-delivery.md |
| workplan | SECRETS-WP-0012 | finished | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| workplan | SECRETS-WP-0013 | proposed | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| workplan | SECRETS-WP-0014 | active | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| task | SECRETS-WP-ADHOC-2026-08-21-T01 | done | — | workplans/ADHOC-2026-08-21.md |
| task | SECRETS-WP-ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md |
| task | SECRETS-WP-0001-T01 | done | — | workplans/SECRETS-WP-0001-statehub-bootstrap.md |
@@ -83,9 +84,12 @@
| task | SECRETS-WP-0012-T02 | cancel | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T03 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
| task | SECRETS-WP-0012-T04 | done | — | workplans/SECRETS-WP-0012-keycape-openbao-consumer-custody.md |
-| task | SECRETS-WP-0013-T01 | progress | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0013-T01 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T02 | done | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
| task | SECRETS-WP-0013-T03 | wait | — | workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md |
+| task | SECRETS-WP-0014-T01 | done | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T02 | progress | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
+| task | SECRETS-WP-0014-T03 | wait | — | workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md |
| intake | SECRETS-IN-0001 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0002 | closed | — | intakes/intakes.md |
| intake | SECRETS-IN-0003 | closed | — | intakes/intakes.md |

View file

@ -0,0 +1,49 @@
# Secrets trusted-successor source review R1
Base: canonical Secrets `5a2099f400fb0c97fed2d991832c0e31513bbdfb`.
Source proposal only. Canonical owner checkout and production installed tools,
credentials, configuration, keys, policy, grants and native state are unchanged.
Preexisting owner metadata diff is retained separately and excluded from patch.
The exact producer API/sign files match frozen Access B2/B3 R1 SHA
`6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1`.
No signature or canonicalization implementation was forked. Wrapper calls
producer DecodeEnvelope. Exact authenticated typed JSON is retained through
strict CAS, rather than reconstructing signed bytes from Python numbers.
Snapshots additionally compare parsed types via JSON serialization (which is
never signing input), refusing boolean/integer/float substitution. The helper
runs with empty environment. Direct authentication now also rejects malformed,
duplicate, nonfinite and invalid UTF-8 inputs before the producer process.
Signed optional source observation is independently signature/policy-pin bound.
Strict handoff checks nonempty source/revision, digest syntax, exact native act
digest, observation <= validation <= current upper bound, current freshness,
and decision expiry <= source deadline. It supplements original carried claim;
original request/context/freshness, exact descriptor and caller, native and full
PDP joins, deadlines and once-only CAS remain required. Source identifiers are
not independently admitted by a fixture, and raw claim-byte digest is not
recomputed from a parsed claim with different formatting.
103 focused signature/strict tests pass on final runtime source; 192 combined
focused tests passed before the final direct-parser strengthening (unchanged
strict signature suite rerun on final source). Seven actual offline bundle tests
pass, including deterministic builds/fresh installation/signed consumer and
missing or tampered helper refusals. No test signer is installed. Go packages
build/test and exact producer source checker pass. Existing legacy producer
signed rotate fixture remains accepted; fourteen actual Approval owner native
vectors verify and each tampered envelope refuses. These signature vectors do
not broaden request numeric admission.
Actual regenerated Approval App HTTPS -> frozen Access production serve v2
ALLOW -> Secrets signature boundary -> exact-byte reauthentication succeeds;
actual provider revocation then refuses, without a new durable record.
`actual-source-signature/receipt.json` retains artifacts/hashes. That separate
synthetic request is not accepted as a strict prepared task or estate admission.
REMAINING POSITIVE STRICT BLOCKER: actual Secrets builder emits no
`service_actor_binding`, whereas v2 service input and policy require that exact
five-field public declaration before approval issuance. Current builder's actual
request therefore refuses. No injected context, callback, or equality relaxation
hides this mismatch. Parent has been asked to scope the narrow owner mapping
addition. This R1 package is ready for verifier-source review, not final v2 strict
positive handoff acceptance. Native trust/custody/runtime blockers remain open.

View file

@ -0,0 +1,40 @@
# Secrets successor R2 source disposition
R1 artifacts remain immutable. The full patch applies to canonical Secrets
5a2099f400fb0c97fed2d991832c0e31513bbdfb; the delta applies after frozen R1.
Exact hashes and source preimages are retained in manifest-r2.json/preimages.
The execution builder adds only the fixed Access DeclaredServiceBinding public
mapping after validating an actual ExecutionContract, prod/platform lane and exact
secrets-engine service subject. It sets subject/resource platform tenants before
approval issue, matching existing Access normalization. The strict exact subject
check accepts only that complete fixed subject. Other subjects refuse; lifecycle
requests gain no declaration or tenant change. This declaration is caller-supplied
public input, never authentication evidence; the independent signed enforced
transport caller remains mandatory. No arbitrary enrichment API or schema exists.
196 focused tests and 7 actual offline reproducible build/install tests pass on
final R2 source. Actual source checker matches frozen producer bytes. The real
builder -> actual Approval App/SQLite with separate synthetic human entry ->
Access production serve with actual TokenReview and separately configured source
reader -> strict Secrets authorize -> exact CAS -> independent actual
ExecutionConfirmationClient GET succeeds. Six negative cases refuse: absent
service declaration, changed descriptor, changed native target, expired original
claim despite fresh provider, signature tamper, and prepared replay. After CAS,
Access also refuses the carried claim without writing another durable record.
No monkeypatch, injected approval context or authorization callback substitutes
for production request/claim/signature/handoff paths. Original context is exact.
The proof uses fixed whole-second operation deadline inside v2's15-second allow
TTL, because consumer requires operation deadline<=decision expiry while producer
caps/floors expiry at that deadline. Fractional deadlines or longer operations
continue to refuse; this source change does not waive or extend any authority.
No native metadata task, package install outside disposable prefix, production
trust/key/custody/grant/policy admission or live protected effect occurred.
The first actual proof reached strict CAS then failed a test-only GET assertion
using a nonexistent top-level provider key. Failure is preserved in
actual-strict-attempt1-r2.txt; correction uses actual owner ConfirmationClient.
Final source proof and six negative cases pass in actual-strict-test-r2.txt and
actual-strict-r2/. Prior R1 blocker report remains dated historical evidence;
this R2 resolves only that source mapping gap, not native acceptance.

View file

@ -0,0 +1,35 @@
# Secrets temporal successor R3 review
R1/R2 patches, manifests, tests and proof evidence remain immutable. This delta
changes only contract/workplan/verifier-provenance documentation; all Python and
Go runtime bytes match frozen Secrets R2 exactly (manifest-r3.json records each
runtime hash). Exact vendored producer API/sign source also matches temporal
Access R2; there is no canonicalizer refresh or algorithm change.
The contract now describes the actual fixed backend_budget.ttl_seconds request,
not a dynamic min calculation. Before issuance the native owner requires
remaining>=declaredTTL+5s; requested/explicit maximum TTL equal the immutable
budget and returned TTL must be positive and <=budget. Operation deadline remains
bounded by decision/approval/caller and original/fresh source authority; token
expiry may precede operation deadline, with no retry/remint. A120s budget plus5s
cannot fit the120s hard operation cap and refuses. These are conservative bounds,
not authority extensions. Platform owner coordination was requested.
Actual ApprovalApp -> Access temporalR2 production serve -> strict Secrets prepare
and CAS -> independent actual Approval ExecutionConfirmationClient GET passes
using explicit fixture freshness120/backendTTL30/operationdeadline60. Returned
signed expiry equals immutable operation deadline as an aware instant (Z and
+00:00 presentation differ). The original subsecond-context, full/native/approval
and submitted digest checks remain unchanged. Six negatives plus Access refusal
after consume pass. Seven actual offline bundle/install tests pass.
An unresolved precision seam is retained: Approval consumed_at is whole-second
truncated, whereas Access temporalR2 signs subsecond not_before. Fast real CAS
can fail the strict receipt-before-start guard; actual-strict-fast-cas-refusal-r3.txt
retains this. Positive proof sequences CAS using real elapsed time until the
signed start's next whole second (<1s); it does not inject time or relax checks.
This establishes reachable positive source interoperability, not reliable fast-CAS
admission. Root and Access/Approval owners were notified; runtime is frozen until
they resolve the precision contract. The first R3 proof additionally failed a
test-only lexical Z/+00 comparison; preserved separately and corrected to aware
instant equality. No live credentials/effects/trust/policy/native admission.

View file

@ -0,0 +1,46 @@
# Secrets retained-registry source R4 review
R1/R2/R3 source packages remain immutable. R4 full applies to canonical5a2099f;
over-R3 delta applies only after frozen R3. Manifest records exact patch/source
hashes, producer R3 full46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6,
and unchanged admitted-policy proposal digest de5683e5... (not native admission).
Exact producer API/sign snapshot is unchanged; no cryptography/canonicalizer fork.
prepare_task_approval(cfg,entry,execution_contract=typed_contract) uses actual
existing signed Check with no approval claim. It accepts DENY only, authenticates
independently pinned signer/evaluator/mode/package/version/digest, checks original
request and type-sensitive immutable context/submitted digest, producer evaluated
self-digest, explicit claimfree approval-free digest, task/service/native/caller
correspondence, live original operation/caller bounds, nonfuture decision time,
and signed registry digest syntax. It never consumes or authorizes execution.
Its detached original_request, native_binding, approval_binding_digest,
submitted_request_digest, decision_id, execution_contract and
registry_snapshot_digest are public preapproval metadata. Normal Approval issue
uses normalized native act and evaluated approval-free digest, never a local
registry reconstruction. Final authorization preserves the original proposal and
attaches only the original issued claim; Access renormalizes and source-joins.
No independent normalization TTL is invented: DENY is not authority. An old
normalization cannot replace final signed ALLOW/current source verification.
Original operation/caller deadlines still apply. Unit case demonstrates an old
valid timestamp produces only metadata and cannot consume; actual retained
registry drift source proof refuses final Check with no new ALLOW record/CAS and
unconsumed Approval. Full final proof awaits owner timestamp repair; prototype
wallclock success is not precision acceptance. Registry fileSHA and producer
canonical registry digest are distinct evidence fields, not interchangeable pins.
Shared task identity validation now checks requested resource-attribute subsets
with type-sensitive JSON semantic comparisons, preserving legitimate extra
registry facts while refusing bool/int/float substitutions. Original signed
context/descriptor checks are also type-sensitive. This JSON comparison never
supplies signature bytes; exact producer-authenticated JSON remains reverified.
213 focused checks and7 actual offline bundle tests pass. Full suite784pass,
16fail,7skip; exact16failure list matches retained previous handoff baseline
(ownership guards on sandbox/tmp ancestry), with no new failures. Full logs and
explicit list comparison retained. No guard is relaxed to obtain a green suite.
Canonical checkout, installed tools, production keys/config/grants/policy and
native state are untouched. No actual protected native effect is claimed.
Subsequent final cross-owner proof: frozen Access R3 and Approval precision patch74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd resolve the pending provider dependency. Actual retained-registry signed-DENY preapproval, human approval, registry drift refusal before CAS, restored signed ALLOW, immediate strict CAS and independent GET pass without sleep/backdating/waiver. Precise durable consumption instant 2026-10-16T15:05:44.235319+00:00 is independently confirmed. Original signed DENY/ALLOW records and schema-valid synthetic public trust replay offline. Exact final dependency/proof hashes are in verification-final-r4.json. Earlier pending/fast-CAS observations remain historical evidence; no native runtime/trust/grant/policy adoption is inferred. Source R4 and all prior source patch hashes remain unchanged.

View file

@ -0,0 +1,2 @@
....... [100%]
7 passed in 6.17s

View file

@ -0,0 +1,2 @@
....... [100%]
7 passed in 4.03s

View file

@ -0,0 +1,2 @@
....... [100%]
7 passed in 8.91s

View file

@ -0,0 +1,2 @@
....... [100%]
7 passed in 3.92s

View file

@ -0,0 +1,2 @@
............ [100%]
12 passed in 1.74s

View file

@ -0,0 +1,2 @@
..................................................................... [100%]
69 passed in 8.59s

View file

@ -0,0 +1,78 @@
# Strict Secrets service task-execution handoff
This is a narrow source seam for existing SECRETS-WP-0013, not a native grant or
an execute-ccr CLI. Both Access runtime-deny policy variants remain unchanged.
The proposed service subject/transport mapping and the complete native executor
still need independent owner admission. Positive source fixtures do not admit
that mapping, a policy, a caller, a production key or an OpenBao entitlement.
Platform's trusted owner process calls:
```python
from secrets_engine.task_execution_handoff import (
authorize_task_execution, consume_task_execution,
)
authorization = authorize_task_execution(cfg, entry, execution_contract=contract)
record = authorization.as_owner_handoff()
# Independently bind exact owner Plan/descriptor; reserve durable one-shot journal.
# Persist pending strict-CAS intent before calling:
consumption = consume_task_execution(cfg, authorization)
# Independently GET-confirm exact original consumption with Approval Engine's
# ExecutionConfirmationClient before journaling consume-confirmed/native work.
```
Preparation uses the existing empty fields/policy_targets/auth_targets defaults
and existing build_action_request/claim/Check validators. No caller-selected
transport, authorizer callback, policy result or raw handoff mapping is accepted.
The normal authorizer retains detached serialized request, claim and authenticated
Go-typed envelope snapshots; its existing evidence output does not expose them.
The narrow adapter reuses the validators, rather than evaluating a policy.
The adapter checks the exact descriptor in the signed context, full evaluated and
submitted request digests, approval-free digest, exact five-field native act and
native binding digest, service subject/resource/tenant correspondence and original
decision/approval/deadline bounds. Initiating/represented subject is strictly
`secrets-engine` of type `service`; the execution actor is `service:secrets-engine`.
Signed caller provenance must report `enforce`, the proposed exact Secrets
ServiceAccount principal, `flex-auth` audience and live expiry bounding the
operation deadline. This consumes the signed PDP's caller attestation; it does
not independently perform TokenReview or promote the proposed server mapping.
Human/delegation/projected-identity flags cannot substitute for this service seam.
No service_actor_binding request propagation or source-policy activation is added.
`as_owner_handoff()` returns a detached non-secret mapping with:
- execution_contract
- approval_id, request_digest, decision_id
- native_binding, approval_binding_digest, submitted_request_digest
- decision_not_before, decision_expires_at, approval_expires_at
The exact native binding is derived from the signed evaluated request using the
published Approval five-field mapping, and is compared to the served claim.
Descriptor/CCR/grant/source/catalog/target correspondence is retained end to end;
Platform still independently compiles its owner Plan and grant from owner bytes.
The mapping is evidence for that join, never a transferable bearer authorization.
Consumption accepts only the module-created opaque prepared object in that same
trusted owner process. It fixes source/config coordinates, reauthenticates using
currently admitted responder keys, and rechecks claim/decision/descriptor/caller
freshness before reading the CAS credential. Each object is reserved once before
CAS/revalidation and cannot retry after any failure or unknown outcome. Its
process-local guard complements Platform's durable cross-process journal; it is
not a replacement for that journal or process isolation.
The strict response path uses the existing CAS transport but rejects duplicate,
nonfinite or excessively deep JSON, missing/mismatched approval and decision IDs,
wrong digest/status, missing or nonboolean idempotence, historical idempotent
success, verdict fields and absent/naive/invalid consumption timestamps. The
consumption instant and post-CAS freshness must remain inside the original
bounds. No field is coerced or filled from the submitted binding. A late/uncertain
CAS may have occurred; it never licenses retry or protected native work.
The returned immutable TaskExecutionConsumption contains exact approval_id,
request_digest, decision_id, consumed_at and explicit false idempotent. It proves
only the strict CAS response observed by Secrets. Platform must independently
confirm original durable consumption using Approval's real provider client,
journal it, then recheck its exact Plan, owner sources, grant and deadlines before
any native operation. Neither a dataclass nor a caller-supplied boolean admits
execution. Existing generic CLI CAS behavior is otherwise unchanged.

View file

@ -0,0 +1,334 @@
# Task-scoped protected OpenBao execution — owner design
Design ID: `SECRETS-EXEC-20261011-v1`. Status: proposed, source preparation only.
Owning draft: `SECRETS-WP-0013`; adoption remains `SECRETS-WP-0008-T06`.
No native entitlement, owner assent or founder go is established here.
## The concrete gap and immediate correction
`cli._require_lane_approval` consumes the approved action before `_open_backend`.
`engine_auth.select_engine_auth` selects service-jwt for auto whenever its login
contract is configured. CCR-2026-0038 grants a 300-second, eight-use token with
exactly `secrets-engine-login-self`. It explicitly denies custody data/metadata,
policy/auth writes and child-token creation. Thus a valid approval can be
consumed before an ordinary apply/provision/exec fails for lack of backend power.
The staged correction checks **local execution readiness before claim/PDP/CAS**
for every caller about to access protected OpenBao work. Until a real reviewed
execution backend ships, service-jwt is rejected for those callers. Explicit
bootstrap/env providers retain their existing approval/stance gates; no fallback
or default changes. Service-auth login/self probes remain available. Local-only
compromise/reactivate records retain their existing path. A file/boolean claiming
entitlement cannot make the guard pass. Tests exercise the actual apply handler,
asserting zero claim/PDP/consume/login/apply calls for configured auto and missing
explicit service-jwt entitlement. This correction fixes early consumption; it
does **not** implement task-scoped protected work or complete T06.
## One initial execution surface
Implement `secrets-engine execute-ccr --catalog <id> --stage prod --request <file>`
as an explicit, metadata-only action. It admits one owner-reviewed Platform
`workload-kv-read` CCR with already-existing KV and auth mounts, exact generated
policy and exact Kubernetes or existing netkingdom OIDC role. It does not call
generic `apply_plan`: that function can manage AppRoles/mounts, which differs
from Platform's typed CCR applier. Provision, exec delivery, rotation, KV reads,
SecretIDs, token creation by the caller, mounting and arbitrary CLI commands
remain outside this first entitlement. Their backend obligations require their
own later exact action contracts, not reuse of this metadata allowance.
CCR38 and its role/policy stay byte-for-byte unchanged. The engine identifies
itself through that login-only service session; authentication does not create
authority for the metadata action. A new separately reviewed Platform
`task-execution-grant` CCR admits the founder-owned execution mediator and its
temporary exact two-path task token. Existing applied CCRs, including CCR38,
cannot be replayed to manufacture new native work. A real unexecuted, separately
approved metadata CCR and native success/negative/cleanup receipts are needed
for the first proof; none is selected or approved by this design.
## Exact request binding
Use `authorization.build_action_request(entry, 'execute-ccr', ...)` with the
existing canonical digest rules. `apply` is a distinct legacy action: its v2
policy ignores this metadata contract and must never stand in for execute-ccr.
Add a reviewed `context.execution_contract` object
before observing the claim or calling Check. Its fields are:
| Field | Binding |
| --- | --- |
| contract_version | `SECRETS-EXEC-20261011-v1` |
| operation_id | Owner-issued UUID, part of the request digest |
| operation | `metadata-apply-ccr`, closed enum |
| execution_actor | Verified `service:secrets-engine`, separate from initiating subject |
| ccr_id / ccr_immutable_sha256 | Exact unapplied owner CCR ID and immutable content digest |
| execution_grant_ccr_id / grant_immutable_sha256 | Exact reviewed task-token materialization and cleanup contract |
| catalog_sha256 | Exact non-secret approved catalog target specification |
| source_bundle | Absolute owner-resolved files and immutable SHA256 hashes for engine dispatcher, Platform mediator, typed CCR module and policy/auth source |
| backend_origin / cluster_uid | Exact reviewed estate OpenBao origin and railiance01 identity |
| operations | Ordered exact HTTP method, normalized path and canonical JSON/text payload hash for each read/write/readback; no user supplied body |
| operation_deadline | Upper bound no later than approval, PDP or task-token expiry |
| backend_budget | TTL at most 120s, at most twelve uses, no renew/child/wrap/unlimited uses |
| expected_before / expected_after | Owner-authenticated revision/state digests, never secret contents |
The action subject/type/purpose/tenant are the actual initiating principal's
verified identity and existing catalog purpose, not inferred from engine service
identity or arbitrary environment fields. Founder-attended invocation names that
principal and the representation explicitly. The mediator refuses absent owner
identity correspondence; a caller-authored subject string is not evidence.
The signed/served approval must bind the same approval-free execution context;
the PDP must return the exact canonical request and matching approval digest.
Publish and pin the updated access-engine policy package/version that evaluates
this context; an older package accepting only a catalog ID cannot authorize it.
No package default or implicit semantic acceptance is assumed. Approval identity,
decision ID and canonical request digest travel together in a non-secret request
descriptor. They are provenance until freshly validated and CAS-consumed.
CCR immutable digests exclude only explicitly owner-defined append-only review/
evidence fields; policy bytes, role bytes, typed mutation body, target coordinates,
grant/deadline and source revisions are included. Approval IDs added after
issuance do not change their own binding. The mediator independently reconstructs
all operation bodies from owner sources and compares the resulting digest list.
It accepts no caller ACL template, path override, shell argv or raw JSON body.
## Execution mediation and consume ordering
Use the existing attended Platform front door, invoked by the founder, to run a
new fixed Platform owner mediator. No daemon, broad new service or new bearer
delivery to agents is required. The caller engine prepares a non-secret request
and receives a non-secret receipt. The founder-held admin credential exists only
inside the reviewed Platform child, through the existing attended route. It is
never exported to the caller. Neither local receipt files nor user-authored
`approved` labels authorize the child.
The Platform child runs the reviewed Secrets Engine authorization/consume code
at the last responsible execution boundary; the CLI preparation path does not
consume early and then hand an untrusted receipt to a more privileged process.
The backend mediation contract must preserve engine ownership of lifecycle
validation while Platform owns its OpenBao credential and typed native primitive.
1. Load pinned sources and exact non-secret request; check typed CCR/grant owner
approval, catalog/subject/operation correspondence, source/state expectations,
owner-only execution state directory, supported method/body allowlist and
local clock/deadline. No protected OpenBao call occurs yet.
2. Atomically reserve the operation in a dedicated engine execution ledger,
uniquely keyed by `approval_id + request_digest`; the operation UUID is inside
the digest. Bind the authenticated caller, sources and deadline. This is
execution state, not the audit outbox or a local evidence-presence gate.
Multiworker locking uses SQLite `BEGIN IMMEDIATE` and uniqueness constraints;
only the reservation creator may proceed.
3. Use `authorize_action` / approval-claim validation / `check_decision` / exact
envelope validation, with pinned policy package and trusted application time.
Recheck freshness after network calls. Require served human control for the
production grant; missing claims, unreachable PDP or mismatched binding refuse.
4. Call `require_production_consume` once for that exact binding. Require
confirmed matching consumption. Persist `consume-confirmed` before any
protected OpenBao login, custody read, capability check or mutation. A consume
timeout is `unknown`; never treat it as absence or automatically retry.
A consumed/idempotent response without the same live local admission is not
permission to execute again. Retrying a completed operation returns only its
existing receipt; an incomplete/unknown operation requires owner reconciliation
and a fresh exact approval for any additional backend action.
5. After consume, perform the existing attended in-memory client-secret source
and CCR38 service login without provider fallback. Verify its exact login-self
policy/TTL/uses and service identity; keep this session self-only. Failure
consumes the trial without metadata effects; no implicit re-login or retry.
6. In the Platform child, use the separately authorized founder-held identity
only to materialize the reviewed exact ephemeral task policy/token. OpenBao's
token parent remains platform-owned; the CCR38 token cannot mint it. The grant
binds every policy/token create/revoke/delete step to this operation and its
cleanup. Do not give a standing `sys/policies/acl/*`, `auth/*/role/*` or
`auth/token/create` grant to the engine service.
7. Recheck decision, approval and operation deadlines before **each** protected
request. The task token is nonrenewable, no default/identity policies and
grants only read/create/update on the one approved ACL policy path and the
one approved workload auth-role path, plus revoke-self. No child token, mount
administration, KV data/metadata, SecretID or sibling role is admitted.
OpenBao ACL narrows endpoint access; it cannot restrict the HCL/role payload.
Only the mediator's independently reconstructed body equality enforces that.
8. Use an injected restricted backend adapter for the exact ordered native
calls. Compare current expected state; write only the approved generated body,
read it back, and compare it. No `OpenBaoClient._run_ok` raw failure reaches
receipts: capture/discard backend output and use closed reason codes. A generic
403/timeout or “already exists” is not successful verification.
9. Finally self-revoke the task token and login-self token, then retire only the
exact per-operation grant policy under founder-owned cleanup authority.
Record confirmed revocation/absence separately from native apply status.
Policy retirement alone does not prove issued-token revocation.
Task-token use budget is declared before issuance: two before-state reads, two
writes, two readbacks and one self-revoke (seven uses), plus at most four explicit
metadata capability checks and one reserved cleanup use (twelve total). Failed
backend calls count; never retry to evade limits. Task TTL is
`min(120s, approval_remaining, decision_remaining, operation_remaining)` minus
the declared clock uncertainty/cleanup reserve. Reject if fewer than 30s remain
for planned requests. No renewal or remint extends the operation. CCR38 keeps its
own 300s/eight-use limit; login-self checks and revoke fit its separate budget.
## Failure, replay and recovery
Ledger states: `reserved → consume-confirmed → authenticating → grant-issued →
applying → verified`, followed by a separate `cleanup-confirmed` or
`cleanup-unconfirmed` result. Every external call is durably recorded as about
to execute before the call, then recorded complete afterward. Crash between
those steps is `unknown` rather than presumed failure. Concurrent duplicate
requests never both reach consume or OpenBao.
An operation can end refused before consumption, consumed-without-backend-work,
partially-applied, backend-outcome-unknown, verified-cleanup-unconfirmed or
verified-cleanup-confirmed. No incomplete state permits blind reapplication.
Cleanup continues when an action deadline expires, under the original explicit
cleanup obligation, without new business writes. Read/repair of changed target
state requires a fresh exact owner-reviewed recovery approval; no old snapshot
overwrites concurrent owner edits. Hard interruption uses the Platform owner's
bounded revocation/cleanup procedure with protected accessor custody. Ledger
and evidence carry only fingerprints/metadata; the raw accessor/token never
leaves owner memory or the separately admitted owner-private recovery channel.
Revocation failure blocks owner acceptance and T06 closure, even if TTL will
eventually expire. Emit non-secret atomic local lifecycle events/outbox records
at the ledger boundaries; audit-core outage does not suppress cleanup. Do not
claim an empty archive proves no effects, or that local ledger state is founder
approval. Owner acceptance requires native before/after equality, denied sibling
actions and verified cleanup under the same exact trial.
## Implementation hooks and closure tests
- `engine_auth.py`: staged readiness guard now. Later separate identity-session
and execution-adapter objects; generic service-jwt client remains self-only.
- `cli.py`: staged guard passed through backend-action approval callers now.
New explicit execute-ccr preparation/mediated dispatch later; ordinary apply
does not acquire new rights merely because the login contract is configured.
- `authorization.py`, `approval_consume.py`: execution context participates in
both approval-free binding and full PDP digest; owner child returns the actual
`ConsumedApproval`, not only an `AuthorizedAction` ID.
- New `execution_contract.py`, `execution_ledger.py`, `mediated_backend.py`:
strict closed schema, deterministic compiler, separate admission state,
pinned owner-native adapter and non-secret outcome types. Ordinary subprocess
command execution is not an implementation of this adapter.
- Platform owns task-execution-grant CCR/typed validator/token materializer/
fixed execution child/recovery hooks. Approval Engine/PDP owners acknowledge
the request+claim/package correspondence; secrets-engine cannot assent for them.
Counterexamples required before shipping: login-only auto provider consumes
nothing; stale/forged grant file changes nothing; same name with changed HCL or
role body refuses; wrong method/sibling policy/role/KV/SecretID/child-token/mount
write refuses; changed source/caller/tenant/catalog/deadline alters the bound
request and refuses; consume conflict/unavailability/timeout produces zero
backend calls; idempotent approval consumption never causes a second execution;
concurrent calls admit one worker; postconsume login failure never falls back;
expiry during a multi-call operation stops new mutations but permits scoped
cleanup; readback mismatch/transport failure is incomplete; revoke failure
never passes; partial apply and crash after write require explicit recovery.
Current staged readiness regression suite: eight new checks plus 28 existing
auth, consume and privileged-evidence checks pass (36 total). No native action
was performed. Full entitlement implementation, policy-package publication,
independent Platform/Secrets Engine/identity/approval/PDP acknowledgement and
exact founder-scoped native proof remain required. A successful metadata trial
alone would not cover value provision or exec delivery.
## Existing commands and the next contract they actually need
| Existing operation | Current login-only outcome | Contract required after the metadata pilot |
| --- | --- | --- |
| apply / apply_plan | Readiness refusal before consume; no backend | A separate exact policy/AppRole/mount structure compiler and grant for each admitted PlanAction. The workload-kv-read execute-ccr pilot does not implement these generic AppRole/mount calls. |
| provision / rotate | Readiness refusal before consume; no KV writes | Exact KV v2 data path, declared field set, expected CAS version, no sibling changes, owner-held opaque input handle, confirmed new version and separately admitted revoke/cleanup. Never expose secret plaintext or guessable secret-value hashes in request/evidence. |
| verify positive/negative | Readiness refusal before consume; no custody reads | Metadata capability probe plus, only when required, exact declared field-presence read through a restricted owner adapter. Return booleans; malformed/denied/transport error is inconclusive, not absence. Independent negative identity and exact deny paths are separate bindings. |
| exec / wrap | Readiness refusal before consume; no read/injection/wrap | Exact custody path/field, signed executable/cwd/destination/exec-owner binding and duration, no user arbitrary shell, owner-mediated read and approved process-local delivery, or exact one-use wrap creation/unwrap/expiry; self revoke and injected material cleanup. |
| handoff | Readiness refusal before consume; no role-id/SecretID mint | Exact existing AppRole and single-use SecretID creation, bounded TTL/uses and owner-admitted delivery; native role-id lookup/SecretID mint/login/revoke budget and no caller standing credential. |
| revoke / native suspend/deactivate | Readiness refusal before consume; no backend revocation/deletion | Exact policy/role/lease/accessor target and expected state, admitted lifecycle effect, rollback/custody preservation and denial readback. Token/lease handles remain owner-private; receipt fingerprints cannot authorize another handle. |
| session revoke | Readiness refusal before its backend; legacy stance remains | Exact already-held protected handle, typed token/lease revocation and independent subject/authority binding; no arbitrary accessor or lease-prefix wildcard. |
| destroy | Existing live destroy refusal remains | Exact destructive dual-control contract and custody/backup/version proof; metadata pilot supplies neither. |
| compromise / reactivate local state | Existing local state gate remains | No new OpenBao entitlement; these local records do not establish actual native revocation/reactivation. |
| service-auth probe-openbao | Existing explicit self-only identity trial | CCR38 positive/negative/lifetime/revoke proof, with no implied entitlement to any catalog lane. |
Each subsequent contract needs its own owner-reviewed compiler, request/PDP
package binding, method/body enforcement, consume admission, TTL/use budget,
failure/recovery rules and native counterexamples. No success in the initial
metadata pilot unblocks these rows by default. Full T06 adoption requires the
actual selected engine production workflow to run through the corresponding
reviewed entitlement, with native effect, denial/no-fallback and cleanup proof.
## Source preparation: typed request propagation (2026-10-11)
The staged ExecutionContract descriptor is deliberately a closed, immutable
non-secret value. `parse` validates shape and captures JSON; every `as_dict`
revalidates even directly constructed instances and returns a detached copy.
It is not an owner approval, source authenticity attestation, principal
representation or proof that the requested origin is admitted. Compiler and
mediator must independently reconstruct and authenticate sources and bodies.
The exact proposed descriptor serialization refines the table above:
- Digests use `sha256:` plus 64 lowercase hexadecimal characters. UUIDs use
canonical lowercase hyphenated form. CCR and grant IDs must be distinct.
- `source_bundle` has exactly engine_dispatcher, platform_mediator, typed_ccr and
policy_auth; each has exactly an absolute normalized `path` and `sha256`.
This parses paths without reading files; pin verification remains unimplemented.
- `operations` is an ordered list of exactly method/path/payload_sha256 objects.
It accepts only GET/PUT metadata policy and auth-role endpoint shapes, with
normalized relative paths. This schema does not reconstruct bodies or prove
that all before/write/readback operations occur or target exactly two paths.
A later owner compiler must enforce those semantics before any execution.
- `backend_origin` is an explicit-port HTTP(S) origin without credentials, path,
query or fragment. Syntactic acceptance creates no approved route or network
call. Estate pinning and server authentication remain mediator obligations.
- `backend_budget` has exactly ttl_seconds (integer 30–120) and uses (integer
1–12); booleans fail. Listed calls must leave at least one cleanup use.
Renewal, remint and live per-call accounting remain outside this descriptor.
- expected_before/expected_after are non-secret state digests; operation_deadline
is timezone-aware. Before claim, refuse expired deadlines using the existing
trusted-time window. After claim and Check, refuse a deadline later than either
served approval or decision expiry. A future mediator must recheck before every
effect and provide separately authorized cleanup after business expiry.
- catalog_sha256 is a descriptor-local sorted compact JSON fingerprint of the
complete existing context.catalog_target, distinct from canonical PDP digests.
Resource catalog ID/stage, subject, tenant and purpose remain bound separately
through the unchanged canonical request hashing implementation.
The same typed descriptor passes through build_action_request, _expected_request,
resolve_approval_observation and authorize_action. Any contract on apply or another
legacy action refuses. execute-ccr without a typed valid contract, prod stage or
platform tenant refuses. execute-ccr always binds human_control=true, even on a
legacy bootstrap catalog, and cannot use an unserved/demo fallback. Invalid or
catalog-mismatched descriptors refuse before claim/PDP/CAS. Returned requests are
independent copies; later caller mutation cannot change the stored descriptor.
99 focused source tests pass across context propagation, the existing approval
join, decision validation, readiness, consume and engine auth. New tests exercise
all four actual builders, actual consume request formatting and claim-free/full
joins; mutation of operation, CCR/grant/source/body/method/path/budget/deadline/
estate/state/principal/purpose/catalog/tenant/stage refuses the original approval.
Transport/PDP stand-ins deliberately supply hypothetical human control and an
ALLOW: they prove propagation, not a shipped policy or real principal admission.
No execute-ccr CLI, identity-owner verified initiating-principal interface,
new access policy package, typed owner compiler, task-grant materializer,
independent authenticated consume-confirmation integration, durable admission
journal, restricted backend, per-call deadline checks or native entitlement is
supplied by this stage. Existing cfg subject strings remain legacy provenance;
this change does not make them verified human representation. Existing generic
consume receipt validation limitations remain explicitly separate. service-jwt
readiness guard/default and ordinary actions remain unchanged. T01 stays in
progress, T03 stays wait, WP13 stays proposed, and WP8-T06 stays open.
## D3 wrapped-delivery primitive and its separate entitlement
D3 P3=b chooses response-wrapped issuer delivery with one-time unwrap. The
existing wrapped-handoff primitive now verifies native TTL/accessor/creation-path
metadata and reserves a private immutable recipient file before minting. Native
KV-v2 creation paths include the `data/` endpoint; caller-requested CLI paths
cannot substitute for returned metadata. Failed mint/write or an altered
reservation retains an uncertain attempt and never silently overwrites/remints.
A disposable local OpenBao integration proves the exact source path,60-second
TTL, successful first unwrap and specific HTTP400 consumed-token replay denial.
This proves backend semantics, not the estate's first delivered lane, audited
initiating principal, consumer identity, independent owner acceptance or cleanup
under crash/revocation. Production CLI wrapping remains fail-closed. Existing
metadata execute-ccr contracts expressly exclude wrap/KV value delivery and cannot
be borrowed for this purpose. A separate exact owner-admitted delivery operation
must bind actor,path,audience,deadline,authorization/consume state,destination,
audit,replay/crash recovery and rollback before native adoption. The prepared
D2 repository-issuer lane remains attended until its own reviewed execution and
independent destination/adoption proofs pass.

View file

@ -0,0 +1,563 @@
"""PEP consume-before-side-effect client (GH-DEC-2026-003).
The PEP that is about to cause a protected OpenBao write MUST obtain a
successful approval-engine CAS consume first. Holding a claim or an ALLOW is
not authority to act. Conflict, unavailability, or a missing binding means
do not call OpenBao.
This module does not render an authorization decision. The consume response
is mutation evidence, never a permission.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
from urllib.error import HTTPError, URLError
from urllib.request import Request
from secrets_engine.approval_auth import approval_auth_configured, approval_token, credential_urlopen
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
from secrets_engine.catalog import human_control_required
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
from secrets_engine.decision_signature import load_trust, strict_json
from secrets_engine.authorization import (
build_action_request,
validate_decision_envelope,
wait_for_decision_start,
)
from secrets_engine.errors import DecisionError
from secrets_engine.application_time import read_window, recheck_binding
from secrets_engine.openbao import read_strict_token_file
from secrets_engine.pep_stance import demo_exception_enabled
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
_MAX_BODY = 256 * 1024
@dataclass(frozen=True)
class ConsumeBinding:
"""Inputs the PEP presents to approval-engine consume."""
approval_id: str
request_digest: str
decision_id: str = ""
human_control: bool = False
decision_not_before: str = ""
decision_expires_at: str = ""
@dataclass(frozen=True)
class ApprovalObservation:
"""Fresh approval input awaiting PDP correspondence; never a consume binding."""
approval_id: str
request: dict[str, Any] = field(repr=False)
claim: dict[str, Any] = field(repr=False)
@dataclass(frozen=True)
class AuthorizedAction:
"""A validated claim and decision for one exact proposed action.
Holding this is not authority to act: GH-DEC-2026-003 still requires a
successful CAS consume before the OpenBao call.
"""
binding: ConsumeBinding
decision_id: str
expires_at: str
# Detached original served artifacts for the narrow task-execution handoff.
artifacts: str = field(default="", repr=False, compare=False)
@property
def id(self) -> str:
"""The actual PDP decision id for shared CLI evidence/plan callers."""
return self.decision_id
def as_evidence(self) -> dict[str, object]:
return {
"authorization_decision_id": self.decision_id,
"authorization_expires_at": self.expires_at,
"request_digest": self.binding.request_digest,
"approval_human_control": self.binding.human_control,
}
@dataclass(frozen=True)
class ConsumedApproval:
"""Non-secret confirmation that consume succeeded for this request."""
approval_id: str
request_digest: str
decision_id: str = ""
idempotent: bool = False
consumed_at: str = ""
def as_evidence(self) -> dict[str, object]:
payload: dict[str, object] = {
"approval_consumed": True,
"approval_id": self.approval_id,
"request_digest": self.request_digest,
"approval_consume_idempotent": self.idempotent,
}
if self.decision_id:
payload["decision_id"] = self.decision_id
if self.consumed_at:
payload["approval_consumed_at"] = self.consumed_at
return payload
def _authorization_id(entry: Any, decision: Any) -> str:
"""Non-secret approval-engine object id for this lane, or "" if unbound.
flex-auth: ``ActionAuthorization.id`` is the approval-engine object UUID.
It is never the State Hub decision UUID, so it is not inferred from one.
"""
approval = getattr(entry, "approval", None) or {}
if isinstance(approval, dict):
declared = str(approval.get("authorization_id", "") or "").strip()
if declared:
return declared
for attr in ("authorization_id", "action_authorization_id"):
served = str(getattr(decision, attr, "") or "").strip()
if served:
return served
return ""
def _request_purpose(entry: Any) -> str:
"""Declared purpose for the request context. Never invented at call time."""
approval = getattr(entry, "approval", None) or {}
if isinstance(approval, dict):
declared = str(approval.get("purpose", "") or "").strip()
if declared:
return declared
for consumer in getattr(entry, "consumers", None) or []:
if isinstance(consumer, dict):
declared = str(consumer.get("purpose", "") or "").strip()
if declared:
return declared
return ""
def _expected_request(
cfg: Any,
entry: Any,
action: str,
*,
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
) -> dict[str, Any]:
"""Build the exact CheckRequest both steps must agree on.
Steps 1 and 2 must describe the same proposed action or the digests cannot
correspond, so neither builds its own.
"""
subject_id = str(getattr(cfg, "authorization_subject_id", "") or "")
subject_type = str(getattr(cfg, "authorization_subject_type", "") or "")
if not subject_id or not subject_type:
raise DecisionError(
"authorization join requires SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID "
"and _SUBJECT_TYPE; the PEP must not assert an unnamed subject"
)
purpose = _request_purpose(entry)
if not purpose:
raise DecisionError(
"authorization join requires a declared approval/consumer purpose"
)
return build_action_request(
entry,
action,
subject_id=subject_id,
subject_type=subject_type,
purpose=purpose,
fields=fields,
policy_targets=policy_targets,
auth_targets=auth_targets,
execution_contract=execution_contract,
)
def fetch_approval_claim(
*,
base_url: str,
token_file: Path | None = None,
token_provider: Callable[[], str] | None = None,
authorization_id: str,
timeout_seconds: float = 3,
opener: Callable[..., Any] = credential_urlopen,
) -> dict[str, Any]:
"""GET /v1/approvals/{id}/claim (PIP). Any non-200 fails closed.
The body is approval-engine's approval-claim, not a flex-auth
ActionAuthorization -- that object is deferred and was never ratified
(GH-DEC-2026-005 / FLEX-DEC-2026-006).
"""
if not base_url or not base_url.startswith(("http://", "https://")):
raise DecisionError("approval-engine claim URL is missing or invalid")
ident = authorization_id.strip()
if not ident or "/" in ident or any(ch.isspace() for ch in ident):
raise DecisionError("approval claim requires a concrete authorization id")
token = _request_token(token_file, token_provider)
request = Request(
base_url.rstrip("/") + f"/v1/approvals/{ident}/claim",
method="GET",
)
request.add_header("Authorization", f"Bearer {token}")
request.add_header("Accept", "application/json")
try:
with opener(request, timeout=timeout_seconds) as response:
if getattr(response, "status", 200) != 200:
raise DecisionError("approval claim did not return a claim")
payload = json.loads(response.read(_MAX_BODY).decode("utf-8"))
except HTTPError as e:
raise DecisionError(f"approval claim refused: {_status_message(e.code)}") from e
except URLError as e:
raise DecisionError("approval-engine is unreachable for claim") from e
except json.JSONDecodeError as e:
raise DecisionError("approval claim returned a non-JSON body") from e
if not isinstance(payload, dict):
raise DecisionError("approval claim returned a non-object body")
return payload
def resolve_approval_observation(
cfg: Any,
entry: Any,
action: str,
decision: Any,
*,
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
opener: Callable[..., Any] | None = None,
) -> ApprovalObservation | None:
"""Observe a fresh fact and prepare the claim-bearing request (step 1).
Missing serving coordinates return None for undeclared lanes. A declared
human control raises instead, so no stage/demo fallback can discharge it.
Correspondence and a consume binding require authorize_action's PDP step.
"""
execution_request = None
if action == "execute-ccr" or execution_contract is not None:
execution_request = _expected_request(
cfg, entry, action, fields=fields, policy_targets=policy_targets,
auth_targets=auth_targets, execution_contract=execution_contract,
)
# The caller's descriptor is not authenticated principal/owner evidence.
# This requirement cannot disappear on an unserved/demo lane.
base_url = str(getattr(cfg, "approval_url", "") or "")
auth_configured = approval_auth_configured(cfg)
authorization_id = _authorization_id(entry, decision)
required_human_control = action == "execute-ccr" or human_control_required(entry)
if not base_url or not auth_configured or not authorization_id:
if required_human_control:
raise DecisionError("human_control requires a served approval claim and exact action binding")
return None
expected_request = execution_request or _expected_request(
cfg, entry, action,
fields=fields, policy_targets=policy_targets, auth_targets=auth_targets,
execution_contract=execution_contract,
)
require_execution_deadline(expected_request, time_window=read_window(cfg))
claim = fetch_approval_claim(
base_url=base_url,
token_provider=lambda: approval_token(cfg, scope="approval:read"),
authorization_id=authorization_id,
opener=opener or credential_urlopen,
)
observe_pdp_approval_claim(claim, approval_id=authorization_id,
require_human_control=required_human_control, time_window=read_window(cfg))
require_execution_deadline(expected_request, time_window=read_window(cfg),
bounds=(claim["validity"]["expires_at"],))
expected_request["context"]["approval"] = claim
return ApprovalObservation(authorization_id, expected_request, claim)
def authorize_action(
cfg: Any,
entry: Any,
action: str,
decision: Any = None,
*,
fields: tuple[str, ...] = (),
policy_targets: tuple[str, ...] = (),
auth_targets: tuple[str, ...] = (),
execution_contract: ExecutionContract | None = None,
opener: Callable[..., Any] | None = None,
pdp_opener: Callable[..., Any] | None = None,
) -> AuthorizedAction | None:
"""Run steps 1 and 2 for one proposed action, or return None if unserved.
Step 1 validates the approval-claim; step 2 obtains and validates the
flex-auth DecisionEnvelope. Returning None means no serving path is
configured at all, which leaves production fail-closed. A configured but
failing path raises: a partial deployment must not read as an absent one.
"""
serving = (getattr(cfg, "approval_url", "") and approval_auth_configured(cfg)
and _authorization_id(entry, decision))
if serving or (getattr(cfg, "pdp_url", "") and getattr(cfg, "pdp_token_file", None)):
if not getattr(cfg, "pdp_url", "") or not getattr(cfg, "pdp_token_file", None):
raise DecisionError("production action requires configured access-engine URL/token and responder trust")
require_supported_pdp_address(str(cfg.pdp_url))
load_trust(getattr(cfg, "pdp_trust_file", None))
verifier = getattr(cfg, "pdp_verifier", None)
if verifier is None or not Path(verifier).is_absolute() or not Path(verifier).is_file():
raise DecisionError("access-engine signature verifier is not installed")
observation = resolve_approval_observation(
cfg, entry, action, decision,
fields=fields,
policy_targets=policy_targets,
auth_targets=auth_targets,
execution_contract=execution_contract,
opener=opener,
)
if observation is None:
return None
pdp_url = str(getattr(cfg, "pdp_url", "") or "")
pdp_token = getattr(cfg, "pdp_token_file", None)
if not pdp_url or not pdp_token:
raise DecisionError(
"production action requires an access-engine decision; "
"SECRETS_ENGINE_PDP_URL / _PDP_TOKEN_FILE are unset"
)
package = str(getattr(cfg, "authorization_policy_package", "") or "")
version = str(getattr(cfg, "authorization_policy_version", "") or "")
if not package or not version:
raise DecisionError(
"authorization join requires an explicitly configured policy "
"package/version pin; the reserved coordinate is a reservation, "
"not a publication, and must not be used as a default"
)
expected_request = observation.request
envelope = check_decision(
base_url=pdp_url,
token_file=Path(pdp_token),
request=expected_request,
opener=pdp_opener,
trust_file=getattr(cfg, "pdp_trust_file", None),
verifier=getattr(cfg, "pdp_verifier", None),
)
if not getattr(cfg, "clock_trust_file", None):
wait_for_decision_start(envelope)
window = read_window(cfg)
validated = validate_decision_envelope(
envelope,
expected_request,
accepted_policy_packages={package},
accepted_policy_versions={version},
expected_approval_binding_digest=observation.claim["binding"]["pdp_digest"],
time_window=window,
)
# Recheck freshness after the network call before issuing a consume binding.
validate_approval_claim(
observation.claim, approval_id=observation.approval_id,
expected_pdp_digest=envelope["binding"]["approval_binding_digest"],
require_human_control=expected_request["context"].get("human_control") is True,
time_window=window,
)
require_execution_deadline(expected_request, time_window=window,
bounds=(observation.claim["validity"]["expires_at"], validated.expires_at))
if validated.action != action:
raise DecisionError("access-engine decision does not bind this action")
return AuthorizedAction(
binding=ConsumeBinding(
approval_id=observation.approval_id,
request_digest=validated.request_digest,
decision_id=validated.decision_id,
human_control=observation.claim["binding"].get("human_control") is True,
decision_not_before=envelope["lifetime"].get("not_before") or "",
decision_expires_at=validated.expires_at,
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
sort_keys=True,separators=(",",":"),allow_nan=False),
)
def consume_approval(
*,
base_url: str,
token_file: Path | None = None,
token_provider: Callable[[], str] | None = None,
binding: ConsumeBinding,
timeout_seconds: float = 3,
opener: Callable[..., Any] = credential_urlopen,
strict_response: bool = False,
) -> ConsumedApproval:
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
if not base_url or not base_url.startswith(("http://", "https://")):
raise DecisionError("approval-engine consume URL is missing or invalid")
approval_id = binding.approval_id.strip()
if not approval_id or "/" in approval_id or any(ch.isspace() for ch in approval_id):
raise DecisionError("approval consume requires a concrete approval id")
if not DIGEST_RE.fullmatch(binding.request_digest):
raise DecisionError("approval consume requires the canonical request digest")
token = _request_token(token_file, token_provider)
body: dict[str, str] = {"request_digest": binding.request_digest}
if binding.decision_id:
body["decision_id"] = binding.decision_id
encoded = json.dumps(body).encode("utf-8")
request = Request(
base_url.rstrip("/") + f"/v1/approvals/{approval_id}/consume",
data=encoded,
method="POST",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
},
)
try:
response = opener(request, timeout=timeout_seconds)
try:
status = int(response.getcode())
raw = response.read(_MAX_BODY + 1)
finally:
response.close()
except HTTPError as exc:
status = int(getattr(exc, "code", 0) or 0)
try:
exc.read(_MAX_BODY)
except Exception:
pass
raise DecisionError(_status_message(status)) from None
except (URLError, TimeoutError, OSError):
raise DecisionError(
"approval-engine unreachable; OpenBao must not be called"
) from None
if status != 200 or len(raw) > _MAX_BODY:
raise DecisionError(_status_message(status if status != 200 else 502))
try:
payload = (strict_json if strict_response else json.loads)(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError, RecursionError) as exc:
raise DecisionError("approval consume returned invalid JSON") from exc
if not isinstance(payload, dict):
raise DecisionError("approval consume returned invalid payload")
if payload.get("status") != "consumed":
raise DecisionError("approval consumption was not confirmed")
if payload.get("request_digest") != binding.request_digest:
raise DecisionError("approval consume digest does not match the request")
if strict_response:
from datetime import datetime
if (payload.get("approval_id") != approval_id
or payload.get("decision_id") != binding.decision_id
or payload.get("idempotent") is not False
or any(key in payload for key in ("effect","allow","deny","decision"))):
raise DecisionError("strict task consumption correspondence refused")
try:
stamp = datetime.fromisoformat(payload["consumed_at"].replace("Z","+00:00"))
if stamp.tzinfo is None or stamp.utcoffset() is None:
raise ValueError("timezone required")
except (KeyError,TypeError,AttributeError,ValueError):
raise DecisionError("strict task consumption timestamp refused") from None
decision_id = payload.get("decision_id")
if decision_id is not None and (
not isinstance(decision_id, str) or not decision_id
):
raise DecisionError("approval consume returned an invalid decision id")
consumed_at = payload.get("consumed_at")
if consumed_at is not None and not isinstance(consumed_at, str):
raise DecisionError("approval consume returned an invalid consumed_at")
return ConsumedApproval(
approval_id=str(payload.get("approval_id") or approval_id),
request_digest=binding.request_digest,
decision_id=decision_id or binding.decision_id,
idempotent=bool(payload.get("idempotent")),
consumed_at=consumed_at or "",
)
def require_production_consume(
cfg: Any,
entry: Any,
*,
binding: ConsumeBinding | None,
evidence: Any = None,
opener: Callable[..., Any] | None = None,
) -> ConsumedApproval | None:
"""CAS-consume before production or an explicitly human-controlled action.
Undeclared build/test and the unsafe-demo exception retain their existing
behavior. Declared human controls require a verified binding and consume
regardless of stage/demo; missing inputs cannot fall back to a lane review.
"""
required_human_control = human_control_required(entry)
if required_human_control:
if binding is None or binding.human_control is not True:
raise DecisionError("human_control requires an observed declared-human consume binding")
else:
if getattr(entry, "stage", "") != "prod":
return None
if demo_exception_enabled(cfg):
return None
if binding is None:
raise DecisionError(
"production OpenBao call requires CAS consume of an approval "
"after an access-engine ALLOW; no durable consume binding is served"
)
base_url = str(getattr(cfg, "approval_url", "") or "")
auth_configured = approval_auth_configured(cfg)
if not base_url:
raise DecisionError(
"production OpenBao call requires approval-engine consume; "
"SECRETS_ENGINE_APPROVAL_URL is unset"
)
if not auth_configured:
raise DecisionError(
"production OpenBao call requires approval-engine consume; "
"SECRETS_ENGINE_APPROVAL_TOKEN_FILE or _CLIENT_SECRET_FILE is unset"
)
recheck_binding(cfg, binding)
consumed = consume_approval(
base_url=base_url,
token_provider=lambda: approval_token(cfg, scope="approval:consume"),
binding=binding,
opener=opener or credential_urlopen,
)
if evidence is not None and hasattr(evidence, "mark_consumed"):
evidence.mark_consumed(consumed)
recheck_binding(cfg, binding)
return consumed
def _status_message(status: int) -> str:
if status == 409:
return "approval consume conflict; OpenBao must not be called"
if status == 404:
return "approval not found; OpenBao must not be called"
if status in {401, 403}:
return "approval consume unauthorized; OpenBao must not be called"
if status == 503:
return "approval-engine unavailable; OpenBao must not be called"
if status == 0:
return "approval-engine unreachable; OpenBao must not be called"
return "approval consume failed; OpenBao must not be called"
def _request_token(
token_file: Path | None, token_provider: Callable[[], str] | None,
) -> str:
if (token_file is None) == (token_provider is None):
raise DecisionError("approval request requires exactly one credential provider")
token = (
token_provider() if token_provider is not None
else read_strict_token_file(Path(token_file), purpose="approval credential")
)
if not isinstance(token, str) or not token or any(ch.isspace() for ch in token):
raise DecisionError("approval credential is invalid")
return token

View file

@ -0,0 +1,83 @@
"""Independently admitted Access Engine responder keys, never responder discovery.
The bundled Go helper uses the exact producer types and verifier. No Python
canonicalization or unsigned compatibility mode participates in authorization.
"""
from __future__ import annotations
import base64
import json
from pathlib import Path
import stat
import subprocess
from typing import Any
from secrets_engine.errors import DecisionError
def _unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate JSON member")
result[key] = value
return result
def strict_json(data):
return json.loads(data, object_pairs_hook=_unique_object,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
def load_trust(path: Path | None) -> dict[str, Any]:
"""Read an operator-owned public admission file; missing trust is a refusal.
Admission config is a local authority input like the existing policy pin.
File provenance/approval and the executable's installation are rollout gates,
not established by a successful local signature test.
"""
try:
if path is None or not Path(path).is_absolute() or Path(path).is_symlink():
raise ValueError("trust path must be absolute and regular")
with Path(path).open('rb') as stream:
import os
info = os.fstat(stream.fileno())
if not stat.S_ISREG(info.st_mode) or info.st_mode & 0o022:
raise ValueError("trust must not be group/world writable")
raw = stream.read(65537)
if len(raw) > 65536:
raise ValueError("trust is oversized")
trust = strict_json(raw)
if set(trust) != {'schema', 'evaluator', 'mode', 'policy_package', 'policy_version', 'policy_package_digest', 'keys'}:
raise ValueError("invalid trust fields")
if trust['schema'] != 'secrets-engine.pdp-trust.v1':
raise ValueError("unsupported trust schema")
if any(not isinstance(trust[k], str) or not trust[k] for k in trust if k != 'keys'):
raise ValueError("missing issuer/pin")
if not isinstance(trust['keys'], dict) or not trust['keys'] or len(trust['keys']) > 16:
raise ValueError("missing admitted keys")
if any(not isinstance(k,str) or not k or not isinstance(v,str) or not v for k,v in trust['keys'].items()):
raise ValueError("invalid keys")
for key in trust["keys"].values():
if len(key) != 43 or len(base64.b64decode(key + "=", altchars=b"-_", validate=True)) != 32:
raise ValueError("invalid Ed25519 public key")
return trust
except (OSError, ValueError, TypeError, KeyError, RecursionError) as exc:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
raise ValueError("signature refused")
authenticated = strict_json(result.stdout)
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
return authenticated
except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc

View file

@ -0,0 +1,176 @@
"""Closed, immutable metadata descriptor; shape is never execution authority.
This source-only compiler input cannot grant an action, authenticate a principal,
prove owner source authenticity or open a backend. Those integration gates remain
outside this type. Existing actions cannot borrow the new descriptor.
"""
from __future__ import annotations
import copy
import hashlib
import json
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import PurePosixPath
from urllib.parse import urlsplit
from uuid import UUID
from secrets_engine.errors import DecisionError
VERSION = "SECRETS-EXEC-20261011-v1"
HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
KEYS = {
"contract_version", "operation_id", "operation", "execution_actor", "ccr_id",
"ccr_immutable_sha256", "execution_grant_ccr_id", "grant_immutable_sha256",
"catalog_sha256", "source_bundle", "backend_origin", "cluster_uid",
"operations", "operation_deadline", "backend_budget", "expected_before",
"expected_after",
}
SOURCE_ROLES = {"engine_dispatcher", "platform_mediator", "typed_ccr", "policy_auth"}
def _refuse() -> None:
# Do not interpolate untrusted descriptors into messages/evidence.
raise DecisionError("invalid execution_contract descriptor")
def _closed(value, keys):
if type(value) is not dict or set(value) != set(keys):
_refuse()
return value
def _text(value):
if type(value) is not str or not value or len(value) > 4096 or any(ord(c) < 32 for c in value):
_refuse()
return value
def _hash(value):
if type(value) is not str or not HASH.fullmatch(value):
_refuse()
def _path(value, *, absolute=False):
_text(value)
if (value.startswith("/") != absolute or "\\" in value or "%" in value
or "?" in value or "#" in value or ":" in value
or any(p in {"", ".", ".."} for p in value.lstrip("/").split("/"))
or str(PurePosixPath(value)) != value):
_refuse()
def _validated(value):
value = copy.deepcopy(_closed(value, KEYS))
if value['contract_version'] != VERSION or value['operation'] != 'metadata-apply-ccr' or value['execution_actor'] != 'service:secrets-engine':
_refuse()
try:
if str(UUID(_text(value['operation_id']))) != value['operation_id']:
_refuse()
except (ValueError, AttributeError):
_refuse()
for key in ['ccr_id', 'execution_grant_ccr_id', 'cluster_uid']:
_text(value[key])
if value['ccr_id'] == value['execution_grant_ccr_id']:
_refuse()
for key in ['ccr_immutable_sha256', 'grant_immutable_sha256', 'catalog_sha256', 'expected_before', 'expected_after']:
_hash(value[key])
sources = _closed(value['source_bundle'], SOURCE_ROLES)
for source in sources.values():
_closed(source, {'path', 'sha256'})
_path(source['path'], absolute=True)
_hash(source['sha256'])
try:
origin = urlsplit(_text(value['backend_origin']))
if (origin.scheme not in {'http', 'https'} or not origin.hostname
or origin.username is not None or origin.password is not None
or origin.path or origin.query or origin.fragment or origin.port is None):
_refuse()
except ValueError:
_refuse()
try:
deadline = datetime.fromisoformat(_text(value['operation_deadline']).replace('Z', '+00:00'))
if deadline.tzinfo is None:
_refuse()
except ValueError:
_refuse()
budget = _closed(value['backend_budget'], {'ttl_seconds', 'uses'})
if (type(budget['ttl_seconds']) is not int or not 30 <= budget['ttl_seconds'] <= 120
or type(budget['uses']) is not int or not 1 <= budget['uses'] <= 12):
_refuse()
operations = value['operations']
if type(operations) is not list or not 1 <= len(operations) < budget['uses']:
_refuse() # Reserve at least one cleanup use; no silent retries.
for operation in operations:
_closed(operation, {'method', 'path', 'payload_sha256'})
if type(operation['method']) is not str or operation['method'] not in {'GET', 'PUT'}:
_refuse()
_path(operation['path'])
# Only metadata pilot endpoint shapes, never KV/mount/token/secretid.
if not re.fullmatch(r'(sys/policies/acl/[A-Za-z0-9_-]+|auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+)', operation['path']):
_refuse()
_hash(operation['payload_sha256'])
return value
def catalog_target_digest(target: dict) -> str:
"""Descriptor-local sorted JSON fingerprint, distinct from PDP digest."""
encoded = json.dumps(target, sort_keys=True, ensure_ascii=False, allow_nan=False,
separators=(',', ':')).encode('utf-8')
return 'sha256:' + hashlib.sha256(encoded).hexdigest()
@dataclass(frozen=True)
class ExecutionContract:
_serialized: str
@classmethod
def parse(cls, descriptor: dict) -> 'ExecutionContract':
validated = _validated(descriptor)
return cls(json.dumps(validated, sort_keys=True, separators=(',', ':'), allow_nan=False))
def as_dict(self) -> dict:
# Revalidate even a directly constructed instance: no trusted label bypass.
try:
return _validated(json.loads(self._serialized))
except (ValueError, TypeError, RecursionError):
_refuse()
def attach_execution_contract(request: dict, contract: ExecutionContract | None) -> None:
action = request['action']
if action != 'execute-ccr':
if contract is not None:
raise DecisionError('execution_contract requires execute-ccr action')
return
if type(contract) is not ExecutionContract:
raise DecisionError('execute-ccr requires typed execution_contract')
value = contract.as_dict()
if request['resource']['attributes']['stage'] != 'prod':
raise DecisionError('execute-ccr requires prod stage')
if request['tenant'] != 'tenant:platform':
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
request['context']['execution_contract'] = value
request['context']['human_control'] = True
def require_execution_deadline(request: dict, *, time_window=None, bounds=()) -> None:
"""Check trusted-time freshness and served upper bounds before CAS binding."""
contract = request.get('context', {}).get('execution_contract')
if contract is None:
return
from secrets_engine.application_time import validity_bounds
deadline = datetime.fromisoformat(contract['operation_deadline'].replace('Z', '+00:00'))
_, upper = validity_bounds(None, time_window)
if upper >= deadline:
raise DecisionError('execution_contract deadline has expired')
for bound in bounds:
try:
limit = datetime.fromisoformat(bound.replace('Z', '+00:00'))
if limit.tzinfo is None or deadline > limit:
raise DecisionError('execution_contract deadline exceeds served authority')
except (ValueError, TypeError, AttributeError):
raise DecisionError('execution_contract requires served deadline') from None

View file

@ -0,0 +1,194 @@
"""Narrow service-task PEP handoff; records never grant native execution.
Platform calls preparation itself, binds its independently compiled owner Plan,
reserves/journals once, then calls strict consumption and independent Approval
GET confirmation. No inbound mapping/boolean or constructor creates authority.
Actual policy/server input mapping and runtime entitlement remain unadmitted.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest, validate_approval_claim
from secrets_engine.approval_consume import authorize_action, consume_approval
from secrets_engine.application_time import read_window, validity_bounds
from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest
from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
_CALLER = 'system:serviceaccount:secrets-engine:secrets-engine'
_NATIVE_FIELDS = ('action','actor','principal','purpose','target')
_COORDINATES = ('approval_url','approval_token_file','approval_client_secret_file','keycape_token_url','keycape_issuer',
'pdp_url','pdp_token_file','pdp_trust_file','pdp_verifier','clock_trust_file',
'authorization_subject_id','authorization_subject_type','authorization_policy_package','authorization_policy_version')
_ISSUED = object()
def _refuse():
raise DecisionError('strict task execution handoff correspondence refused')
def _coordinates(cfg):
return tuple(str(getattr(cfg,name,'') or '') for name in _COORDINATES)
def _stamp(value):
try:
parsed=datetime.fromisoformat(value.replace('Z','+00:00'))
if parsed.tzinfo is None or parsed.utcoffset() is None:
_refuse()
return parsed
except (AttributeError,TypeError,ValueError):
_refuse()
class TaskExecutionAuthorization:
"""Opaque process-local prepared state; not a bearer credential or allow."""
__slots__=('_action','_contract','_coordinates','_handoff','_used','_lock')
def __init__(self, issuer=None, *, action=None, contract=None, coordinates=None, handoff=None):
if issuer is not _ISSUED:
_refuse()
object.__setattr__(self,'_action',action)
object.__setattr__(self,'_contract',contract)
object.__setattr__(self,'_coordinates',coordinates)
object.__setattr__(self,'_handoff',json.dumps(handoff,sort_keys=True,separators=(',',':'),allow_nan=False))
object.__setattr__(self,'_used',False)
object.__setattr__(self,'_lock',Lock())
def __setattr__(self,name,value):
raise AttributeError('prepared task handoff is immutable')
def as_owner_handoff(self):
return strict_json(self._handoff)
@dataclass(frozen=True)
class TaskExecutionConsumption:
approval_id: str
request_digest: str
decision_id: str
consumed_at: str
idempotent: bool
def _validate(cfg, action, contract):
if type(contract) is not ExecutionContract or not action.artifacts:
_refuse()
descriptor=contract.as_dict()
artifacts=strict_json(action.artifacts)
request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
# Rotation/removal and installed-helper changes cannot hide behind a cached
# prepared label. Reauthenticate before each strict CAS, without networking.
authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
if authenticated != envelope:
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
validated=validate_decision_envelope(envelope,request,
accepted_policy_packages={str(cfg.authorization_policy_package)},
accepted_policy_versions={str(cfg.authorization_policy_version)},
expected_approval_binding_digest=claim['binding']['pdp_digest'],time_window=window)
validate_approval_claim(claim,approval_id=action.binding.approval_id,
expected_pdp_digest=envelope['binding']['approval_binding_digest'],
require_human_control=True,time_window=window)
require_execution_deadline(request,time_window=window,
bounds=(claim['validity']['expires_at'],envelope['lifetime']['expires_at']))
binding=envelope['binding']
if (envelope['lifetime'].get('kind')!='ttl'
or _stamp(envelope['lifetime'].get('not_before'))>lower):
_refuse()
if (request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
or binding.get('context')!=request['context']
or request['context'].get('execution_contract')!=descriptor
or binding.get('request_digest')!=request_digest(binding)
or binding.get('approval_binding_digest')!=approval_binding_digest(binding)
or action.binding.request_digest!=validated.request_digest
or action.binding.decision_id!=validated.decision_id):
_refuse()
subject=binding['subject']; resource=binding['resource']
if (request['subject']!={'id':'secrets-engine','type':'service'}
or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
or subject.get('tenant') not in (None,'tenant:platform')
or 'principal' in subject.get('attributes',{})
or envelope['subject']!=subject or envelope['resource']!=resource
or descriptor['execution_actor']!='service:secrets-engine'
or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
or resource.get('tenant') not in (None,'tenant:platform')
or any(resource.get('attributes',{}).get(key)!=value for key,value in request['resource']['attributes'].items())
or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
_refuse()
caller=envelope['provenance'].get('caller')
if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
or upper >= _stamp(caller.get('not_after'))):
_refuse()
native=binding_from_check_request(binding)
supplied=claim['binding']
# Recompute the served native act too: Python equality alone conflates
# boolean/integer enrichment and cannot establish canonical correspondence.
if (any(supplied.get(key)!=native[key] for key in _NATIVE_FIELDS)
or supplied.get('digest')!=claim_binding_digest(**native)
or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
'submitted_request_digest':validated.submitted_request_digest,
'decision_not_before':envelope['lifetime'].get('not_before') or '',
'decision_expires_at':envelope['lifetime']['expires_at'],
'approval_expires_at':claim['validity']['expires_at']}
def authorize_task_execution(cfg, entry, *, execution_contract):
if type(execution_contract) is not ExecutionContract:
_refuse()
# No injected transports or policy callbacks: use the actual existing PIP/PDP
# clients and mandatory signature validators in the owner process.
action=authorize_action(cfg,entry,'execute-ccr',execution_contract=execution_contract)
if action is None:
_refuse()
try:
handoff=_validate(cfg,action,execution_contract)
return TaskExecutionAuthorization(_ISSUED,action=action,contract=ExecutionContract.parse(execution_contract.as_dict()),
coordinates=_coordinates(cfg),handoff=handoff)
except (ValueError,KeyError,TypeError,RecursionError) as exc:
raise DecisionError('strict task execution handoff malformed') from exc
def consume_task_execution(cfg, authorization):
if type(authorization) is not TaskExecutionAuthorization:
_refuse()
with authorization._lock:
if authorization._used or _coordinates(cfg)!=authorization._coordinates:
_refuse()
# Reserve once before all work; failures/unknown outcomes never retry this
# prepared object. Platform's durable journal is the cross-process guard.
object.__setattr__(authorization,'_used',True)
try:
handoff=_validate(cfg,authorization._action,authorization._contract)
if handoff!=authorization.as_owner_handoff():
_refuse()
result=consume_approval(base_url=str(cfg.approval_url),
token_provider=lambda:approval_token(cfg,scope='approval:consume'),
binding=authorization._action.binding,opener=credential_urlopen,strict_response=True)
stamp=_stamp(result.consumed_at)
window=read_window(cfg);_,upper=validity_bounds(window=window)
if (stamp>upper or (handoff['decision_not_before'] and stamp<_stamp(handoff['decision_not_before']))
or any(stamp>=_stamp(value) for value in (handoff['decision_expires_at'],handoff['approval_expires_at'],
handoff['execution_contract']['operation_deadline']))):
_refuse()
# Late CAS completion is historical evidence only; refuse native dispatch.
_validate(cfg,authorization._action,authorization._contract)
return TaskExecutionConsumption(result.approval_id,result.request_digest,result.decision_id,result.consumed_at,result.idempotent)
except (ValueError,KeyError,TypeError,RecursionError) as exc:
raise DecisionError('strict task execution consumption malformed') from exc

View file

@ -0,0 +1,202 @@
"""Source-only builder tests; mocks confer no real grant or principal admission."""
import copy
from dataclasses import replace
import json
from datetime import datetime, timedelta, timezone
import pytest
from secrets_engine.approval_consume import consume_approval, _expected_request, authorize_action, resolve_approval_observation
from secrets_engine.authorization import approval_binding_digest, build_action_request, request_digest
from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest
from tests.test_catalog import VALID
from tests.test_consume_binding_join import _Cfg, _token, _opener, APPROVAL_ID
from tests.authorization_stub import AuthorizationStub
H = 'sha256:' + 'a' * 64
def entry():
raw = copy.deepcopy(VALID)
raw['stage'] = 'prod'
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': APPROVAL_ID, 'purpose': 'metadata-test'}
return validate_entry(raw)
def descriptor(e):
baseline = build_action_request(e, 'apply', subject_id='secrets-engine', subject_type='service', purpose='metadata-test')
return {
'contract_version': 'SECRETS-EXEC-20261011-v1', 'operation_id': 'cbfbced0-09d5-48c8-a1e7-a2ea4dc69153',
'operation': 'metadata-apply-ccr', 'execution_actor': 'service:secrets-engine',
'ccr_id': 'CCR-2099-0001', 'ccr_immutable_sha256': H, 'execution_grant_ccr_id': 'CCR-2099-0002',
'grant_immutable_sha256': H, 'catalog_sha256': catalog_target_digest(baseline['context']['catalog_target']),
'source_bundle': {name: {'path': '/owner/'+name+'.py', 'sha256': H}
for name in ['engine_dispatcher', 'platform_mediator', 'typed_ccr', 'policy_auth']},
'backend_origin': 'http://127.0.0.1:18200', 'cluster_uid': 'test-estate-not-live',
'operations': [{'method': 'GET', 'path': 'sys/policies/acl/test-policy', 'payload_sha256': H},
{'method': 'PUT', 'path': 'auth/netkingdom/role/test-role', 'payload_sha256': H}],
'operation_deadline': (datetime.now(timezone.utc)+timedelta(seconds=60)).isoformat(),
'backend_budget': {'ttl_seconds': 120, 'uses': 12}, 'expected_before': H, 'expected_after': H,
}
def request(e, contract):
return build_action_request(e, 'execute-ccr', subject_id='secrets-engine', subject_type='service',
purpose='metadata-test', execution_contract=contract)
def served(pdp):
claim = pdp.claim()
claim['binding']['human_control'] = True
return claim
def test_four_builders_preserve_context_and_binding(tmp_path):
e = entry(); raw = descriptor(e); contract = ExecutionContract.parse(raw)
cfg = _Cfg(_token(tmp_path), authorization_subject_id='secrets-engine', authorization_subject_type='service')
expected = request(e, contract)
assert expected['context']['human_control'] is True
assert _expected_request(cfg,e,'execute-ccr',execution_contract=contract) == expected
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp.bind_request(expected); claim = served(pdp)
observed = resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=contract,opener=_opener(claim))
assert observed.request['context']['execution_contract'] == raw
assert approval_binding_digest(observed.request) == request_digest(expected)
assert request_digest(observed.request) != request_digest(expected)
submitted=[]
def check(req, timeout=None):
value=json.loads(req.data); submitted.append(value)
return _opener(pdp.decision(value))(req)
authorized = authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
assert authorized.binding.request_digest == request_digest(submitted[0])
assert submitted[0] == observed.request
from tests.test_approval_consume import _Response
consume_bodies = []
def consume(req, timeout=None):
value = json.loads(req.data); consume_bodies.append(value)
return _Response(200, {'status':'consumed', 'approval_id':APPROVAL_ID,
'request_digest':value['request_digest'], 'decision_id':value['decision_id'],
'consumed_at':datetime.now(timezone.utc).isoformat(), 'idempotent':False})
confirmed = consume_approval(base_url=cfg.approval_url, token_file=cfg.approval_token_file,
binding=authorized.binding, opener=consume)
assert consume_bodies == [{'request_digest':request_digest(submitted[0]), 'decision_id':authorized.decision_id}]
assert confirmed.request_digest != request_digest(expected)
raw['operations'][0]['path']='sys/policies/acl/changed'
observed.request['context']['execution_contract']['source_bundle']['typed_ccr']['path']='/changed.py'
assert contract.as_dict()['operations'][0]['path']=='sys/policies/acl/test-policy'
assert contract.as_dict()['source_bundle']['typed_ccr']['path']=='/owner/typed_ccr.py'
@pytest.mark.parametrize('change', ['operation_id','ccr','grant','source','body','method','path','budget','deadline','state','cluster'])
def test_changed_binding_cannot_use_original_claim(tmp_path,change):
e=entry(); original=descriptor(e); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,ExecutionContract.parse(original))); claim=served(pdp)
changed=copy.deepcopy(original)
if change=='operation_id': changed[change]='775ab8f4-4731-47c8-bd98-342af22885dd'
elif change=='ccr': changed['ccr_immutable_sha256']='sha256:'+'b'*64
elif change=='grant': changed['grant_immutable_sha256']='sha256:'+'b'*64
elif change=='source': changed['source_bundle']['typed_ccr']['sha256']='sha256:'+'b'*64
elif change=='body': changed['operations'][1]['payload_sha256']='sha256:'+'b'*64
elif change=='method': changed['operations'][0]['method']='PUT'
elif change=='path': changed['operations'][0]['path']='sys/policies/acl/changed'
elif change=='budget': changed['backend_budget']['uses']=11
elif change=='deadline': changed['operation_deadline']=(datetime.now(timezone.utc)+timedelta(seconds=50)).isoformat()
elif change=='state': changed['expected_before']='sha256:'+'b'*64
elif change=='cluster': changed['cluster_uid']='different-estate'
typed=ExecutionContract.parse(changed)
assert approval_binding_digest(request(e,typed)) != pdp._pdp_digest
def check(req,timeout=None): return _opener(pdp.decision(json.loads(req.data)))(req)
with pytest.raises(DecisionError,match='digest'):
authorize_action(cfg,e,'execute-ccr',execution_contract=typed,opener=_opener(claim),pdp_opener=check)
@pytest.mark.parametrize('mutation', ['unknown','extra_nested','missing','wrong_actor','boolean_budget','oversize_budget','sibling_endpoint','path_traversal','source_relative','wrong_hash','bad_deadline','method_object'])
def test_closed_schema_refuses(mutation):
raw=descriptor(entry())
if mutation=='unknown': raw['approved']=True
elif mutation=='extra_nested': raw['backend_budget']['renewable']=True
elif mutation=='missing': del raw['source_bundle']['typed_ccr']
elif mutation=='wrong_actor': raw['execution_actor']='user:admin'
elif mutation=='boolean_budget': raw['backend_budget']['uses']=True
elif mutation=='oversize_budget': raw['backend_budget']['ttl_seconds']=121
elif mutation=='sibling_endpoint': raw['operations'][0]['path']='secret/data/x'
elif mutation=='path_traversal': raw['operations'][0]['path']='auth/a/role/../x'
elif mutation=='source_relative': raw['source_bundle']['typed_ccr']['path']='foo.py'
elif mutation=='wrong_hash': raw['expected_after']='unsafe'
elif mutation=='bad_deadline': raw['operation_deadline']='2026-10-11T10:00:00'
elif mutation=='method_object': raw['operations'][0]['method']={}
with pytest.raises(DecisionError): ExecutionContract.parse(raw)
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
authorize_action(cfg,e,action,execution_contract=contract,opener=fetch,pdp_opener=fetch)
assert calls==[]
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
cfg=_Cfg(_token(tmp_path)); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
e=entry(); cfg.approval_url=''
with pytest.raises(DecisionError,match='human_control'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=ExecutionContract.parse(descriptor(e)))
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
cfg=_Cfg(_token(tmp_path)); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@pytest.mark.parametrize('changed', ['subject', 'purpose', 'catalog', 'tenant', 'stage'])
def test_request_correspondence_changes_refuse_before_authorization(tmp_path, changed):
e = entry(); contract = ExecutionContract.parse(descriptor(e))
cfg = _Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp = AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); claim = served(pdp)
if changed == 'subject': cfg.authorization_subject_id = 'foreign-service'
elif changed == 'purpose': e = replace(e,approval={**e.approval, 'purpose':'different-purpose'})
elif changed == 'catalog': e = replace(e,path='changed/path')
elif changed == 'stage': e = replace(e,stage='test')
calls = []
def check(req,timeout=None):
calls.append('check'); return _opener(pdp.decision(json.loads(req.data)))(req)
with pytest.raises(DecisionError):
if changed == 'tenant':
build_action_request(e,'execute-ccr',subject_id='secrets-engine',subject_type='service',purpose='metadata-test',tenant='tenant:foreign',execution_contract=contract)
else:
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
if changed in {'catalog','stage','tenant'}: assert not calls
def test_raw_dictionary_and_direct_invalid_type_cannot_bypass_validation():
e = entry()
with pytest.raises(DecisionError): request(e,descriptor(e))
with pytest.raises(DecisionError): request(e,ExecutionContract('{}'))
def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)+timedelta(hours=2)).isoformat()
contract=ExecutionContract.parse(raw); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); calls=[]
with pytest.raises(DecisionError,match='exceeds served authority'):
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(served(pdp)),pdp_opener=lambda *a,**k:calls.append('check'))
assert not calls
# Reuse admitted native producer signing, never bypass responder verification.
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools

View file

@ -0,0 +1,35 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
by revision and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
Build in this directory: `go build -trimpath -o /tmp/secrets-engine-pdp-verify .`.
Install that binary through the deployment's reviewed package/image path and set
`SECRETS_ENGINE_PDP_VERIFIER` to its absolute path. Do not compile at request time.
The Python wheel alone is insufficient: deployments must install this helper.
The `testsupport/signer` executable is test-only and must not be installed.
From repository root, run `python3 scripts/check_pdp_producer_source.py` before
building, and optionally add `--upstream /path/to/access-engine` to detect producer
drift. To update: select/review an upstream revision, copy these exact source
files and native replay fixtures, update the manifest, run both upstream signature
checks and downstream consumer tests, then review the complete diff. Never change
canonicalization locally. Producer contract additions fail closed until reviewed.
The helper reads the original JSON envelope, verifies using Go's producer verifier,
and returns only the authenticated typed envelope. The Python boundary rejects
duplicate keys, nonfinite values and invalid UTF-8 before invoking it. Unknown
typed fields refuse. The unsigned producer mode is never a production fallback.
The Check API defaults to mandatory authentication. Its deliberately named
`allow_unsigned_test_fixture` argument is used only by synthetic transport tests;
production `authorize_action` never passes it. The helper runs with an empty
environment, so ambient backend and identity credentials cannot reach it.
For the complete reproducible CLI build and offline fresh-prefix installation,
use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.

View file

@ -0,0 +1,76 @@
// Package sign attaches a detached Ed25519 signature to a decision envelope
// (FLEX-WP-0024). The signed material is json.Marshal of the envelope with
// the signature field omitted.
package sign
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/pkg/api"
)
const AlgorithmEd25519 = "ed25519"
// CanonicalBytes is the exact payload that is signed and verified: the
// envelope as emitted, with signature absent.
func CanonicalBytes(envelope api.DecisionEnvelope) ([]byte, error) {
envelope.Signature = nil
data, err := json.Marshal(envelope)
if err != nil {
return nil, fmt.Errorf("marshal canonical envelope: %w", err)
}
return data, nil
}
func Sign(envelope *api.DecisionEnvelope, private ed25519.PrivateKey, kid string) error {
if envelope == nil {
return fmt.Errorf("envelope is required")
}
if len(private) != ed25519.PrivateKeySize {
return fmt.Errorf("ed25519 private key has length %d", len(private))
}
if kid == "" {
return fmt.Errorf("kid is required")
}
payload, err := CanonicalBytes(*envelope)
if err != nil {
return err
}
sig := ed25519.Sign(private, payload)
envelope.Signature = &api.EnvelopeSignature{
Mode: api.EnvelopeSignatureSigned,
Alg: AlgorithmEd25519,
Kid: kid,
Value: base64.RawURLEncoding.EncodeToString(sig),
}
return nil
}
func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) error {
sig := envelope.Signature
if sig == nil || sig.Mode != api.EnvelopeSignatureSigned {
return fmt.Errorf("envelope is not signed")
}
if sig.Alg != AlgorithmEd25519 {
return fmt.Errorf("unsupported signature alg %q", sig.Alg)
}
pub, ok := keys[sig.Kid]
if !ok {
return fmt.Errorf("unknown signature kid %q", sig.Kid)
}
raw, err := base64.RawURLEncoding.DecodeString(sig.Value)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
payload, err := CanonicalBytes(envelope)
if err != nil {
return err
}
if !ed25519.Verify(pub, payload, raw) {
return fmt.Errorf("signature does not match envelope")
}
return nil
}

View file

@ -0,0 +1,52 @@
// Decision verification uses the unchanged Access Engine producer implementation.
package main
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
type input struct {
Envelope json.RawMessage `json:"envelope"`
Keys map[string]string `json:"keys"`
}
func run() error {
var in input
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
var envelope api.DecisionEnvelope
decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}
for kid, encoded := range in.Keys {
raw, err := base64.RawURLEncoding.Strict().DecodeString(encoded)
if err != nil || len(raw) != ed25519.PublicKeySize || kid == "" {
return fmt.Errorf("invalid admitted key")
}
keys[kid] = ed25519.PublicKey(raw)
}
if err := sign.Verify(envelope, keys); err != nil {
return err
}
// Return the exact typed interpretation authenticated by the producer; Python
// must never act on a second interpretation of unverified response fields.
return json.NewEncoder(os.Stdout).Encode(envelope)
}
func main() {
if run() != nil {
fmt.Fprintln(os.Stderr, "decision signature verification refused")
os.Exit(1)
}
}

View file

@ -0,0 +1,685 @@
package api
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
// ProtectedSystemManifest describes a system that delegates authorization to
// flex-auth.
type ProtectedSystemManifest struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ClientBinding *ClientBinding `json:"client_binding,omitempty" yaml:"client_binding,omitempty"`
ResourceTypes []ResourceType `json:"resource_types,omitempty" yaml:"resource_types,omitempty"`
Actions []ActionDefinition `json:"actions,omitempty" yaml:"actions,omitempty"`
CaringProfiles []string `json:"caring_profiles,omitempty" yaml:"caring_profiles,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// ClientBinding names the key-cape client a protected system's users sign in
// through. It is descriptive only: the readback registry projection lists it,
// and it never decides an allow (FLEX-DEC-2026-019).
type ClientBinding struct {
Issuer string `json:"issuer" yaml:"issuer"`
ClientID string `json:"client_id" yaml:"client_id"`
}
// ResourceType describes a resource namespace entry owned by a protected system.
type ResourceType struct {
Name string `json:"name" yaml:"name"`
ParentTypes []string `json:"parent_types,omitempty" yaml:"parent_types,omitempty"`
ScopeLevel ScopeLevel `json:"scope_level,omitempty" yaml:"scope_level,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// ActionDefinition maps a protected-system action to CARING capabilities.
type ActionDefinition struct {
Name string `json:"name" yaml:"name"`
Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// SubjectManifest declares subjects, groups, teams, and tenants for local
// registry loading.
type SubjectManifest struct {
ID string `json:"id" yaml:"id"`
Subjects []Subject `json:"subjects,omitempty" yaml:"subjects,omitempty"`
Groups []Group `json:"groups,omitempty" yaml:"groups,omitempty"`
Teams []Team `json:"teams,omitempty" yaml:"teams,omitempty"`
Tenants []Tenant `json:"tenants,omitempty" yaml:"tenants,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Subject is a human, service, automation, agent, or other acting identity.
type Subject struct {
ID string `json:"id" yaml:"id"`
Type SubjectType `json:"type" yaml:"type"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
OrganizationRelation OrganizationRelation `json:"organization_relation,omitempty" yaml:"organization_relation,omitempty"`
Roles []CanonicalRole `json:"roles,omitempty" yaml:"roles,omitempty"`
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Claims map[string]any `json:"claims,omitempty" yaml:"claims,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Group is an assignment convenience, not a canonical role.
type Group struct {
ID string `json:"id" yaml:"id"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
Members []string `json:"members,omitempty" yaml:"members,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Team is a group-like ownership unit used by protected systems.
type Team struct {
ID string `json:"id" yaml:"id"`
DisplayName string `json:"display_name,omitempty" yaml:"display_name,omitempty"`
Members []string `json:"members,omitempty" yaml:"members,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
CaringDescriptors []CaringAccessDescriptor `json:"caring_descriptors,omitempty" yaml:"caring_descriptors,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// Tenant is a structural isolation boundary.
type Tenant struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// RelationshipFact records a relation between subjects, groups, teams, tenants,
// and resources.
type RelationshipFact struct {
ID string `json:"id" yaml:"id"`
System string `json:"system,omitempty" yaml:"system,omitempty"`
Subject string `json:"subject" yaml:"subject"`
Relation string `json:"relation" yaml:"relation"`
Object string `json:"object" yaml:"object"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
Caring *CaringAccessDescriptor `json:"caring,omitempty" yaml:"caring,omitempty"`
Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// PolicyPackageMetadata is the frontmatter contract for Rego-in-Markdown
// policy packages.
type PolicyPackageMetadata struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Version string `json:"version" yaml:"version"`
Status string `json:"status,omitempty" yaml:"status,omitempty"`
Package string `json:"package" yaml:"package"`
Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"`
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
Fixtures []string `json:"fixtures,omitempty" yaml:"fixtures,omitempty"`
Caring CaringPolicyMetadata `json:"caring" yaml:"caring"`
Activation map[string]any `json:"activation,omitempty" yaml:"activation,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
// AllowTTL is a Go duration (for example "15m") that bounds every allow
// this package produces. Omit to use DefaultAllowTTL. "none" or "0s"
// means no stated end; the engine denies those allows (§9.7.1).
AllowTTL string `json:"allow_ttl,omitempty" yaml:"allow_ttl,omitempty"`
}
// CaringPolicyMetadata declares the CARING envelope a policy governs.
type CaringPolicyMetadata struct {
Profile string `json:"profile" yaml:"profile"`
Enforce bool `json:"enforce,omitempty" yaml:"enforce,omitempty"`
CanonicalRoles []CanonicalRole `json:"canonical_roles,omitempty" yaml:"canonical_roles,omitempty"`
OrganizationRelations []OrganizationRelation `json:"organization_relations,omitempty" yaml:"organization_relations,omitempty"`
Scopes []CaringScope `json:"scopes,omitempty" yaml:"scopes,omitempty"`
Planes []Plane `json:"planes,omitempty" yaml:"planes,omitempty"`
Capabilities []Capability `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
Conditions []Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"`
Restrictions []Restriction `json:"restrictions,omitempty" yaml:"restrictions,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// PolicyFixture binds a check request to an expected decision.
type PolicyFixture struct {
ID string `json:"id" yaml:"id"`
Request CheckRequest `json:"request" yaml:"request"`
Expect DecisionExpectation `json:"expect" yaml:"expect"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// DecisionExpectation is the compact fixture expectation for policy tests.
type DecisionExpectation struct {
Effect DecisionEffect `json:"effect" yaml:"effect"`
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"`
ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"`
}
// CheckRequest is the stable protected-system-facing decision request.
type CheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
CaringContext *CaringAccessDescriptor `json:"caring_context,omitempty" yaml:"caring_context,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
// Purpose is empty on every enforcement request. The readback pin sets it
// to PurposeReadback, and it is hashed into every request digest, so an
// advisory readback decision can never pass a PEP's §6.4.2 replay test for
// an enforcement request (FLEX-DEC-2026-019). /v1/check and /v1/batch_check
// refuse a request that carries it.
Purpose string `json:"purpose,omitempty" yaml:"purpose,omitempty"`
}
// PurposeReadback marks an advisory application-access readback decision.
const PurposeReadback = "readback"
// BatchCheckRequest evaluates one subject/action against multiple resources.
type BatchCheckRequest struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resources []ResourceRef `json:"resources" yaml:"resources"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
}
// SubjectRef is a normalized subject reference in request and decision shapes.
type SubjectRef struct {
ID string `json:"id" yaml:"id"`
Type SubjectType `json:"type,omitempty" yaml:"type,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// ResourceRef is a normalized resource reference in request and decision shapes.
type ResourceRef struct {
ID string `json:"id" yaml:"id"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
System string `json:"system,omitempty" yaml:"system,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}
// DecisionEffect is the stable decision outcome vocabulary.
type DecisionEffect string
const (
DecisionEffectAllow DecisionEffect = "allow"
DecisionEffectDeny DecisionEffect = "deny"
DecisionEffectRedact DecisionEffect = "redact"
DecisionEffectAuditOnly DecisionEffect = "audit_only"
DecisionEffectNotApplicable DecisionEffect = "not_applicable"
)
// DecisionEffects returns the complete effect vocabulary. Every effect is an
// emission class in cadence.yaml; a test fails if a constant above is missing
// here, so a new effect cannot ship unclassified (FLEX-WP-0031-T03).
func DecisionEffects() []DecisionEffect {
return []DecisionEffect{
DecisionEffectAllow,
DecisionEffectDeny,
DecisionEffectRedact,
DecisionEffectAuditOnly,
DecisionEffectNotApplicable,
}
}
// Restricts reports whether an effect withholds authority. This classification
// does not exempt an effect from required durable emission (GH-DEC-2026-024).
func (e DecisionEffect) Restricts() bool {
switch e {
case DecisionEffectDeny, DecisionEffectRedact, DecisionEffectNotApplicable:
return true
}
return false
}
// DecisionRecordContractV1 is the published decision-record contract identifier.
const DecisionRecordContractV1 = "flex-auth.decision-record.v1"
// DecisionEnvelope is the stable response produced by standalone and delegated
// evaluators. It is flex-auth's published decision-record contract (§17).
type DecisionEnvelope struct {
ID string `json:"id" yaml:"id"`
ContractVersion string `json:"contract_version,omitempty" yaml:"contract_version,omitempty"`
RequestID string `json:"request_id,omitempty" yaml:"request_id,omitempty"`
Effect DecisionEffect `json:"effect" yaml:"effect"`
Reason string `json:"reason,omitempty" yaml:"reason,omitempty"`
MatchedPolicyVersion string `json:"matched_policy_version,omitempty" yaml:"matched_policy_version,omitempty"`
MatchedRule string `json:"matched_rule,omitempty" yaml:"matched_rule,omitempty"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Binding *DecisionBinding `json:"binding,omitempty" yaml:"binding,omitempty"`
Lifetime *DecisionLifetime `json:"lifetime,omitempty" yaml:"lifetime,omitempty"`
Obligations []Obligation `json:"obligations,omitempty" yaml:"obligations,omitempty"`
Diagnostics map[string]any `json:"diagnostics,omitempty" yaml:"diagnostics,omitempty"`
Provenance DecisionProvenance `json:"provenance" yaml:"provenance"`
Signature *EnvelopeSignature `json:"signature,omitempty" yaml:"signature,omitempty"`
Caring *CaringDecisionMetadata `json:"caring,omitempty" yaml:"caring,omitempty"`
}
// EnvelopeSignatureMode is how the responder authenticates the envelope.
type EnvelopeSignatureMode string
const (
EnvelopeSignatureSigned EnvelopeSignatureMode = "signed"
EnvelopeSignatureUnsigned EnvelopeSignatureMode = "unsigned"
)
// EnvelopeSignature is a detached signature over the canonical envelope with
// this field omitted (FLEX-WP-0024). It is not decision material and is not
// part of request_digest.
type EnvelopeSignature struct {
Mode EnvelopeSignatureMode `json:"mode" yaml:"mode"`
Alg string `json:"alg,omitempty" yaml:"alg,omitempty"`
Kid string `json:"kid,omitempty" yaml:"kid,omitempty"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
}
func UnsignedEnvelopeSignature() *EnvelopeSignature {
return &EnvelopeSignature{Mode: EnvelopeSignatureUnsigned}
}
// DecisionLifetimeKind identifies how an allow ends.
type DecisionLifetimeKind string
const (
DecisionLifetimeTTL DecisionLifetimeKind = "ttl"
)
// DefaultAllowTTL is the engine default when a policy package omits allow_ttl.
const DefaultAllowTTL = 15 * time.Minute
// ReasonAllowLifetimeUnstated is the deny reason for an allow with no stated end.
const ReasonAllowLifetimeUnstated = "allow_lifetime_unstated"
// DecisionLifetime bounds an allow (§9.7.1). flex-auth has no session concept,
// so the first honest shape is a policy-package-declared TTL.
type DecisionLifetime struct {
Kind DecisionLifetimeKind `json:"kind" yaml:"kind"`
TTL string `json:"ttl,omitempty" yaml:"ttl,omitempty"`
NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"`
ExpiresAt string `json:"expires_at" yaml:"expires_at"`
}
// DecisionBinding is the exact normalized authorization request evaluated by
// a decision. It lets a consumer verify structured action, target, actor, and
// context fields without parsing reason or diagnostic prose.
type DecisionBinding struct {
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Action string `json:"action" yaml:"action"`
Resource ResourceRef `json:"resource" yaml:"resource"`
Context map[string]any `json:"context,omitempty" yaml:"context,omitempty"`
// Purpose is "readback" on an advisory readback decision and absent on an
// enforcement decision (FLEX-DEC-2026-019).
Purpose string `json:"purpose,omitempty" yaml:"purpose,omitempty"`
RequestDigest string `json:"request_digest" yaml:"request_digest"`
// ApprovalBindingDigest is RequestDigest computed with the approval claim
// removed from context. It exists to break a circularity: an approval's
// pdp_digest is recorded at issue time, but the request that later carries
// the claim inside its hashed context has a different RequestDigest by
// construction, so a claim can never name the request that carries it.
//
// This digest is stable across attaching the claim, so it is the value
// claim.binding.pdp_digest must equal (GH-DEC-2026-008). It is NOT a replay
// identity and must not be used as one: two requests differing only in
// which approval was presented share it, and the decision does not.
ApprovalBindingDigest string `json:"approval_binding_digest,omitempty" yaml:"approval_binding_digest,omitempty"`
// SubmittedRequestDigest is RequestDigest over the request exactly as the
// consumer sent it, before the evaluator overlays registry facts onto the
// subject and resource.
//
// It exists because RequestDigest is not consumer-computable and was
// published as though it were. The evaluator hashes the ENRICHED request —
// subject.tenant, subject.attributes, and resource attributes the registry
// contributed — and the registry is flex-auth's, so a consumer recomputing
// the digest over what it sent gets a different value on every real allow.
// secrets-engine found this on its first live request (FLEX-DEC-2026-012).
//
// This is the digest a PEP compares for §6.4 obligation 2's replay test.
// Nothing is lost by hashing the pre-enrichment form: enrichment is a
// function of the request and the registry snapshot, and
// provenance.registry_snapshot_digest already pins the snapshot, so
// SubmittedRequestDigest together with that digest identifies the evaluated
// request completely.
SubmittedRequestDigest string `json:"submitted_request_digest,omitempty" yaml:"submitted_request_digest,omitempty"`
}
// ApprovalContextKey is the context key carrying an approval-engine
// approval-claim. It is excluded from ApprovalBindingDigest and from nothing
// else.
const ApprovalContextKey = "approval"
// requestDigestMaterial is the exact tuple hashed for §6.4.2 replay. Request
// id, policy version, and caring_context are excluded: id is correlation, the
// version is provenance, and caring_context is an input-claim digest.
type requestDigestMaterial struct {
Tenant string `json:"tenant,omitempty"`
Subject SubjectRef `json:"subject"`
Action string `json:"action"`
Resource ResourceRef `json:"resource"`
Context map[string]any `json:"context,omitempty"`
Purpose string `json:"purpose,omitempty"`
}
// NewDecisionBinding returns a stable structured binding for the exact request
// an evaluator consumed.
//
// Prefer NewDecisionBindingFor, which also records the digest of the request as
// submitted. This form leaves SubmittedRequestDigest empty, which reads as "the
// evaluator did not record one" rather than "the two are equal".
func NewDecisionBinding(request CheckRequest) *DecisionBinding {
contextCopy := make(map[string]any, len(request.Context))
for key, value := range request.Context {
contextCopy[key] = value
}
binding := &DecisionBinding{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: contextCopy,
Purpose: request.Purpose,
RequestDigest: RequestDigest(request),
}
if _, carries := request.Context[ApprovalContextKey]; carries {
binding.ApprovalBindingDigest = ApprovalBindingDigest(request)
}
return binding
}
// NewDecisionBindingFor records the binding of the evaluated request together
// with the digest of the request as submitted.
//
// The two arguments are the same request before and after the evaluator
// overlaid registry facts. Where a request named nothing the registry knows they
// are equal, and the field is emitted anyway: a consumer that only sees it on
// enriched decisions would build a check that passes by absence.
func NewDecisionBindingFor(evaluated, submitted CheckRequest) *DecisionBinding {
binding := NewDecisionBinding(evaluated)
binding.SubmittedRequestDigest = RequestDigest(submitted)
return binding
}
// ApprovalBindingDigest is RequestDigest over the same material with the
// approval claim removed from context.
//
// When a request carries no approval claim the two are identical, which is the
// point: an approval issued against a claim-free Check records that Check's
// request_digest, and the later claim-bearing request reproduces the same value
// here. Comparing it to claim.binding.pdp_digest establishes correspondence by
// identity rather than by translating between two action vocabularies
// (GH-DEC-2026-008).
func ApprovalBindingDigest(request CheckRequest) string {
if _, carries := request.Context[ApprovalContextKey]; !carries {
return RequestDigest(request)
}
stripped := make(map[string]any, len(request.Context))
for key, value := range request.Context {
if key == ApprovalContextKey {
continue
}
stripped[key] = value
}
if len(stripped) == 0 {
stripped = nil
}
return CanonicalDigest(requestDigestMaterial{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: stripped,
Purpose: request.Purpose,
})
}
// RequestDigest is the mechanical §6.4.2 replay test: SHA-256 over canonical
// JSON of tenant, subject, action, resource, context, and purpose. Purpose is
// omitted when empty, so enforcement digests are unchanged by its existence.
func RequestDigest(request CheckRequest) string {
return CanonicalDigest(requestDigestMaterial{
Tenant: request.Tenant,
Subject: request.Subject,
Action: request.Action,
Resource: request.Resource,
Context: request.Context,
Purpose: request.Purpose,
})
}
// CanonicalDigest returns "sha256:" plus the hex SHA-256 of canonical JSON.
// encoding/json sorts map keys, so two equal Go values agree.
func CanonicalDigest(value any) string {
data, err := json.Marshal(value)
if err != nil {
sum := sha256.Sum256(nil)
return "sha256:" + hex.EncodeToString(sum[:])
}
sum := sha256.Sum256(data)
return "sha256:" + hex.EncodeToString(sum[:])
}
// InputClaimDigests hashes the request-time claim classes the evaluator joined.
func InputClaimDigests(request CheckRequest) map[string]string {
digests := make(map[string]string)
if len(request.Context) > 0 {
digests["context"] = CanonicalDigest(request.Context)
}
if request.CaringContext != nil {
digests["caring_context"] = CanonicalDigest(request.CaringContext)
}
if len(digests) == 0 {
return nil
}
return digests
}
// DecisionCompletion carries evaluator-side inputs used to finish an envelope.
type DecisionCompletion struct {
AllowTTL string
Now time.Time
}
// CompleteDecision stamps contract version, input-claim digests, decision time,
// and an explicit allow lifetime. An allow with no stated end becomes a deny.
func CompleteDecision(envelope *DecisionEnvelope, request CheckRequest, completion DecisionCompletion) {
if envelope == nil {
return
}
if envelope.ContractVersion == "" {
envelope.ContractVersion = DecisionRecordContractV1
}
if envelope.Provenance.InputClaimDigests == nil {
envelope.Provenance.InputClaimDigests = InputClaimDigests(request)
}
ApplyAllowLifetime(envelope, completion.AllowTTL, completion.Now)
}
// ParseAllowTTL resolves a package-declared TTL. ok is false when the allow
// would have no stated end. Invalid strings return an error so package
// validation can reject them.
func ParseAllowTTL(declared string) (time.Duration, error) {
trimmed := strings.TrimSpace(declared)
if trimmed == "" {
return DefaultAllowTTL, nil
}
if strings.EqualFold(trimmed, "none") {
return 0, nil
}
ttl, err := time.ParseDuration(trimmed)
if err != nil {
return 0, fmt.Errorf("allow_ttl %q is not a Go duration: %w", declared, err)
}
if ttl <= 0 {
return 0, nil
}
return ttl, nil
}
// ApplyAllowLifetime sets DecisionTime and, for allows, an explicit TTL. A
// missing or zero TTL denies the allow rather than mint a standing grant.
func ApplyAllowLifetime(envelope *DecisionEnvelope, declaredTTL string, now time.Time) {
if envelope == nil {
return
}
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
if envelope.Provenance.DecisionTime == "" {
envelope.Provenance.DecisionTime = now.Format(time.RFC3339)
}
if envelope.Effect != DecisionEffectAllow {
return
}
ttl, err := ParseAllowTTL(declaredTTL)
if err != nil || ttl <= 0 {
if envelope.Diagnostics == nil {
envelope.Diagnostics = map[string]any{}
}
if envelope.Reason != "" {
envelope.Diagnostics["unstated_allow_reason"] = envelope.Reason
}
envelope.Effect = DecisionEffectDeny
envelope.Reason = ReasonAllowLifetimeUnstated
envelope.MatchedRule = ReasonAllowLifetimeUnstated
envelope.Lifetime = nil
return
}
display := strings.TrimSpace(declaredTTL)
if display == "" {
display = "15m"
}
envelope.Lifetime = &DecisionLifetime{
Kind: DecisionLifetimeTTL,
TTL: display,
NotBefore: now.Format(time.RFC3339),
ExpiresAt: now.Add(ttl).Format(time.RFC3339),
}
}
// ActionAuthorizationStatus is the lifecycle state of a durable authorization.
type ActionAuthorizationStatus string
const (
ActionAuthorizationPending ActionAuthorizationStatus = "pending"
ActionAuthorizationApproved ActionAuthorizationStatus = "approved"
ActionAuthorizationDenied ActionAuthorizationStatus = "denied"
ActionAuthorizationSuperseded ActionAuthorizationStatus = "superseded"
ActionAuthorizationExpired ActionAuthorizationStatus = "expired"
ActionAuthorizationRevoked ActionAuthorizationStatus = "revoked"
)
// ActionAuthorization joins a durable approval lifecycle to one exact
// flex-auth request and decision. Storage and approval collection remain the
// responsibility of the organizational decision authority.
type ActionAuthorization struct {
SchemaVersion string `json:"schema_version" yaml:"schema_version"`
ID string `json:"id" yaml:"id"`
Status ActionAuthorizationStatus `json:"status" yaml:"status"`
SupersededBy string `json:"superseded_by,omitempty" yaml:"superseded_by,omitempty"`
Request CheckRequest `json:"request" yaml:"request"`
Validity ActionAuthorizationValidity `json:"validity" yaml:"validity"`
Approvals ActionAuthorizationApprovals `json:"approvals" yaml:"approvals"`
Decision DecisionEnvelope `json:"decision" yaml:"decision"`
Provenance map[string]any `json:"provenance,omitempty" yaml:"provenance,omitempty"`
}
// ActionAuthorizationValidity bounds execution of an approved action.
type ActionAuthorizationValidity struct {
NotBefore string `json:"not_before,omitempty" yaml:"not_before,omitempty"`
ExpiresAt string `json:"expires_at" yaml:"expires_at"`
}
// ActionAuthorizationApprovals declares the approval threshold and evidence.
type ActionAuthorizationApprovals struct {
RequiredCount int `json:"required_count" yaml:"required_count"`
Entries []ActionAuthorizationApprovalEntry `json:"entries" yaml:"entries"`
}
// ActionAuthorizationApprovalEntry is one authenticated approver's evidence.
type ActionAuthorizationApprovalEntry struct {
SubjectID string `json:"subject_id" yaml:"subject_id"`
ApprovedAt string `json:"approved_at" yaml:"approved_at"`
Assurance string `json:"assurance,omitempty" yaml:"assurance,omitempty"`
EvidenceRef string `json:"evidence_ref,omitempty" yaml:"evidence_ref,omitempty"`
}
// Obligation describes a follow-up behavior required by a decision.
type Obligation struct {
Type string `json:"type" yaml:"type"`
Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"`
}
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
Evaluator string `json:"evaluator" yaml:"evaluator"`
Mode string `json:"mode" yaml:"mode"`
PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
}
// CallerProvenance records how the request was authenticated to flex-auth.
// It is not decision material: the same request from a different authenticated
// caller must decide identically. Mode is required. Under disabled the object
// is {"mode":"disabled"} with no principal (FLEX-DEC-2026-009).
type CallerProvenance struct {
Mode string `json:"mode" yaml:"mode"`
Principal string `json:"principal,omitempty" yaml:"principal,omitempty"`
Audience string `json:"audience,omitempty" yaml:"audience,omitempty"`
NotAfter string `json:"not_after,omitempty" yaml:"not_after,omitempty"`
}
// CaringDecisionMetadata carries CARING descriptor and conformance details in
// a decision envelope.
type CaringDecisionMetadata struct {
Profile string `json:"profile" yaml:"profile"`
Descriptor *CaringAccessDescriptor `json:"descriptor,omitempty" yaml:"descriptor,omitempty"`
RestrictionsEvaluated []Restriction `json:"restrictions_evaluated,omitempty" yaml:"restrictions_evaluated,omitempty"`
ExposureModes []ExposureMode `json:"exposure_modes,omitempty" yaml:"exposure_modes,omitempty"`
DerivedCapabilities []CaringDerivedCapability `json:"derived_capabilities,omitempty" yaml:"derived_capabilities,omitempty"`
ConformanceFindings []CaringConformanceFinding `json:"conformance_findings,omitempty" yaml:"conformance_findings,omitempty"`
ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"`
}
// AuditEvent is the local log shape for decisions and exposure events.
type AuditEvent struct {
ID string `json:"id" yaml:"id"`
Type string `json:"type" yaml:"type"`
DecisionID string `json:"decision_id,omitempty" yaml:"decision_id,omitempty"`
Subject SubjectRef `json:"subject" yaml:"subject"`
Resource ResourceRef `json:"resource,omitempty" yaml:"resource,omitempty"`
Action string `json:"action,omitempty" yaml:"action,omitempty"`
Effect DecisionEffect `json:"effect,omitempty" yaml:"effect,omitempty"`
Timestamp string `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
ExposureEvent *CaringExposureEvent `json:"exposure_event,omitempty" yaml:"exposure_event,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}

View file

@ -0,0 +1,15 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
"revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
"files": {
"pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
"internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
}
}

View file

@ -0,0 +1,28 @@
// Synthetic test-only signer with the producer's published nonproduction seed.
package main
import (
"bytes"
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"os"
)
func main() {
seed := byte(0x42)
kid := "testdata-ed25519"
if len(os.Args) > 1 {
seed = 0x43
kid = "next-admitted"
}
var env api.DecisionEnvelope
if json.NewDecoder(os.Stdin).Decode(&env) != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {
os.Exit(1)
}
json.NewEncoder(os.Stdout).Encode(env)
}

View file

@ -0,0 +1,126 @@
---
id: SECRETS-WP-0013
type: workplan
title: "Design and prove task-scoped protected OpenBao execution"
domain: infotech
repo: secrets-engine
status: proposed
flavor: planning
owner: codex
topic_slug: netkingdom
created: "2026-10-11"
updated: "2026-10-16"
depends_on: []
state_hub_workstream_id: "5e38aecc-85ca-5f55-94c6-9fbb08604819"
---
Execution-contract direction remains proposed; the narrow local readiness guard
is now reviewed, published and verified in the owner checkout. No native
execution authority or general entitlement adoption is inferred.
Existing SECRETS-WP-0008-T06 remains open for actual adoption. CCR38 is an
unchanged identity-only login and is not a protected execution entitlement.
## Define the exact mediated execution contract
```task
id: SECRETS-WP-0013-T01
status: wait
needs_human: false
blocked_on: message-from:railiance-platform
blocking_reason: "Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred."
priority: high
state_hub_task_id: "b2f0d462-a542-518b-a26b-b7ec07b3c7c1"
```
Design SECRETS-EXEC-20261011-v1 in docs/task-scoped-openbao-execution-contract.md
binds action, target, payload/source digests, initiating identity, exact PDP/CAS,
owner-held ephemeral two-path token, bounded TTL/uses, durable admission and
cleanup. Independent Platform/Secrets Engine/identity/approval/PDP owner returns
are required. No foreign review is written by this draft.
2026-10-11 source-only preparation: typed execution_contract is staged through
all four request builders. The new descriptor is restricted to execute-ccr,
requires prod/platform/human control and binds the catalog fingerprint before
claim. Existing apply/other actions cannot borrow this descriptor. 99 focused
source checks verify propagation, canonical approval/full consume joins,
immutable copies and changed-binding refusals. Detailed schema/limitations are
in the execution-contract document. No source owner assent, initiating-principal
admission, new policy package, ledger/mediator/backend or native acceptance is
claimed. Independent source review remains required. Task statuses are unchanged.
2026-10-11: D3 response-wrapped primitive now verifies native creation path and bounded TTL/accessor, rejects overwrite/symlink/shared-parent paths, and reserves/fsyncs the private recipient before mint. Backend/write uncertainty retains a non-replayable reservation. Disposable OpenBao proves one successful unwrap and native consumed-token HTTP400 denial, not a production lane or entitlement. Separate delivery contracts cannot reuse metadata execute-ccr authority. Source design is ready for independent owner review; RPF-IN-0012 requests the Platform return. Identity/approval/PDP returns and the complete native mediated path remain open; no founder decision or foreign owner assent is fabricated.
## Prevent consumption without execution readiness
```task
id: SECRETS-WP-0013-T02
status: done
priority: high
state_hub_task_id: "4ad61917-506d-5c12-a715-3d336e6b95e0"
```
Completed the narrow local implementation and source-verification obligation.
Secrets Engine source commit 97dfa5ea8fbfe998094bf03a3581ee0c5313ac30 was published
through the root's C17 source-publication record. Independent Platform source
acknowledgment is published in 46f5c956b52524d9d8177002bb60020c7e558039 and
`railiance-platform/docs/reviews/2026-10-11-secrets-execution-readiness-v2-platform-review.json`.
The root verified all 36 focused guard/auth/consume/privileged-evidence checks in
the actual owner checkout; Platform's independent source review passed 58 checks.
Current published CLI/auth/test hashes match those accepted exact sources.
Configured auto and explicit service-jwt refuse the ten guarded backend handlers
before claim/PDP/CAS/login/backend work because no task-execution backend exists.
Named bootstrap/env paths retain their existing approval/stance gates; login-self
probes remain available. The unchanged ambient route/readiness surface is outside
this guard's scope and is not service-JWT adoption. Task completion covers only
this local fail-closed guard, its source review/publication and source verification.
T01 remains progress, T03 remains wait with qualified dependencies, WP13 remains
proposed, and SECRETS-WP-0008-T06 remains open for actual protected execution
adoption/native effect, denial/no-fallback and cleanup evidence. No task-scoped
backend entitlement, provider default flip, founder native go or native acceptance
is claimed.
## Implement and prove one native metadata action
```task
id: SECRETS-WP-0013-T03
status: wait
depends_on: [SECRETS-WP-0013-T01, RPF-WP-0035-T12]
wait_reason: "Wait for independent execution-contract acceptance and Platform task-scoped entitlement design/materialization, then exact founder native go."
priority: high
state_hub_task_id: "5f8f7c41-5921-543f-a7c6-0b1350ea6d82"
```
After T01 independent owner acceptance, implement the narrow execute-ccr surface,
Platform task-grant mediator, bound policy package and admission ledger. Select
an actual unexecuted approved metadata CCR, exact founder go and attended owner
custody. Prove real protected effect, sibling/body/refusal/replay counterexamples
and confirmed revocation/cleanup. No applied CCR38 replay or broad default flip.
Native metadata proof does not certify provision/exec delivery or all T06 scope.
2026-10-11 source revision v2: qualified T03 dependencies point to the owning design and Platform execution-entitlement task. WP13 owns the missing task-scoped execution seam; WP8-T06 retains production adoption and its full workflow proof. Source publication and fixture checks do not imply native acceptance. Original v1 stage remains unchanged.
2026-10-11 completed bounded source preparation within T01: repaired the actual
Python/Go canonical request digest mismatch for valid Unicode/HTML purpose and
nested strings, optional maps and decoded numeric formatting. Full request and
approval-free request bindings retain the opaque sha256 prefix; catalogue,
claim-binding and body fingerprints remain distinct and unchanged. Real Access
Go-oracle cases and unchanged canonical/replay/approval/descriptor fixtures pass.
This is source preparation awaiting independent technical review/publication,
not a formal owner assent, policy deployment or native execution milestone. T01
remains progress; T03 and SECRETS-WP-0008-T06 remain open. Detailed limitations
and retained failed test observations are in the source preparation receipt.
2026-10-16 bounded source handoff preparation: `task_execution_handoff.py` uses
the actual serving approval/mandatory signed Check path and retained detached
artifacts to check exact descriptor/request/native approval/caller/lifetime joins,
then performs one strict exact-response CAS only after Platform journal admission.
It accepts no inbound boolean/mapping/authorizer callback and grants no native
execution. Platform independently confirms durable consumption with the actual
Approval provider client before native dispatch. Focused local HTTP/native Go
producer-signature source checks cover altered joins/caller/grant, missing fields,
coercion/idempotence, expiry, signer removal and replay refusal. No service binding
propagation, policy gate flip, identity mapping admission or production trust/key
change was made. Independent source integration/review and actual whole-provider
proof remain required. T01/T03 and WP8-T06 retain their open native obligations.

View file

@ -0,0 +1,116 @@
---
id: SECRETS-WP-0014
type: workplan
title: "Constrain PDP transport and authenticate decision responses"
domain: infotech
repo: secrets-engine
status: active
owner: codex
topic_slug: secrets-engine
created: "2026-10-12"
updated: "2026-10-16"
flavor: implementation
depends_on: []
state_hub_workstream_id: "7252469d-d7a5-5c18-8911-b511b8c6a736"
---
Native owner work for CUST-WP-0080 G13. Reuses the accepted Check contract and
FLEX-WP-0024 producer signature surface; does not create another PDP or grant
any catalog-lane execution authority. SECRETS-WP-0013 still owns task-scoped
protected execution. Transport review is complete; key admission and native
rollout remain separate gates below.
## Prevent the Check credential from leaving the admitted local transport
```task
id: SECRETS-WP-0014-T01
status: done
priority: high
state_hub_task_id: "8baba9a3-3513-5f02-bafe-ed9dce743173"
```
Reproduced redirects (301/302/303), environment proxy forwarding, malformed URL
handling, oversized-body truncation and invalid UTF-8 against the prior source.
The bounded fix disables redirects and ambient proxies/global openers, requires
a literal loopback bare origin before credential access, caps the complete
response body and rejects invalid UTF-8 as a DecisionError. The approval consumer
no longer overrides this safe default with urlopen. Synthetic local-server tests
cover both entry points, all five redirect status codes and proxy forwarding.
No real credentials or protected backend were touched. Source verification is
recorded in the Custodian evidence directory for 2026-10-12, pdp-transport.json.
This closes the source transport defects only; T02/T03 remain mandatory.
## Implement independently anchored responder authentication
```task
id: SECRETS-WP-0014-T02
status: progress
priority: high
state_hub_task_id: "877782e5-045b-5186-8b95-82e25db21a5a"
```
Review producer canonical signature bytes and the native pin's unsigned/signed
mode. Establish independently admitted verification keys and exact issuer/pin
binding with AccessEngine and Platform; an unauthenticated keys endpoint on the
same responder is not a trust anchor. Reuse the native producer fixtures rather
than inventing a Python canonicalization. Require valid signed envelopes on the
production consumer path; prove forged allow, altered payload, unknown key,
wrong issuer/pin, unsigned/downgrade, expired and rotation overlap/refusal cases.
Track any temporary exception using all required SEC-DSM-EXC fields and actual
approval; neither source support nor a loopback URL is that approval.
2026-10-16 source preparation: production `authorize_action` now requires an
independently admitted local trust file and installed verifier before approval/PDP
credential reads. The Go helper vendors exact producer API/signature sources,
checks source hashes, authenticates the original response and returns only its
verified typed interpretation. Signed evaluator/mode and exact package/version/
artifact digest are bound to admitted public keys. Unknown/unsigned/altered
responses refuse; existing request/approval/lifetime/CAS checks remain required.
Native replay, real producer signing and consumer-boundary tests cover downgrade,
issuer/pin, request joins, and distinct-key overlap/removal. Build/install and
source update procedures are documented in `verifier/README.md`.
This is an isolated source implementation awaiting integration/independent review.
No production verification keys, native pin signed-mode change, trust admission,
owner assent or rollout has occurred. T02 stays progress because independent
key admission remains part of its scope; T03 stays wait. WP13 execution and
identity-only CCR38 are unchanged. Custodian source/verification handoff:
`docs/changes/netkingdom-target-2026-10-16/decision-auth/` in the Custodian repo.
2026-10-16 packaging preparation: `scripts/build_release_bundle.py` and its
separate offline installer deliver the existing wheel plus the reviewed producer
verifier together into a new owner-controlled prefix. Source/hash drift checks,
artifact/toolchain receipts and a narrow fixed-helper launcher remove the silent
wheel-only deployment gap. Disposable build/install tests prove deterministic
artifacts, signed HTTP consumption and missing/tampered artifact refusal; they do
not admit packages, keys or a production prefix. T02 remains progress and T03
wait; no native installation, trust change or protected entitlement is claimed.
## Verify the admitted native consumer and rotation path
```task
id: SECRETS-WP-0014-T03
status: wait
priority: high
depends_on: [SECRETS-WP-0014-T02]
blocking_reason: "Requires reviewed verification-key admission, source enforcement and exact native rollout receipt; no production trust change is proposed yet."
state_hub_task_id: "85aa4c78-b66d-5201-8e31-0ed50e7eaeec"
```
Under an exact reviewed rollout, prove the real pin and consumer accept the
admitted signer, reject forgery/downgrade, rotate without accepting removed keys,
and preserve request/approval binding, lifetime and single-use consumption.
Retain rollback and failure evidence. Update CUST-WP-0080 G13 only with these
native receipts; do not equate passing transport tests with closure.
2026-10-16 native preparation (no rollout): `docs/proposals/native-cli-admission/`
now scopes the existing CLI/lifecycle signer stage, exact delegated PEP denominator,
independent key/policy admission, owner-controlled bundle/config placement and
rotation/rollback refusal. Canonical ff1a13d (packaging plus strict handoff) was
rebuilt in an isolated clean checkout; artifact receipts are in Custodian
`decision-native-preparation/`. Production kid/public fingerprint/custody receipt,
producer candidate OCI and actual runtime/config placement remain missing. The
empty-key trust template deliberately refuses. No install, key read, caller mint,
protected effect, policy switch or trust admission occurred. T02 remains progress,
T03 wait; task source/live obligations remain separately in SECRETS-WP-0013.

View file

@ -0,0 +1,3 @@
........................................................................ [ 69%]
............................... [100%]
103 passed in 14.94s

View file

@ -0,0 +1,642 @@
diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py
index 1cf4140..e4865bb 100644
--- a/src/secrets_engine/approval_consume.py
+++ b/src/secrets_engine/approval_consume.py
@@ -382,7 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
- artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
+ artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope,
+ "authenticated_envelope_json":getattr(envelope,"authenticated_json","")},
sort_keys=True,separators=(",",":"),allow_nan=False),
)
diff --git a/src/secrets_engine/decision_signature.py b/src/secrets_engine/decision_signature.py
index 24fa8d3..d9bd0b9 100644
--- a/src/secrets_engine/decision_signature.py
+++ b/src/secrets_engine/decision_signature.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import base64
import json
+import math
from pathlib import Path
import stat
import subprocess
@@ -24,8 +25,15 @@ def _unique_object(pairs):
return result
+def _finite_float(value):
+ parsed = float(value)
+ if not math.isfinite(parsed):
+ raise ValueError("nonfinite JSON number")
+ return parsed
+
+
def strict_json(data):
- return json.loads(data, object_pairs_hook=_unique_object,
+ return json.loads(data, object_pairs_hook=_unique_object, parse_float=_finite_float,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
@@ -66,10 +74,18 @@ def load_trust(path: Path | None) -> dict[str, Any]:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
+class AuthenticatedEnvelope(dict):
+ """Verified typed values with exact producer JSON retained for reauthentication."""
+ def __init__(self, values, authenticated_json):
+ super().__init__(values)
+ self.authenticated_json = authenticated_json
+
+
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
+ strict_json(body.decode("utf-8"))
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
@@ -78,6 +94,6 @@ def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) ->
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
- return authenticated
- except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
+ return AuthenticatedEnvelope(authenticated, result.stdout.decode("utf-8"))
+ except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError, UnicodeDecodeError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
index 9772861..eea6a85 100644
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -10,6 +10,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
+import re
from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
@@ -85,10 +86,10 @@ def _validate(cfg, action, contract):
request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
# Rotation/removal and installed-helper changes cannot hide behind a cached
# prepared label. Reauthenticate before each strict CAS, without networking.
- authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
+ authenticated=authenticate_response(artifacts["authenticated_envelope_json"].encode("utf-8"),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
- if authenticated != envelope:
+ if json.dumps(authenticated,sort_keys=True,allow_nan=False) != json.dumps(envelope,sort_keys=True,allow_nan=False):
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
@@ -140,6 +141,22 @@ def _validate(cfg, action, contract):
or supplied.get('digest')!=claim_binding_digest(**native)
or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
+ observation=envelope['provenance'].get('approval_observation')
+ if observation is not None:
+ # Signed producer source evidence supplements the original carried claim;
+ # it cannot replace original context, extend freshness, or authorize CAS.
+ if (not isinstance(observation,dict)
+ or any(not isinstance(observation.get(k),str) or not observation[k]
+ for k in ('source_id','source_revision'))
+ or any(not isinstance(observation.get(k),str)
+ or re.fullmatch(r'sha256:[0-9a-f]{64}',observation[k]) is None
+ for k in ('configuration_digest','claim_digest','native_binding_digest'))
+ or observation['native_binding_digest']!=supplied['digest']
+ or _stamp(observation.get('observed_at'))>_stamp(observation.get('validated_at'))
+ or _stamp(observation.get('validated_at'))>upper
+ or upper>=_stamp(observation.get('not_after'))
+ or _stamp(envelope['lifetime']['expires_at'])>_stamp(observation.get('not_after'))):
+ _refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
diff --git a/tests/fixtures/pdp-signature/approval-native-vectors.json b/tests/fixtures/pdp-signature/approval-native-vectors.json
new file mode 100644
index 0000000..46adb26
--- /dev/null
+++ b/tests/fixtures/pdp-signature/approval-native-vectors.json
@@ -0,0 +1,210 @@
+[
+ {
+ "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"
+ }
+]
diff --git a/tests/test_trusted_successor.py b/tests/test_trusted_successor.py
new file mode 100644
index 0000000..3c47a1d
--- /dev/null
+++ b/tests/test_trusted_successor.py
@@ -0,0 +1,80 @@
+"""Owner signature interoperability, independent synthetic admission only."""
+import copy
+import json
+from pathlib import Path
+import subprocess
+
+import pytest
+from secrets_engine.decision_signature import authenticate_response,load_trust
+from secrets_engine.errors import DecisionError
+from tests.test_decision_signature import FIXTURE,trust_file,verifier
+from tests.signature_support import signature_tools
+from tests.test_task_execution_handoff import lane,prepared,consume_task_execution
+
+VECTORS=json.loads((Path(__file__).parent/'fixtures/pdp-signature/approval-native-vectors.json').read_text())
+
+@pytest.mark.parametrize('vector',VECTORS)
+def test_fourteen_owner_native_types_retain_signed_bytes(vector,trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env.pop('signature',None)
+ env['diagnostics']={'native_vector':'REPLACE_RAW_TARGET'}
+ raw=json.dumps(env).replace('"REPLACE_RAW_TARGET"',vector['input_target_json']).encode()
+ signed=subprocess.run([str(signature_tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ result=authenticate_response(signed,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ again=authenticate_response(result.authenticated_json.encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ assert again['diagnostics']['native_vector']==json.loads(vector['input_target_json'])
+ tampered=signed.replace(b'"action":"rotate"',b'"action":"destroy"')
+ assert tampered!=signed
+ with pytest.raises(DecisionError):authenticate_response(tampered,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_unknown_provenance_refuses(trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env['provenance']['unknown_source_authority']=True
+ with pytest.raises(DecisionError):
+ authenticate_response(json.dumps(env).encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_exact_original_numeric_lexeme_survives_strict_cas(lane,monkeypatch,signature_tools):
+ import tests.test_task_execution_handoff as fixtures
+ cfg,e,contract,state=lane
+ def raw_sign(env,tools):
+ env['diagnostics']={'numeric':'LEXEME'}
+ raw=json.dumps(env).replace('"LEXEME"','1e-7').encode()
+ return subprocess.run([str(tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ monkeypatch.setattr(fixtures,'sign_envelope',raw_sign)
+ authorization=prepared(lane)
+ assert '1e-7' in authorization._action.artifacts
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+def source_evidence(envelope,claim):
+ from datetime import datetime,timezone
+ return {'source_id':'synthetic-source','source_revision':'synthetic-revision',
+ 'configuration_digest':'sha256:'+'2'*64,'claim_digest':'sha256:'+'3'*64,
+ 'native_binding_digest':claim['binding']['digest'],
+ 'observed_at':datetime.now(timezone.utc).isoformat(),
+ 'validated_at':datetime.now(timezone.utc).isoformat(),
+ 'not_after':envelope['lifetime']['expires_at']}
+
+
+def test_signed_source_evidence_keeps_original_context_and_cas(lane):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda env:env['provenance'].__setitem__('approval_observation',source_evidence(env,state['claim']))
+ authorization=prepared(lane)
+ snapshot=json.loads(authorization._action.artifacts)
+ assert snapshot['envelope']['binding']['context']==snapshot['request']['context']
+ assert snapshot['request']['context']['approval']==state['claim']
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+@pytest.mark.parametrize('field,value',[
+ ('source_id',''),('source_revision',''),('configuration_digest','wrong'),('claim_digest','wrong'),
+ ('native_binding_digest','sha256:'+'0'*64),('observed_at','2099-01-01T00:00:00Z'),
+ ('validated_at','2099-01-01T00:00:00Z'),('not_after','2000-01-01T00:00:00Z')])
+def test_signed_invalid_source_evidence_refuses_before_cas(lane,field,value):
+ cfg,e,contract,state=lane
+ def change(env):
+ evidence=source_evidence(env,state['claim']);evidence[field]=value
+ env['provenance']['approval_observation']=evidence
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
diff --git a/verifier/README.md b/verifier/README.md
index a63b2a0..6a1b56b 100644
--- a/verifier/README.md
+++ b/verifier/README.md
@@ -1,7 +1,7 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
-by revision and SHA-256 in `producer-source.json`. License attribution and the
+by source revision/proposal and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
@@ -33,3 +33,15 @@ For the complete reproducible CLI build and offline fresh-prefix installation,
use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.
+
+The trusted-service successor snapshot is the frozen Access B2/B3 R1 proposal,
+identified by its patch SHA-256 in `producer-source.json`; this is source review,
+not a claim of published producer revision or runtime/key admission. The wrapper
+uses producer-owned `sign.DecodeEnvelope` to preserve JSON numeric lexemes in
+signed maps. It retains exact authenticated producer JSON through strict
+preparation and CAS reauthentication; Python does not recreate signed bytes.
+Approval's fourteen native binding vectors are signature interoperability cases,
+not an expansion of existing request numeric admission. Signed
+`approval_observation` supplements the original immutable carried claim;
+strict handoff checks its native digest and observation/deadline bounds. It never
+replaces original context or freshness.
diff --git a/verifier/internal/sign/sign.go b/verifier/internal/sign/sign.go
index fc607f7..011f8f7 100644
--- a/verifier/internal/sign/sign.go
+++ b/verifier/internal/sign/sign.go
@@ -4,10 +4,12 @@
package sign
import (
+ "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
+ "io"
"github.com/netkingdom/access-engine/pkg/api"
)
@@ -74,3 +76,20 @@ func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) er
}
return nil
}
+
+// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs.
+// Legacy producer envelopes remain byte-compatible. It does not invent another
+// signature canonicalization: CanonicalBytes still calls typed json.Marshal.
+func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) {
+ var envelope api.DecisionEnvelope
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.UseNumber()
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&envelope); err != nil {
+ return envelope, err
+ }
+ if _, err := decoder.Token(); err != io.EOF {
+ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data")
+ }
+ return envelope, nil
+}
diff --git a/verifier/main.go b/verifier/main.go
index 1f3f9de..7449c64 100644
--- a/verifier/main.go
+++ b/verifier/main.go
@@ -2,13 +2,11 @@
package main
import (
- "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
@@ -23,10 +21,8 @@ func run() error {
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
- var envelope api.DecisionEnvelope
- decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
- decoder.DisallowUnknownFields()
- if err := decoder.Decode(&envelope); err != nil {
+ envelope, err := sign.DecodeEnvelope(in.Envelope)
+ if err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}
diff --git a/verifier/pkg/api/canonical.go b/verifier/pkg/api/canonical.go
index 3baa784..ae11a2d 100644
--- a/verifier/pkg/api/canonical.go
+++ b/verifier/pkg/api/canonical.go
@@ -635,16 +635,30 @@ type Obligation struct {
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
- Evaluator string `json:"evaluator" yaml:"evaluator"`
- Mode string `json:"mode" yaml:"mode"`
- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+ Evaluator string `json:"evaluator" yaml:"evaluator"`
+ Mode string `json:"mode" yaml:"mode"`
+ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
+ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
+ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
+ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
+ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
+ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
+ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
+ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"`
+ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+}
+
+// ApprovalObservationProvenance binds fresh server-owned fact evidence without
+// rewriting the original carried claim/context or either PDP request digest.
+type ApprovalObservationProvenance struct {
+ SourceID string `json:"source_id"`
+ SourceRevision string `json:"source_revision"`
+ ConfigurationDigest string `json:"configuration_digest"`
+ ClaimDigest string `json:"claim_digest"`
+ NativeBindingDigest string `json:"native_binding_digest"`
+ ObservedAt string `json:"observed_at"`
+ NotAfter string `json:"not_after"`
+ ValidatedAt string `json:"validated_at"`
}
// CallerProvenance records how the request was authenticated to flex-auth.
diff --git a/verifier/producer-source.json b/verifier/producer-source.json
index b6b0cba..99e651d 100644
--- a/verifier/producer-source.json
+++ b/verifier/producer-source.json
@@ -1,15 +1,19 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
- "revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
+ "revision": "B2/B3 R1 source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
- "pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
+ "pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
- "internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
+ "internal/sign/sign.go": "0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
+ },
+ "source_proposal": {
+ "patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "status": "source proposal; no runtime or key admission"
}
}
diff --git a/verifier/testsupport/signer/main.go b/verifier/testsupport/signer/main.go
index 2a7fecd..994ea82 100644
--- a/verifier/testsupport/signer/main.go
+++ b/verifier/testsupport/signer/main.go
@@ -6,7 +6,7 @@ import (
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
+ "io"
"os"
)
@@ -17,8 +17,12 @@ func main() {
seed = 0x43
kid = "next-admitted"
}
- var env api.DecisionEnvelope
- if json.NewDecoder(os.Stdin).Decode(&env) != nil {
+ raw, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ os.Exit(1)
+ }
+ env, err := sign.DecodeEnvelope(raw)
+ if err != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 28102cf..8d42c05 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -114,3 +114,18 @@ producer candidate OCI and actual runtime/config placement remain missing. The
empty-key trust template deliberately refuses. No install, key read, caller mint,
protected effect, policy switch or trust admission occurred. T02 remains progress,
T03 wait; task source/live obligations remain separately in SECRETS-WP-0013.
+
+2026-10-16 trusted successor source preparation: refreshed exact API/sign source
+from frozen Access B2/B3 R1 proposal (patch SHA-256
+6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1).
+Producer DecodeEnvelope preserves signed numeric lexemes; strict CAS retains and
+reauthenticates original producer JSON. Existing request/claim/native/caller joins
+remain mandatory; signed source observation additionally binds the native digest
+and caps decision lifetime. Fourteen Approval owner vectors, legacy signatures,
+tamper/unknown provenance and source deadline refusals are locally checked.
+Actual ApprovalApp-to-Access v2 signed response verifies with the consumer helper;
+actual source revocation refuses. Actual builder-to-v2 strict positive execution
+is separately blocked on missing owner-reviewed service_actor_binding mapping;
+no caller context injection or equality relaxation hides that gap.
+Artifacts: Custodian `decision-trusted-successor/`. T02 remains progress, T03
+wait; no package/key/policy/native admission or runtime installation occurred.

View file

@ -0,0 +1,749 @@
diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py
index 1cf4140..e4865bb 100644
--- a/src/secrets_engine/approval_consume.py
+++ b/src/secrets_engine/approval_consume.py
@@ -382,7 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
- artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
+ artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope,
+ "authenticated_envelope_json":getattr(envelope,"authenticated_json","")},
sort_keys=True,separators=(",",":"),allow_nan=False),
)
diff --git a/src/secrets_engine/decision_signature.py b/src/secrets_engine/decision_signature.py
index 24fa8d3..d9bd0b9 100644
--- a/src/secrets_engine/decision_signature.py
+++ b/src/secrets_engine/decision_signature.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import base64
import json
+import math
from pathlib import Path
import stat
import subprocess
@@ -24,8 +25,15 @@ def _unique_object(pairs):
return result
+def _finite_float(value):
+ parsed = float(value)
+ if not math.isfinite(parsed):
+ raise ValueError("nonfinite JSON number")
+ return parsed
+
+
def strict_json(data):
- return json.loads(data, object_pairs_hook=_unique_object,
+ return json.loads(data, object_pairs_hook=_unique_object, parse_float=_finite_float,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
@@ -66,10 +74,18 @@ def load_trust(path: Path | None) -> dict[str, Any]:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
+class AuthenticatedEnvelope(dict):
+ """Verified typed values with exact producer JSON retained for reauthentication."""
+ def __init__(self, values, authenticated_json):
+ super().__init__(values)
+ self.authenticated_json = authenticated_json
+
+
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
+ strict_json(body.decode("utf-8"))
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
@@ -78,6 +94,6 @@ def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) ->
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
- return authenticated
- except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
+ return AuthenticatedEnvelope(authenticated, result.stdout.decode("utf-8"))
+ except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError, UnicodeDecodeError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc
diff --git a/src/secrets_engine/execution_contract.py b/src/secrets_engine/execution_contract.py
index 5dc1cae..f9b2538 100644
--- a/src/secrets_engine/execution_contract.py
+++ b/src/secrets_engine/execution_contract.py
@@ -153,6 +153,18 @@ def attach_execution_contract(request: dict, contract: ExecutionContract | None)
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
+ if request['subject'] != {'id':'secrets-engine','type':'service'}:
+ raise DecisionError('execute-ccr requires secrets-engine service subject')
+ # Match the producer's fixed tenant normalization before approval issue.
+ request['subject']['tenant'] = 'tenant:platform'
+ request['resource']['tenant'] = 'tenant:platform'
+ # Public declaration from Access ServicePilotInputs.DeclaredServiceBinding;
+ # bound before approval issuance, never an authentication assertion.
+ request['context']['service_actor_binding'] = {
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform',
+ }
request['context']['execution_contract'] = value
request['context']['human_control'] = True
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
index 9772861..5af36be 100644
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -10,6 +10,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
+import re
from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
@@ -85,10 +86,10 @@ def _validate(cfg, action, contract):
request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
# Rotation/removal and installed-helper changes cannot hide behind a cached
# prepared label. Reauthenticate before each strict CAS, without networking.
- authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
+ authenticated=authenticate_response(artifacts["authenticated_envelope_json"].encode("utf-8"),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
- if authenticated != envelope:
+ if json.dumps(authenticated,sort_keys=True,allow_nan=False) != json.dumps(envelope,sort_keys=True,allow_nan=False):
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
@@ -115,7 +116,7 @@ def _validate(cfg, action, contract):
or action.binding.decision_id!=validated.decision_id):
_refuse()
subject=binding['subject']; resource=binding['resource']
- if (request['subject']!={'id':'secrets-engine','type':'service'}
+ if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
or subject.get('tenant') not in (None,'tenant:platform')
or 'principal' in subject.get('attributes',{})
@@ -140,6 +141,22 @@ def _validate(cfg, action, contract):
or supplied.get('digest')!=claim_binding_digest(**native)
or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
+ observation=envelope['provenance'].get('approval_observation')
+ if observation is not None:
+ # Signed producer source evidence supplements the original carried claim;
+ # it cannot replace original context, extend freshness, or authorize CAS.
+ if (not isinstance(observation,dict)
+ or any(not isinstance(observation.get(k),str) or not observation[k]
+ for k in ('source_id','source_revision'))
+ or any(not isinstance(observation.get(k),str)
+ or re.fullmatch(r'sha256:[0-9a-f]{64}',observation[k]) is None
+ for k in ('configuration_digest','claim_digest','native_binding_digest'))
+ or observation['native_binding_digest']!=supplied['digest']
+ or _stamp(observation.get('observed_at'))>_stamp(observation.get('validated_at'))
+ or _stamp(observation.get('validated_at'))>upper
+ or upper>=_stamp(observation.get('not_after'))
+ or _stamp(envelope['lifetime']['expires_at'])>_stamp(observation.get('not_after'))):
+ _refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
diff --git a/tests/fixtures/pdp-signature/approval-native-vectors.json b/tests/fixtures/pdp-signature/approval-native-vectors.json
new file mode 100644
index 0000000..46adb26
--- /dev/null
+++ b/tests/fixtures/pdp-signature/approval-native-vectors.json
@@ -0,0 +1,210 @@
+[
+ {
+ "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"
+ }
+]
diff --git a/tests/test_execution_context.py b/tests/test_execution_context.py
index 23b6419..70d9415 100644
--- a/tests/test_execution_context.py
+++ b/tests/test_execution_context.py
@@ -133,7 +133,7 @@ def test_closed_schema_refuses(mutation):
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
- e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
+ e=entry(); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
@@ -143,7 +143,7 @@ def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action)
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -154,7 +154,7 @@ def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -200,3 +200,22 @@ def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools
+
+
+def test_execution_service_declaration_is_fixed_before_issue():
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ built=request(e,contract)
+ assert built['context']['service_actor_binding']=={
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform'}
+ assert built['subject']=={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
+ assert built['resource']['tenant']=='tenant:platform'
+ assert 'service_actor_binding' not in build_action_request(e,'apply',subject_id='secrets-engine',subject_type='service',purpose='metadata-test')['context']
+
+
+@pytest.mark.parametrize('subject_id,subject_type',[('other','service'),('secrets-engine','Human'),('user:alice','Human')])
+def test_execution_declaration_cannot_assert_foreign_service(subject_id,subject_type):
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ with pytest.raises(DecisionError,match='service subject'):
+ build_action_request(e,'execute-ccr',subject_id=subject_id,subject_type=subject_type,purpose='metadata-test',execution_contract=contract)
diff --git a/tests/test_trusted_successor.py b/tests/test_trusted_successor.py
new file mode 100644
index 0000000..3c47a1d
--- /dev/null
+++ b/tests/test_trusted_successor.py
@@ -0,0 +1,80 @@
+"""Owner signature interoperability, independent synthetic admission only."""
+import copy
+import json
+from pathlib import Path
+import subprocess
+
+import pytest
+from secrets_engine.decision_signature import authenticate_response,load_trust
+from secrets_engine.errors import DecisionError
+from tests.test_decision_signature import FIXTURE,trust_file,verifier
+from tests.signature_support import signature_tools
+from tests.test_task_execution_handoff import lane,prepared,consume_task_execution
+
+VECTORS=json.loads((Path(__file__).parent/'fixtures/pdp-signature/approval-native-vectors.json').read_text())
+
+@pytest.mark.parametrize('vector',VECTORS)
+def test_fourteen_owner_native_types_retain_signed_bytes(vector,trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env.pop('signature',None)
+ env['diagnostics']={'native_vector':'REPLACE_RAW_TARGET'}
+ raw=json.dumps(env).replace('"REPLACE_RAW_TARGET"',vector['input_target_json']).encode()
+ signed=subprocess.run([str(signature_tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ result=authenticate_response(signed,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ again=authenticate_response(result.authenticated_json.encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ assert again['diagnostics']['native_vector']==json.loads(vector['input_target_json'])
+ tampered=signed.replace(b'"action":"rotate"',b'"action":"destroy"')
+ assert tampered!=signed
+ with pytest.raises(DecisionError):authenticate_response(tampered,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_unknown_provenance_refuses(trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env['provenance']['unknown_source_authority']=True
+ with pytest.raises(DecisionError):
+ authenticate_response(json.dumps(env).encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_exact_original_numeric_lexeme_survives_strict_cas(lane,monkeypatch,signature_tools):
+ import tests.test_task_execution_handoff as fixtures
+ cfg,e,contract,state=lane
+ def raw_sign(env,tools):
+ env['diagnostics']={'numeric':'LEXEME'}
+ raw=json.dumps(env).replace('"LEXEME"','1e-7').encode()
+ return subprocess.run([str(tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ monkeypatch.setattr(fixtures,'sign_envelope',raw_sign)
+ authorization=prepared(lane)
+ assert '1e-7' in authorization._action.artifacts
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+def source_evidence(envelope,claim):
+ from datetime import datetime,timezone
+ return {'source_id':'synthetic-source','source_revision':'synthetic-revision',
+ 'configuration_digest':'sha256:'+'2'*64,'claim_digest':'sha256:'+'3'*64,
+ 'native_binding_digest':claim['binding']['digest'],
+ 'observed_at':datetime.now(timezone.utc).isoformat(),
+ 'validated_at':datetime.now(timezone.utc).isoformat(),
+ 'not_after':envelope['lifetime']['expires_at']}
+
+
+def test_signed_source_evidence_keeps_original_context_and_cas(lane):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda env:env['provenance'].__setitem__('approval_observation',source_evidence(env,state['claim']))
+ authorization=prepared(lane)
+ snapshot=json.loads(authorization._action.artifacts)
+ assert snapshot['envelope']['binding']['context']==snapshot['request']['context']
+ assert snapshot['request']['context']['approval']==state['claim']
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+@pytest.mark.parametrize('field,value',[
+ ('source_id',''),('source_revision',''),('configuration_digest','wrong'),('claim_digest','wrong'),
+ ('native_binding_digest','sha256:'+'0'*64),('observed_at','2099-01-01T00:00:00Z'),
+ ('validated_at','2099-01-01T00:00:00Z'),('not_after','2000-01-01T00:00:00Z')])
+def test_signed_invalid_source_evidence_refuses_before_cas(lane,field,value):
+ cfg,e,contract,state=lane
+ def change(env):
+ evidence=source_evidence(env,state['claim']);evidence[field]=value
+ env['provenance']['approval_observation']=evidence
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
diff --git a/verifier/README.md b/verifier/README.md
index a63b2a0..c2c3bec 100644
--- a/verifier/README.md
+++ b/verifier/README.md
@@ -1,7 +1,7 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
-by revision and SHA-256 in `producer-source.json`. License attribution and the
+by source revision/proposal and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
@@ -33,3 +33,23 @@ For the complete reproducible CLI build and offline fresh-prefix installation,
use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.
+
+The trusted-service successor snapshot is the frozen Access B2/B3 R1 proposal,
+identified by its patch SHA-256 in `producer-source.json`; this is source review,
+not a claim of published producer revision or runtime/key admission. The wrapper
+uses producer-owned `sign.DecodeEnvelope` to preserve JSON numeric lexemes in
+signed maps. It retains exact authenticated producer JSON through strict
+preparation and CAS reauthentication; Python does not recreate signed bytes.
+Approval's fourteen native binding vectors are signature interoperability cases,
+not an expansion of existing request numeric admission. Signed
+`approval_observation` supplements the original immutable carried claim;
+strict handoff checks its native digest and observation/deadline bounds. It never
+replaces original context or freshness.
+
+Successor R2 execution builder correspondence emits the producer's fixed public
+DeclaredServiceBinding only for the typed prod/platform secrets-engine service
+lane before approval issuance. Both reference tenants are explicit to match
+existing producer normalization. It is not caller authentication; independently
+signed enforced caller, exact original/native/descriptor joins and all existing
+approval, expiry and CAS gates still apply. Actual production source-server
+interoperability receipts are separate from native runtime admission.
diff --git a/verifier/internal/sign/sign.go b/verifier/internal/sign/sign.go
index fc607f7..011f8f7 100644
--- a/verifier/internal/sign/sign.go
+++ b/verifier/internal/sign/sign.go
@@ -4,10 +4,12 @@
package sign
import (
+ "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
+ "io"
"github.com/netkingdom/access-engine/pkg/api"
)
@@ -74,3 +76,20 @@ func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) er
}
return nil
}
+
+// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs.
+// Legacy producer envelopes remain byte-compatible. It does not invent another
+// signature canonicalization: CanonicalBytes still calls typed json.Marshal.
+func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) {
+ var envelope api.DecisionEnvelope
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.UseNumber()
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&envelope); err != nil {
+ return envelope, err
+ }
+ if _, err := decoder.Token(); err != io.EOF {
+ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data")
+ }
+ return envelope, nil
+}
diff --git a/verifier/main.go b/verifier/main.go
index 1f3f9de..7449c64 100644
--- a/verifier/main.go
+++ b/verifier/main.go
@@ -2,13 +2,11 @@
package main
import (
- "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
@@ -23,10 +21,8 @@ func run() error {
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
- var envelope api.DecisionEnvelope
- decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
- decoder.DisallowUnknownFields()
- if err := decoder.Decode(&envelope); err != nil {
+ envelope, err := sign.DecodeEnvelope(in.Envelope)
+ if err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}
diff --git a/verifier/pkg/api/canonical.go b/verifier/pkg/api/canonical.go
index 3baa784..ae11a2d 100644
--- a/verifier/pkg/api/canonical.go
+++ b/verifier/pkg/api/canonical.go
@@ -635,16 +635,30 @@ type Obligation struct {
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
- Evaluator string `json:"evaluator" yaml:"evaluator"`
- Mode string `json:"mode" yaml:"mode"`
- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+ Evaluator string `json:"evaluator" yaml:"evaluator"`
+ Mode string `json:"mode" yaml:"mode"`
+ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
+ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
+ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
+ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
+ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
+ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
+ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
+ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"`
+ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+}
+
+// ApprovalObservationProvenance binds fresh server-owned fact evidence without
+// rewriting the original carried claim/context or either PDP request digest.
+type ApprovalObservationProvenance struct {
+ SourceID string `json:"source_id"`
+ SourceRevision string `json:"source_revision"`
+ ConfigurationDigest string `json:"configuration_digest"`
+ ClaimDigest string `json:"claim_digest"`
+ NativeBindingDigest string `json:"native_binding_digest"`
+ ObservedAt string `json:"observed_at"`
+ NotAfter string `json:"not_after"`
+ ValidatedAt string `json:"validated_at"`
}
// CallerProvenance records how the request was authenticated to flex-auth.
diff --git a/verifier/producer-source.json b/verifier/producer-source.json
index b6b0cba..99e651d 100644
--- a/verifier/producer-source.json
+++ b/verifier/producer-source.json
@@ -1,15 +1,19 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
- "revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
+ "revision": "B2/B3 R1 source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
- "pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
+ "pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
- "internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
+ "internal/sign/sign.go": "0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
+ },
+ "source_proposal": {
+ "patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "status": "source proposal; no runtime or key admission"
}
}
diff --git a/verifier/testsupport/signer/main.go b/verifier/testsupport/signer/main.go
index 2a7fecd..994ea82 100644
--- a/verifier/testsupport/signer/main.go
+++ b/verifier/testsupport/signer/main.go
@@ -6,7 +6,7 @@ import (
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
+ "io"
"os"
)
@@ -17,8 +17,12 @@ func main() {
seed = 0x43
kid = "next-admitted"
}
- var env api.DecisionEnvelope
- if json.NewDecoder(os.Stdin).Decode(&env) != nil {
+ raw, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ os.Exit(1)
+ }
+ env, err := sign.DecodeEnvelope(raw)
+ if err != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 28102cf..3109752 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -114,3 +114,31 @@ producer candidate OCI and actual runtime/config placement remain missing. The
empty-key trust template deliberately refuses. No install, key read, caller mint,
protected effect, policy switch or trust admission occurred. T02 remains progress,
T03 wait; task source/live obligations remain separately in SECRETS-WP-0013.
+
+2026-10-16 trusted successor source preparation: refreshed exact API/sign source
+from frozen Access B2/B3 R1 proposal (patch SHA-256
+6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1).
+Producer DecodeEnvelope preserves signed numeric lexemes; strict CAS retains and
+reauthenticates original producer JSON. Existing request/claim/native/caller joins
+remain mandatory; signed source observation additionally binds the native digest
+and caps decision lifetime. Fourteen Approval owner vectors, legacy signatures,
+tamper/unknown provenance and source deadline refusals are locally checked.
+Actual ApprovalApp-to-Access v2 signed response verifies with the consumer helper;
+actual source revocation refuses. Actual builder-to-v2 strict positive execution
+is separately blocked on missing owner-reviewed service_actor_binding mapping;
+no caller context injection or equality relaxation hides that gap.
+Artifacts: Custodian `decision-trusted-successor/`. T02 remains progress, T03
+wait; no package/key/policy/native admission or runtime installation occurred.
+
+2026-10-16 successor R2 source preparation resolves the preceding builder gap:
+only typed prod/platform execute-ccr with exact secrets-engine service subject
+emits Access's fixed five-field public service_actor_binding before approval
+issue. Subject/resource platform tenants are explicit before issue, matching the
+producer's existing normalization and native act; lifecycle requests are unchanged.
+Original carried context is still immutable. Actual builder -> Approval App ->
+Access v2 signed ALLOW -> strict preparation/CAS -> independent Approval confirmation
+GET passes with synthetic scoped identities/trust. Missing declaration, descriptor/
+native changes, stale original claim, signature tamper and replay refuse. The
+operation deadline is explicit whole seconds within v2's 15-second allow TTL;
+no deadline is extended or waived. This resolves source correspondence only;
+T02 remains progress/T03 wait, with no native trust/grant/policy admission.

View file

@ -0,0 +1,127 @@
diff --git a/src/secrets_engine/execution_contract.py b/src/secrets_engine/execution_contract.py
index 5dc1cae..f9b2538 100644
--- a/src/secrets_engine/execution_contract.py
+++ b/src/secrets_engine/execution_contract.py
@@ -153,6 +153,18 @@ def attach_execution_contract(request: dict, contract: ExecutionContract | None)
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
+ if request['subject'] != {'id':'secrets-engine','type':'service'}:
+ raise DecisionError('execute-ccr requires secrets-engine service subject')
+ # Match the producer's fixed tenant normalization before approval issue.
+ request['subject']['tenant'] = 'tenant:platform'
+ request['resource']['tenant'] = 'tenant:platform'
+ # Public declaration from Access ServicePilotInputs.DeclaredServiceBinding;
+ # bound before approval issuance, never an authentication assertion.
+ request['context']['service_actor_binding'] = {
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform',
+ }
request['context']['execution_contract'] = value
request['context']['human_control'] = True
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
index eea6a85..5af36be 100644
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -116,7 +116,7 @@ def _validate(cfg, action, contract):
or action.binding.decision_id!=validated.decision_id):
_refuse()
subject=binding['subject']; resource=binding['resource']
- if (request['subject']!={'id':'secrets-engine','type':'service'}
+ if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
or subject.get('tenant') not in (None,'tenant:platform')
or 'principal' in subject.get('attributes',{})
diff --git a/tests/test_execution_context.py b/tests/test_execution_context.py
index 23b6419..70d9415 100644
--- a/tests/test_execution_context.py
+++ b/tests/test_execution_context.py
@@ -133,7 +133,7 @@ def test_closed_schema_refuses(mutation):
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
- e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
+ e=entry(); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
@@ -143,7 +143,7 @@ def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action)
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -154,7 +154,7 @@ def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -200,3 +200,22 @@ def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools
+
+
+def test_execution_service_declaration_is_fixed_before_issue():
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ built=request(e,contract)
+ assert built['context']['service_actor_binding']=={
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform'}
+ assert built['subject']=={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
+ assert built['resource']['tenant']=='tenant:platform'
+ assert 'service_actor_binding' not in build_action_request(e,'apply',subject_id='secrets-engine',subject_type='service',purpose='metadata-test')['context']
+
+
+@pytest.mark.parametrize('subject_id,subject_type',[('other','service'),('secrets-engine','Human'),('user:alice','Human')])
+def test_execution_declaration_cannot_assert_foreign_service(subject_id,subject_type):
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ with pytest.raises(DecisionError,match='service subject'):
+ build_action_request(e,'execute-ccr',subject_id=subject_id,subject_type=subject_type,purpose='metadata-test',execution_contract=contract)
diff --git a/verifier/README.md b/verifier/README.md
index 6a1b56b..c2c3bec 100644
--- a/verifier/README.md
+++ b/verifier/README.md
@@ -45,3 +45,11 @@ not an expansion of existing request numeric admission. Signed
`approval_observation` supplements the original immutable carried claim;
strict handoff checks its native digest and observation/deadline bounds. It never
replaces original context or freshness.
+
+Successor R2 execution builder correspondence emits the producer's fixed public
+DeclaredServiceBinding only for the typed prod/platform secrets-engine service
+lane before approval issuance. Both reference tenants are explicit to match
+existing producer normalization. It is not caller authentication; independently
+signed enforced caller, exact original/native/descriptor joins and all existing
+approval, expiry and CAS gates still apply. Actual production source-server
+interoperability receipts are separate from native runtime admission.
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 8d42c05..3109752 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -129,3 +129,16 @@ is separately blocked on missing owner-reviewed service_actor_binding mapping;
no caller context injection or equality relaxation hides that gap.
Artifacts: Custodian `decision-trusted-successor/`. T02 remains progress, T03
wait; no package/key/policy/native admission or runtime installation occurred.
+
+2026-10-16 successor R2 source preparation resolves the preceding builder gap:
+only typed prod/platform execute-ccr with exact secrets-engine service subject
+emits Access's fixed five-field public service_actor_binding before approval
+issue. Subject/resource platform tenants are explicit before issue, matching the
+producer's existing normalization and native act; lifecycle requests are unchanged.
+Original carried context is still immutable. Actual builder -> Approval App ->
+Access v2 signed ALLOW -> strict preparation/CAS -> independent Approval confirmation
+GET passes with synthetic scoped identities/trust. Missing declaration, descriptor/
+native changes, stale original claim, signature tamper and replay refuse. The
+operation deadline is explicit whole seconds within v2's 15-second allow TTL;
+no deadline is extended or waived. This resolves source correspondence only;
+T02 remains progress/T03 wait, with no native trust/grant/policy admission.

View file

@ -0,0 +1,806 @@
diff --git a/docs/task-scoped-openbao-execution-contract.md b/docs/task-scoped-openbao-execution-contract.md
index 6128f2e..b9fea01 100644
--- a/docs/task-scoped-openbao-execution-contract.md
+++ b/docs/task-scoped-openbao-execution-contract.md
@@ -65,7 +65,7 @@ before observing the claim or calling Check. Its fields are:
| source_bundle | Absolute owner-resolved files and immutable SHA256 hashes for engine dispatcher, Platform mediator, typed CCR module and policy/auth source |
| backend_origin / cluster_uid | Exact reviewed estate OpenBao origin and railiance01 identity |
| operations | Ordered exact HTTP method, normalized path and canonical JSON/text payload hash for each read/write/readback; no user supplied body |
-| operation_deadline | Upper bound no later than approval, PDP or task-token expiry |
+| operation_deadline | Upper bound no later than approval, PDP, caller and original/fresh source validity; task-token expiry may be earlier |
| backend_budget | TTL at most 120s, at most twelve uses, no renew/child/wrap/unlimited uses |
| expected_before / expected_after | Owner-authenticated revision/state digests, never secret contents |
@@ -158,10 +158,20 @@ validation while Platform owns its OpenBao credential and typed native primitive
Task-token use budget is declared before issuance: two before-state reads, two
writes, two readbacks and one self-revoke (seven uses), plus at most four explicit
metadata capability checks and one reserved cleanup use (twelve total). Failed
-backend calls count; never retry to evade limits. Task TTL is
-`min(120s, approval_remaining, decision_remaining, operation_remaining)` minus
-the declared clock uncertainty/cleanup reserve. Reject if fewer than 30s remain
-for planned requests. No renewal or remint extends the operation. CCR38 keeps its
+backend calls count; never retry to evade limits. The immutable
+`backend_budget.ttl_seconds` declares the requested task-token TTL (30–120s).
+The native adapter does not dynamically recompute or shorten that declaration.
+Before issuance, conservatively require at least the declared TTL plus the 5s
+cleanup reserve within the remaining operation/approval/PDP authority, including
+clock uncertainty. The requested and explicit maximum native token TTL equal
+the declared budget; returned native TTL must be positive and at most that budget.
+The token can expire before the operation deadline; failure or expiry stops work
+without remint. The signed operation deadline remains no later than approval,
+PDP, authenticated caller and both original/fresh source validity. Access's
+trusted source path independently requires the same TTL-plus-5s fit after source
+I/O and an operation interval no longer than 120s. Therefore a structural 120s
+backend budget cannot fit inside a 120s authority interval with the reserve and
+refuses. No renewal or remint extends the operation. CCR38 keeps its
own 300s/eight-use limit; login-self checks and revoke fit its separate budget.
## Failure, replay and recovery
diff --git a/src/secrets_engine/approval_consume.py b/src/secrets_engine/approval_consume.py
index 1cf4140..e4865bb 100644
--- a/src/secrets_engine/approval_consume.py
+++ b/src/secrets_engine/approval_consume.py
@@ -382,7 +382,8 @@ def authorize_action(
),
decision_id=validated.decision_id,
expires_at=validated.expires_at,
- artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
+ artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope,
+ "authenticated_envelope_json":getattr(envelope,"authenticated_json","")},
sort_keys=True,separators=(",",":"),allow_nan=False),
)
diff --git a/src/secrets_engine/decision_signature.py b/src/secrets_engine/decision_signature.py
index 24fa8d3..d9bd0b9 100644
--- a/src/secrets_engine/decision_signature.py
+++ b/src/secrets_engine/decision_signature.py
@@ -7,6 +7,7 @@ from __future__ import annotations
import base64
import json
+import math
from pathlib import Path
import stat
import subprocess
@@ -24,8 +25,15 @@ def _unique_object(pairs):
return result
+def _finite_float(value):
+ parsed = float(value)
+ if not math.isfinite(parsed):
+ raise ValueError("nonfinite JSON number")
+ return parsed
+
+
def strict_json(data):
- return json.loads(data, object_pairs_hook=_unique_object,
+ return json.loads(data, object_pairs_hook=_unique_object, parse_float=_finite_float,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError("invalid number")))
@@ -66,10 +74,18 @@ def load_trust(path: Path | None) -> dict[str, Any]:
raise DecisionError("access-engine responder trust is missing or invalid") from exc
+class AuthenticatedEnvelope(dict):
+ """Verified typed values with exact producer JSON retained for reauthentication."""
+ def __init__(self, values, authenticated_json):
+ super().__init__(values)
+ self.authenticated_json = authenticated_json
+
+
def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) -> dict:
if verifier is None or not Path(verifier).is_absolute():
raise DecisionError("access-engine signature verifier is not installed")
try:
+ strict_json(body.decode("utf-8"))
result = subprocess.run([str(verifier)], input=b'{"envelope":'+body+b',"keys":'+json.dumps(trust['keys']).encode()+b'}',
capture_output=True, env={}, timeout=3, check=False)
if result.returncode != 0 or len(result.stdout) > 524288:
@@ -78,6 +94,6 @@ def authenticate_response(body: bytes, *, trust: dict, verifier: Path | None) ->
provenance = authenticated['provenance']
if any(provenance.get(k) != trust[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')):
raise ValueError("issuer/pin mismatch")
- return authenticated
- except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError) as exc:
+ return AuthenticatedEnvelope(authenticated, result.stdout.decode("utf-8"))
+ except (OSError, subprocess.SubprocessError, ValueError, KeyError, TypeError, RecursionError, UnicodeDecodeError) as exc:
raise DecisionError("access-engine responder authentication failed") from exc
diff --git a/src/secrets_engine/execution_contract.py b/src/secrets_engine/execution_contract.py
index 5dc1cae..f9b2538 100644
--- a/src/secrets_engine/execution_contract.py
+++ b/src/secrets_engine/execution_contract.py
@@ -153,6 +153,18 @@ def attach_execution_contract(request: dict, contract: ExecutionContract | None)
raise DecisionError('execute-ccr requires platform tenant')
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
raise DecisionError('execution_contract catalog target does not match')
+ if request['subject'] != {'id':'secrets-engine','type':'service'}:
+ raise DecisionError('execute-ccr requires secrets-engine service subject')
+ # Match the producer's fixed tenant normalization before approval issue.
+ request['subject']['tenant'] = 'tenant:platform'
+ request['resource']['tenant'] = 'tenant:platform'
+ # Public declaration from Access ServicePilotInputs.DeclaredServiceBinding;
+ # bound before approval issuance, never an authentication assertion.
+ request['context']['service_actor_binding'] = {
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform',
+ }
request['context']['execution_contract'] = value
request['context']['human_control'] = True
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
index 9772861..5af36be 100644
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -10,6 +10,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import json
+import re
from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
@@ -85,10 +86,10 @@ def _validate(cfg, action, contract):
request,claim,envelope=(artifacts[name] for name in ('request','claim','envelope'))
# Rotation/removal and installed-helper changes cannot hide behind a cached
# prepared label. Reauthenticate before each strict CAS, without networking.
- authenticated=authenticate_response(json.dumps(envelope,allow_nan=False).encode(),
+ authenticated=authenticate_response(artifacts["authenticated_envelope_json"].encode("utf-8"),
trust=load_trust(getattr(cfg,'pdp_trust_file',None)),
verifier=getattr(cfg,'pdp_verifier',None))
- if authenticated != envelope:
+ if json.dumps(authenticated,sort_keys=True,allow_nan=False) != json.dumps(envelope,sort_keys=True,allow_nan=False):
_refuse()
window=read_window(cfg)
lower,upper=validity_bounds(window=window)
@@ -115,7 +116,7 @@ def _validate(cfg, action, contract):
or action.binding.decision_id!=validated.decision_id):
_refuse()
subject=binding['subject']; resource=binding['resource']
- if (request['subject']!={'id':'secrets-engine','type':'service'}
+ if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
or subject.get('tenant') not in (None,'tenant:platform')
or 'principal' in subject.get('attributes',{})
@@ -140,6 +141,22 @@ def _validate(cfg, action, contract):
or supplied.get('digest')!=claim_binding_digest(**native)
or supplied.get('digest')!=claim_binding_digest(**{key:supplied.get(key) for key in _NATIVE_FIELDS})):
_refuse()
+ observation=envelope['provenance'].get('approval_observation')
+ if observation is not None:
+ # Signed producer source evidence supplements the original carried claim;
+ # it cannot replace original context, extend freshness, or authorize CAS.
+ if (not isinstance(observation,dict)
+ or any(not isinstance(observation.get(k),str) or not observation[k]
+ for k in ('source_id','source_revision'))
+ or any(not isinstance(observation.get(k),str)
+ or re.fullmatch(r'sha256:[0-9a-f]{64}',observation[k]) is None
+ for k in ('configuration_digest','claim_digest','native_binding_digest'))
+ or observation['native_binding_digest']!=supplied['digest']
+ or _stamp(observation.get('observed_at'))>_stamp(observation.get('validated_at'))
+ or _stamp(observation.get('validated_at'))>upper
+ or upper>=_stamp(observation.get('not_after'))
+ or _stamp(envelope['lifetime']['expires_at'])>_stamp(observation.get('not_after'))):
+ _refuse()
return {'execution_contract':descriptor,'approval_id':action.binding.approval_id,
'request_digest':validated.request_digest,'decision_id':validated.decision_id,
'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
diff --git a/tests/fixtures/pdp-signature/approval-native-vectors.json b/tests/fixtures/pdp-signature/approval-native-vectors.json
new file mode 100644
index 0000000..46adb26
--- /dev/null
+++ b/tests/fixtures/pdp-signature/approval-native-vectors.json
@@ -0,0 +1,210 @@
+[
+ {
+ "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"
+ }
+]
diff --git a/tests/test_execution_context.py b/tests/test_execution_context.py
index 23b6419..70d9415 100644
--- a/tests/test_execution_context.py
+++ b/tests/test_execution_context.py
@@ -133,7 +133,7 @@ def test_closed_schema_refuses(mutation):
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
- e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
+ e=entry(); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
with pytest.raises(DecisionError):
@@ -143,7 +143,7 @@ def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action)
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='catalog'):
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -154,7 +154,7 @@ def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
def test_expired_contract_refuses_before_claim(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
- cfg=_Cfg(_token(tmp_path)); calls=[]
+ cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service'); calls=[]
with pytest.raises(DecisionError,match='expired'):
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
assert not calls
@@ -200,3 +200,22 @@ def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools
+
+
+def test_execution_service_declaration_is_fixed_before_issue():
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ built=request(e,contract)
+ assert built['context']['service_actor_binding']=={
+ 'version':'proposed-service-task-caller/v1',
+ 'transport_principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'subject_id':'secrets-engine','subject_type':'service','tenant':'tenant:platform'}
+ assert built['subject']=={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
+ assert built['resource']['tenant']=='tenant:platform'
+ assert 'service_actor_binding' not in build_action_request(e,'apply',subject_id='secrets-engine',subject_type='service',purpose='metadata-test')['context']
+
+
+@pytest.mark.parametrize('subject_id,subject_type',[('other','service'),('secrets-engine','Human'),('user:alice','Human')])
+def test_execution_declaration_cannot_assert_foreign_service(subject_id,subject_type):
+ e=entry();contract=ExecutionContract.parse(descriptor(e))
+ with pytest.raises(DecisionError,match='service subject'):
+ build_action_request(e,'execute-ccr',subject_id=subject_id,subject_type=subject_type,purpose='metadata-test',execution_contract=contract)
diff --git a/tests/test_trusted_successor.py b/tests/test_trusted_successor.py
new file mode 100644
index 0000000..3c47a1d
--- /dev/null
+++ b/tests/test_trusted_successor.py
@@ -0,0 +1,80 @@
+"""Owner signature interoperability, independent synthetic admission only."""
+import copy
+import json
+from pathlib import Path
+import subprocess
+
+import pytest
+from secrets_engine.decision_signature import authenticate_response,load_trust
+from secrets_engine.errors import DecisionError
+from tests.test_decision_signature import FIXTURE,trust_file,verifier
+from tests.signature_support import signature_tools
+from tests.test_task_execution_handoff import lane,prepared,consume_task_execution
+
+VECTORS=json.loads((Path(__file__).parent/'fixtures/pdp-signature/approval-native-vectors.json').read_text())
+
+@pytest.mark.parametrize('vector',VECTORS)
+def test_fourteen_owner_native_types_retain_signed_bytes(vector,trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env.pop('signature',None)
+ env['diagnostics']={'native_vector':'REPLACE_RAW_TARGET'}
+ raw=json.dumps(env).replace('"REPLACE_RAW_TARGET"',vector['input_target_json']).encode()
+ signed=subprocess.run([str(signature_tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ result=authenticate_response(signed,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ again=authenticate_response(result.authenticated_json.encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+ assert again['diagnostics']['native_vector']==json.loads(vector['input_target_json'])
+ tampered=signed.replace(b'"action":"rotate"',b'"action":"destroy"')
+ assert tampered!=signed
+ with pytest.raises(DecisionError):authenticate_response(tampered,trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_unknown_provenance_refuses(trust_file,signature_tools):
+ env=json.loads(FIXTURE.read_text());env['provenance']['unknown_source_authority']=True
+ with pytest.raises(DecisionError):
+ authenticate_response(json.dumps(env).encode(),trust=load_trust(trust_file),verifier=signature_tools/'verify')
+
+
+def test_exact_original_numeric_lexeme_survives_strict_cas(lane,monkeypatch,signature_tools):
+ import tests.test_task_execution_handoff as fixtures
+ cfg,e,contract,state=lane
+ def raw_sign(env,tools):
+ env['diagnostics']={'numeric':'LEXEME'}
+ raw=json.dumps(env).replace('"LEXEME"','1e-7').encode()
+ return subprocess.run([str(tools/'sign')],input=raw,capture_output=True,check=True).stdout
+ monkeypatch.setattr(fixtures,'sign_envelope',raw_sign)
+ authorization=prepared(lane)
+ assert '1e-7' in authorization._action.artifacts
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+def source_evidence(envelope,claim):
+ from datetime import datetime,timezone
+ return {'source_id':'synthetic-source','source_revision':'synthetic-revision',
+ 'configuration_digest':'sha256:'+'2'*64,'claim_digest':'sha256:'+'3'*64,
+ 'native_binding_digest':claim['binding']['digest'],
+ 'observed_at':datetime.now(timezone.utc).isoformat(),
+ 'validated_at':datetime.now(timezone.utc).isoformat(),
+ 'not_after':envelope['lifetime']['expires_at']}
+
+
+def test_signed_source_evidence_keeps_original_context_and_cas(lane):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda env:env['provenance'].__setitem__('approval_observation',source_evidence(env,state['claim']))
+ authorization=prepared(lane)
+ snapshot=json.loads(authorization._action.artifacts)
+ assert snapshot['envelope']['binding']['context']==snapshot['request']['context']
+ assert snapshot['request']['context']['approval']==state['claim']
+ assert consume_task_execution(cfg,authorization).idempotent is False
+
+
+@pytest.mark.parametrize('field,value',[
+ ('source_id',''),('source_revision',''),('configuration_digest','wrong'),('claim_digest','wrong'),
+ ('native_binding_digest','sha256:'+'0'*64),('observed_at','2099-01-01T00:00:00Z'),
+ ('validated_at','2099-01-01T00:00:00Z'),('not_after','2000-01-01T00:00:00Z')])
+def test_signed_invalid_source_evidence_refuses_before_cas(lane,field,value):
+ cfg,e,contract,state=lane
+ def change(env):
+ evidence=source_evidence(env,state['claim']);evidence[field]=value
+ env['provenance']['approval_observation']=evidence
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
diff --git a/verifier/README.md b/verifier/README.md
index a63b2a0..fe7ec93 100644
--- a/verifier/README.md
+++ b/verifier/README.md
@@ -1,7 +1,7 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
-by revision and SHA-256 in `producer-source.json`. License attribution and the
+by source revision/proposal and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
@@ -33,3 +33,23 @@ For the complete reproducible CLI build and offline fresh-prefix installation,
use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.
+
+The trusted-service successor snapshot follows the frozen Access B2/B3 proposal lineage,
+identified by its patch SHA-256 in `producer-source.json`; this is source review,
+not a claim of published producer revision or runtime/key admission. The wrapper
+uses producer-owned `sign.DecodeEnvelope` to preserve JSON numeric lexemes in
+signed maps. It retains exact authenticated producer JSON through strict
+preparation and CAS reauthentication; Python does not recreate signed bytes.
+Approval's fourteen native binding vectors are signature interoperability cases,
+not an expansion of existing request numeric admission. Signed
+`approval_observation` supplements the original immutable carried claim;
+strict handoff checks its native digest and observation/deadline bounds. It never
+replaces original context or freshness.
+
+Successor R2 execution builder correspondence emits the producer's fixed public
+DeclaredServiceBinding only for the typed prod/platform secrets-engine service
+lane before approval issuance. Both reference tenants are explicit to match
+existing producer normalization. It is not caller authentication; independently
+signed enforced caller, exact original/native/descriptor joins and all existing
+approval, expiry and CAS gates still apply. Actual production source-server
+interoperability receipts are separate from native runtime admission.
diff --git a/verifier/internal/sign/sign.go b/verifier/internal/sign/sign.go
index fc607f7..011f8f7 100644
--- a/verifier/internal/sign/sign.go
+++ b/verifier/internal/sign/sign.go
@@ -4,10 +4,12 @@
package sign
import (
+ "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
+ "io"
"github.com/netkingdom/access-engine/pkg/api"
)
@@ -74,3 +76,20 @@ func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) er
}
return nil
}
+
+// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs.
+// Legacy producer envelopes remain byte-compatible. It does not invent another
+// signature canonicalization: CanonicalBytes still calls typed json.Marshal.
+func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) {
+ var envelope api.DecisionEnvelope
+ decoder := json.NewDecoder(bytes.NewReader(raw))
+ decoder.UseNumber()
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&envelope); err != nil {
+ return envelope, err
+ }
+ if _, err := decoder.Token(); err != io.EOF {
+ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data")
+ }
+ return envelope, nil
+}
diff --git a/verifier/main.go b/verifier/main.go
index 1f3f9de..7449c64 100644
--- a/verifier/main.go
+++ b/verifier/main.go
@@ -2,13 +2,11 @@
package main
import (
- "bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
@@ -23,10 +21,8 @@ func run() error {
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
- var envelope api.DecisionEnvelope
- decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
- decoder.DisallowUnknownFields()
- if err := decoder.Decode(&envelope); err != nil {
+ envelope, err := sign.DecodeEnvelope(in.Envelope)
+ if err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}
diff --git a/verifier/pkg/api/canonical.go b/verifier/pkg/api/canonical.go
index 3baa784..ae11a2d 100644
--- a/verifier/pkg/api/canonical.go
+++ b/verifier/pkg/api/canonical.go
@@ -635,16 +635,30 @@ type Obligation struct {
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
- Evaluator string `json:"evaluator" yaml:"evaluator"`
- Mode string `json:"mode" yaml:"mode"`
- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+ Evaluator string `json:"evaluator" yaml:"evaluator"`
+ Mode string `json:"mode" yaml:"mode"`
+ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
+ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
+ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
+ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
+ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
+ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
+ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
+ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"`
+ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
+}
+
+// ApprovalObservationProvenance binds fresh server-owned fact evidence without
+// rewriting the original carried claim/context or either PDP request digest.
+type ApprovalObservationProvenance struct {
+ SourceID string `json:"source_id"`
+ SourceRevision string `json:"source_revision"`
+ ConfigurationDigest string `json:"configuration_digest"`
+ ClaimDigest string `json:"claim_digest"`
+ NativeBindingDigest string `json:"native_binding_digest"`
+ ObservedAt string `json:"observed_at"`
+ NotAfter string `json:"not_after"`
+ ValidatedAt string `json:"validated_at"`
}
// CallerProvenance records how the request was authenticated to flex-auth.
diff --git a/verifier/producer-source.json b/verifier/producer-source.json
index b6b0cba..9a4d5a3 100644
--- a/verifier/producer-source.json
+++ b/verifier/producer-source.json
@@ -1,15 +1,21 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
- "revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
+ "revision": "B2/B3 R2 temporal source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
- "pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
+ "pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
- "internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
+ "internal/sign/sign.go": "0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
+ },
+ "source_proposal": {
+ "patch_sha256": "ed85a55b54f8380a657c0688d6f216699a0873b1b94b4c0c2e09332620c8ebf4",
+ "predecessor_patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "api_sign_bytes_unchanged": true,
+ "status": "source proposal; no runtime or key admission"
}
}
diff --git a/verifier/testsupport/signer/main.go b/verifier/testsupport/signer/main.go
index 2a7fecd..994ea82 100644
--- a/verifier/testsupport/signer/main.go
+++ b/verifier/testsupport/signer/main.go
@@ -6,7 +6,7 @@ import (
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
- "github.com/netkingdom/access-engine/pkg/api"
+ "io"
"os"
)
@@ -17,8 +17,12 @@ func main() {
seed = 0x43
kid = "next-admitted"
}
- var env api.DecisionEnvelope
- if json.NewDecoder(os.Stdin).Decode(&env) != nil {
+ raw, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ os.Exit(1)
+ }
+ env, err := sign.DecodeEnvelope(raw)
+ if err != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 28102cf..60f9e18 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -114,3 +114,48 @@ producer candidate OCI and actual runtime/config placement remain missing. The
empty-key trust template deliberately refuses. No install, key read, caller mint,
protected effect, policy switch or trust admission occurred. T02 remains progress,
T03 wait; task source/live obligations remain separately in SECRETS-WP-0013.
+
+2026-10-16 trusted successor source preparation: refreshed exact API/sign source
+from frozen Access B2/B3 R1 proposal (patch SHA-256
+6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1).
+Producer DecodeEnvelope preserves signed numeric lexemes; strict CAS retains and
+reauthenticates original producer JSON. Existing request/claim/native/caller joins
+remain mandatory; signed source observation additionally binds the native digest
+and caps decision lifetime. Fourteen Approval owner vectors, legacy signatures,
+tamper/unknown provenance and source deadline refusals are locally checked.
+Actual ApprovalApp-to-Access v2 signed response verifies with the consumer helper;
+actual source revocation refuses. Actual builder-to-v2 strict positive execution
+is separately blocked on missing owner-reviewed service_actor_binding mapping;
+no caller context injection or equality relaxation hides that gap.
+Artifacts: Custodian `decision-trusted-successor/`. T02 remains progress, T03
+wait; no package/key/policy/native admission or runtime installation occurred.
+
+2026-10-16 successor R2 source preparation resolves the preceding builder gap:
+only typed prod/platform execute-ccr with exact secrets-engine service subject
+emits Access's fixed five-field public service_actor_binding before approval
+issue. Subject/resource platform tenants are explicit before issue, matching the
+producer's existing normalization and native act; lifecycle requests are unchanged.
+Original carried context is still immutable. Actual builder -> Approval App ->
+Access v2 signed ALLOW -> strict preparation/CAS -> independent Approval confirmation
+GET passes with synthetic scoped identities/trust. Missing declaration, descriptor/
+native changes, stale original claim, signature tamper and replay refuse. The
+operation deadline is explicit whole seconds within v2's 15-second allow TTL;
+no deadline is extended or waived. This resolves source correspondence only;
+T02 remains progress/T03 wait, with no native trust/grant/policy admission.
+
+2026-10-16 temporal successor source proof: Access B2/B3 R2 replaces its earlier
+15s lifetime with the exact immutable operation deadline, bounded by its 120s
+hard cap, original/fresh source and caller bounds, and post-I/O backend TTL+5s
+fit. Verifier API/sign bytes are unchanged; source provenance records R2. Secrets
+runtime and strict entire-operation deadline checks are unchanged. Contract prose
+now states the actual fixed backend TTL and conservative fit rather than dynamic
+TTL recomputation, and permits native token expiry earlier than operation deadline.
+The actual source proof uses explicit synthetic Approval freshness120/backendTTL30/
+operationdeadline60, without a consumer timing waiver or native admission.
+
+Temporal proof precision observation: Approval consumption receipts use whole
+seconds while Access R2 not_before preserves subsecond precision. Fast CAS can
+therefore refuse receipt-before-start even though live calls are correctly
+ordered. The positive proof explicitly waits real time until the next whole
+second before CAS, without injected clocks or relaxed checks. Fast-CAS failure
+is retained for owner resolution; no Secrets runtime change conceals the seam.

View file

@ -0,0 +1,99 @@
diff --git a/docs/task-scoped-openbao-execution-contract.md b/docs/task-scoped-openbao-execution-contract.md
index 6128f2e..b9fea01 100644
--- a/docs/task-scoped-openbao-execution-contract.md
+++ b/docs/task-scoped-openbao-execution-contract.md
@@ -65,7 +65,7 @@ before observing the claim or calling Check. Its fields are:
| source_bundle | Absolute owner-resolved files and immutable SHA256 hashes for engine dispatcher, Platform mediator, typed CCR module and policy/auth source |
| backend_origin / cluster_uid | Exact reviewed estate OpenBao origin and railiance01 identity |
| operations | Ordered exact HTTP method, normalized path and canonical JSON/text payload hash for each read/write/readback; no user supplied body |
-| operation_deadline | Upper bound no later than approval, PDP or task-token expiry |
+| operation_deadline | Upper bound no later than approval, PDP, caller and original/fresh source validity; task-token expiry may be earlier |
| backend_budget | TTL at most 120s, at most twelve uses, no renew/child/wrap/unlimited uses |
| expected_before / expected_after | Owner-authenticated revision/state digests, never secret contents |
@@ -158,10 +158,20 @@ validation while Platform owns its OpenBao credential and typed native primitive
Task-token use budget is declared before issuance: two before-state reads, two
writes, two readbacks and one self-revoke (seven uses), plus at most four explicit
metadata capability checks and one reserved cleanup use (twelve total). Failed
-backend calls count; never retry to evade limits. Task TTL is
-`min(120s, approval_remaining, decision_remaining, operation_remaining)` minus
-the declared clock uncertainty/cleanup reserve. Reject if fewer than 30s remain
-for planned requests. No renewal or remint extends the operation. CCR38 keeps its
+backend calls count; never retry to evade limits. The immutable
+`backend_budget.ttl_seconds` declares the requested task-token TTL (30–120s).
+The native adapter does not dynamically recompute or shorten that declaration.
+Before issuance, conservatively require at least the declared TTL plus the 5s
+cleanup reserve within the remaining operation/approval/PDP authority, including
+clock uncertainty. The requested and explicit maximum native token TTL equal
+the declared budget; returned native TTL must be positive and at most that budget.
+The token can expire before the operation deadline; failure or expiry stops work
+without remint. The signed operation deadline remains no later than approval,
+PDP, authenticated caller and both original/fresh source validity. Access's
+trusted source path independently requires the same TTL-plus-5s fit after source
+I/O and an operation interval no longer than 120s. Therefore a structural 120s
+backend budget cannot fit inside a 120s authority interval with the reserve and
+refuses. No renewal or remint extends the operation. CCR38 keeps its
own 300s/eight-use limit; login-self checks and revoke fit its separate budget.
## Failure, replay and recovery
diff --git a/verifier/README.md b/verifier/README.md
index c2c3bec..fe7ec93 100644
--- a/verifier/README.md
+++ b/verifier/README.md
@@ -34,7 +34,7 @@ use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.
-The trusted-service successor snapshot is the frozen Access B2/B3 R1 proposal,
+The trusted-service successor snapshot follows the frozen Access B2/B3 proposal lineage,
identified by its patch SHA-256 in `producer-source.json`; this is source review,
not a claim of published producer revision or runtime/key admission. The wrapper
uses producer-owned `sign.DecodeEnvelope` to preserve JSON numeric lexemes in
diff --git a/verifier/producer-source.json b/verifier/producer-source.json
index 99e651d..9a4d5a3 100644
--- a/verifier/producer-source.json
+++ b/verifier/producer-source.json
@@ -1,6 +1,6 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
- "revision": "B2/B3 R1 source proposal over B1 R2; canonical native-prep base 919ce25",
+ "revision": "B2/B3 R2 temporal source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
"pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
@@ -13,7 +13,9 @@
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
},
"source_proposal": {
- "patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "patch_sha256": "ed85a55b54f8380a657c0688d6f216699a0873b1b94b4c0c2e09332620c8ebf4",
+ "predecessor_patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "api_sign_bytes_unchanged": true,
"status": "source proposal; no runtime or key admission"
}
}
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 3109752..60f9e18 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -142,3 +142,20 @@ native changes, stale original claim, signature tamper and replay refuse. The
operation deadline is explicit whole seconds within v2's 15-second allow TTL;
no deadline is extended or waived. This resolves source correspondence only;
T02 remains progress/T03 wait, with no native trust/grant/policy admission.
+
+2026-10-16 temporal successor source proof: Access B2/B3 R2 replaces its earlier
+15s lifetime with the exact immutable operation deadline, bounded by its 120s
+hard cap, original/fresh source and caller bounds, and post-I/O backend TTL+5s
+fit. Verifier API/sign bytes are unchanged; source provenance records R2. Secrets
+runtime and strict entire-operation deadline checks are unchanged. Contract prose
+now states the actual fixed backend TTL and conservative fit rather than dynamic
+TTL recomputation, and permits native token expiry earlier than operation deadline.
+The actual source proof uses explicit synthetic Approval freshness120/backendTTL30/
+operationdeadline60, without a consumer timing waiver or native admission.
+
+Temporal proof precision observation: Approval consumption receipts use whole
+seconds while Access R2 not_before preserves subsecond precision. Fast CAS can
+therefore refuse receipt-before-start even though live calls are correctly
+ordered. The positive proof explicitly waits real time until the next whole
+second before CAS, without injected clocks or relaxed checks. Fast-CAS failure
+is retained for owner resolution; no Secrets runtime change conceals the seam.

View file

@ -0,0 +1,365 @@
diff --git a/docs/task-execution-handoff.md b/docs/task-execution-handoff.md
index 5821175..c62dbe4 100644
--- a/docs/task-execution-handoff.md
+++ b/docs/task-execution-handoff.md
@@ -38,7 +38,9 @@ ServiceAccount principal, `flex-auth` audience and live expiry bounding the
operation deadline. This consumes the signed PDP's caller attestation; it does
not independently perform TokenReview or promote the proposed server mapping.
Human/delegation/projected-identity flags cannot substitute for this service seam.
-No service_actor_binding request propagation or source-policy activation is added.
+The later typed execution builder now propagates the fixed Access public
+service_actor_binding before approval issue; it is not caller authentication.
+No source-policy activation is added.
`as_owner_handoff()` returns a detached non-secret mapping with:
@@ -76,3 +78,36 @@ confirm original durable consumption using Approval's real provider client,
journal it, then recheck its exact Plan, owner sources, grant and deadlines before
any native operation. Neither a dataclass nor a caller-supplied boolean admits
execution. Existing generic CLI CAS behavior is otherwise unchanged.
+
+
+2026-10-16 retained-registry preapproval source preparation adds
+`prepare_task_approval(cfg, entry, execution_contract=contract)`. It calls the
+existing signed Check client with the original claim-free typed proposal and
+accepts only a signed DENY normalization record. This neither authorizes nor
+consumes an operation. No registry algorithm is copied and no registry is emptied.
+
+The detached `as_owner_handoff()` returns `original_request`,
+`submitted_request_digest`, authoritative `native_binding`,
+`approval_binding_digest`, normalization `decision_id`, `execution_contract`, and
+signed `registry_snapshot_digest`. Exact original context (including JSON kinds),
+submitted digest, recomputed evaluated digest, explicit approval-free digest,
+policy/key admission, service/caller identity, decision time and deadline are
+checked. Registry digest is evidence from the admitted signer; this method does
+not independently grant registry/config admission. The caller uses the returned
+native act and approval-free digest for normal Approval issuance and human entry.
+It preserves the original proposal; final authorize_task_execution builds that
+same proposal and attaches only the original issued claim. Access normalizes
+again and independently joins current source facts. Changed evaluated facts or
+native target refuse before CAS. A normalization DENY object/mapping is never
+accepted by consume_task_execution.
+
+Normalization decision_time must be valid and nonfuture; there is deliberately
+no invented standalone normalization TTL. The DENY receipt is not permission and
+cannot discharge final signed ALLOW or source validation. Expired original task/
+caller bounds refuse at preparation; a stale normalization's changed evaluated
+facts refuse when Access independently checks the original issued claim against
+the current retained registry. Unchanged historical normalization can only supply
+public issue metadata; it cannot itself produce a CAS/native admission. The actual
+source-server drift proof restarts Access with changed authoritative subject
+roles after preapproval and observes final refusal with no new ALLOW record, no
+CAS and unconsumed Approval state.
diff --git a/src/secrets_engine/task_execution_handoff.py b/src/secrets_engine/task_execution_handoff.py
index 5af36be..aa998ab 100644
--- a/src/secrets_engine/task_execution_handoff.py
+++ b/src/secrets_engine/task_execution_handoff.py
@@ -15,10 +15,11 @@ from threading import Lock
from secrets_engine.approval_auth import approval_token, credential_urlopen
from secrets_engine.approval_claim import binding_from_check_request, claim_binding_digest, validate_approval_claim
-from secrets_engine.approval_consume import authorize_action, consume_approval
+from secrets_engine.approval_consume import authorize_action, consume_approval, _expected_request
from secrets_engine.application_time import read_window, validity_bounds
-from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest
+from secrets_engine.authorization import validate_decision_envelope, request_digest, approval_binding_digest, CONTRACT_VERSION
from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json
+from secrets_engine.decision_check import check_decision
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
@@ -78,6 +79,93 @@ class TaskExecutionConsumption:
idempotent: bool
+def _same_json(left, right):
+ # Type-sensitive semantic correspondence only; never signature material.
+ return json.dumps(left,sort_keys=True,allow_nan=False)==json.dumps(right,sort_keys=True,allow_nan=False)
+
+
+def _task_identity(request,envelope,descriptor,upper):
+ binding=envelope['binding']
+ subject=binding['subject']; resource=binding['resource']
+ if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
+ or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
+ or subject.get('tenant') not in (None,'tenant:platform')
+ or 'principal' in subject.get('attributes',{})
+ or not _same_json(envelope['subject'],subject) or not _same_json(envelope['resource'],resource)
+ or descriptor['execution_actor']!='service:secrets-engine'
+ or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
+ or resource.get('tenant') not in (None,'tenant:platform')
+ or any(not _same_json(resource.get('attributes',{}).get(key),value)
+ for key,value in request['resource']['attributes'].items())
+ or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
+ _refuse()
+ caller=envelope['provenance'].get('caller')
+ if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
+ or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
+ or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
+ or upper >= _stamp(caller.get('not_after'))):
+ _refuse()
+
+
+@dataclass(frozen=True)
+class TaskApprovalPreparation:
+ """Detached public normalization facts; never authorization or consumption."""
+ _serialized: str
+
+ def as_owner_handoff(self):
+ return strict_json(self._serialized)
+
+
+def prepare_task_approval(cfg, entry, *, execution_contract):
+ """Read signed DENY normalization before Approval issue; never permit an act.
+
+ This read-only normalization record has no independent permission window.
+ decision_time must be a valid nonfuture instant; original operation/caller
+ deadlines remain live. Final source Check independently revalidates current
+ registry/source facts; stale normalized facts cannot replace that Check.
+
+ Use returned authoritative native_binding and approval_binding_digest for
+ issue, preserving original_request. Final Check carries only the original
+ issued claim; producer renormalization independently refuses registry drift.
+ """
+ if type(execution_contract) is not ExecutionContract:
+ _refuse()
+ request=_expected_request(cfg,entry,'execute-ccr',execution_contract=execution_contract)
+ window=read_window(cfg)
+ require_execution_deadline(request,time_window=window)
+ envelope=check_decision(base_url=str(cfg.pdp_url),token_file=cfg.pdp_token_file,request=request,
+ trust_file=cfg.pdp_trust_file,verifier=cfg.pdp_verifier)
+ try:
+ window=read_window(cfg);lower,upper=validity_bounds(window=window)
+ descriptor=execution_contract.as_dict();binding=envelope['binding'];provenance=envelope['provenance']
+ if (envelope.get('contract_version')!=CONTRACT_VERSION or envelope.get('effect')!='deny' or not isinstance(envelope.get('id'),str) or not envelope['id']
+ or provenance.get('policy_package')!=str(cfg.authorization_policy_package)
+ or provenance.get('policy_version')!=str(cfg.authorization_policy_version)
+ or not isinstance(provenance.get('registry_snapshot_digest'),str)
+ or re.fullmatch(r'sha256:[0-9a-f]{64}',provenance['registry_snapshot_digest']) is None
+ or request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
+ or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
+ or 'approval' in request['context'] or 'approval' in binding['context']
+ or not _same_json(binding.get('context'),request['context'])
+ or binding.get('submitted_request_digest')!=request_digest(request)
+ or binding.get('request_digest')!=request_digest(binding)
+ or binding.get('approval_binding_digest')!=binding['request_digest']
+ or not _same_json(envelope.get('subject'),binding['subject']) or not _same_json(envelope.get('resource'),binding['resource'])
+ or _stamp(provenance.get('decision_time'))>upper):
+ _refuse()
+ _task_identity(request,envelope,descriptor,upper)
+ require_execution_deadline(request,time_window=window,bounds=(provenance['caller']['not_after'],))
+ native=binding_from_check_request(binding)
+ claim_binding_digest(**native) # Require native JSON profile, not a translated digest.
+ result={'original_request':request,'submitted_request_digest':binding['submitted_request_digest'],
+ 'native_binding':native,'approval_binding_digest':binding['approval_binding_digest'],
+ 'decision_id':envelope['id'],'execution_contract':descriptor,
+ 'registry_snapshot_digest':provenance['registry_snapshot_digest']}
+ return TaskApprovalPreparation(json.dumps(result,sort_keys=True,separators=(',',':'),allow_nan=False))
+ except (ValueError,KeyError,TypeError,RecursionError,AttributeError) as exc:
+ raise DecisionError('task approval preparation malformed') from exc
+
+
def _validate(cfg, action, contract):
if type(contract) is not ExecutionContract or not action.artifacts:
_refuse()
@@ -108,31 +196,14 @@ def _validate(cfg, action, contract):
_refuse()
if (request['action']!='execute-ccr' or binding.get('action')!='execute-ccr'
or request['tenant']!='tenant:platform' or binding.get('tenant')!='tenant:platform'
- or binding.get('context')!=request['context']
- or request['context'].get('execution_contract')!=descriptor
+ or not _same_json(binding.get('context'),request['context'])
+ or not _same_json(request['context'].get('execution_contract'),descriptor)
or binding.get('request_digest')!=request_digest(binding)
or binding.get('approval_binding_digest')!=approval_binding_digest(binding)
or action.binding.request_digest!=validated.request_digest
or action.binding.decision_id!=validated.decision_id):
_refuse()
- subject=binding['subject']; resource=binding['resource']
- if (request['subject']!={'id':'secrets-engine','type':'service','tenant':'tenant:platform'}
- or subject.get('id')!='secrets-engine' or subject.get('type')!='service'
- or subject.get('tenant') not in (None,'tenant:platform')
- or 'principal' in subject.get('attributes',{})
- or envelope['subject']!=subject or envelope['resource']!=resource
- or descriptor['execution_actor']!='service:secrets-engine'
- or any(resource.get(key)!=request['resource'].get(key) for key in ('id','type','system'))
- or resource.get('tenant') not in (None,'tenant:platform')
- or any(resource.get('attributes',{}).get(key)!=value for key,value in request['resource']['attributes'].items())
- or any(key in binding['context'] for key in ('task_execution_projection','initiator','represented_principal','delegation','verified','owner_admission'))):
- _refuse()
- caller=envelope['provenance'].get('caller')
- if (not isinstance(caller,dict) or caller.get('mode')!='enforce'
- or caller.get('principal')!=_CALLER or caller.get('audience')!='flex-auth'
- or _stamp(caller.get('not_after')) < _stamp(descriptor['operation_deadline'])
- or upper >= _stamp(caller.get('not_after'))):
- _refuse()
+ _task_identity(request,envelope,descriptor,upper)
native=binding_from_check_request(binding)
supplied=claim['binding']
# Recompute the served native act too: Python equality alone conflates
diff --git a/tests/test_task_approval_preparation.py b/tests/test_task_approval_preparation.py
new file mode 100644
index 0000000..96b254f
--- /dev/null
+++ b/tests/test_task_approval_preparation.py
@@ -0,0 +1,89 @@
+"""Signed DENY supplies preapproval facts; it never supplies execution authority."""
+import copy
+import pytest
+from secrets_engine.authorization import request_digest
+from secrets_engine.errors import DecisionError
+from secrets_engine.task_execution_handoff import prepare_task_approval,consume_task_execution
+from tests.test_task_execution_handoff import lane,prepared
+from tests.signature_support import signature_tools
+
+
+def normalize(envelope):
+ envelope['effect']='deny'
+ envelope['provenance']['registry_snapshot_digest']='sha256:'+'4'*64
+ envelope['binding']['subject']['attributes']={'roles':['Operator'],'display_name':'registry service'}
+ envelope['binding']['request_digest']=request_digest(envelope['binding'])
+ envelope['binding']['approval_binding_digest']=envelope['binding']['request_digest']
+
+
+def test_signed_registry_normalization_is_detached_and_cannot_consume(lane):
+ cfg,e,contract,state=lane
+ state['decision_change']=normalize
+ result=prepare_task_approval(cfg,e,execution_contract=contract)
+ output=result.as_owner_handoff()
+ assert state['calls']==['check']
+ assert 'approval' not in output['original_request']['context']
+ assert output['approval_binding_digest']!=output['submitted_request_digest']
+ assert 'attributes' not in output['original_request']['subject']
+ assert output['execution_contract']==contract.as_dict()
+ assert output['native_binding']['target']==output['original_request']['resource']
+ output['original_request']['context']['purpose']='mutated'
+ assert result.as_owner_handoff()['original_request']['context']['purpose']!='mutated'
+ with pytest.raises(DecisionError):consume_task_execution(cfg,result)
+ assert state['calls']==['check']
+
+
+@pytest.mark.parametrize('case',['allow','context','descriptor-type','submitted','evaluated','approval-digest','caller','native','future-time','policy'])
+def test_signed_normalization_refuses_changed_correspondence(lane,case):
+ cfg,e,contract,state=lane
+ def change(envelope):
+ normalize(envelope)
+ if case=='allow':envelope['effect']='allow'
+ elif case=='context':envelope['binding']['context']['purpose']='other'
+ elif case=='descriptor-type':envelope['binding']['context']['execution_contract']['backend_budget']['ttl_seconds']=120.0
+ elif case=='submitted':envelope['binding']['submitted_request_digest']='sha256:'+'0'*64
+ elif case=='evaluated':envelope['binding']['request_digest']='sha256:'+'0'*64
+ elif case=='approval-digest':envelope['binding']['approval_binding_digest']='sha256:'+'0'*64
+ elif case=='caller':envelope['provenance']['caller']['mode']='disabled'
+ elif case=='native':envelope['binding']['resource']['system']='other'
+ elif case=='future-time':envelope['provenance']['decision_time']='2099-01-01T00:00:00Z'
+ else:envelope['provenance']['policy_version']='other'
+ state['decision_change']=change
+ with pytest.raises(DecisionError):prepare_task_approval(cfg,e,execution_contract=contract)
+ assert 'claim' not in state['calls'] and 'consume' not in state['calls']
+
+
+def test_signed_context_numeric_kind_cannot_change(lane):
+ cfg,e,contract,state=lane
+ state['decision_change']=lambda envelope:envelope['binding']['context']['execution_contract']['backend_budget'].__setitem__('ttl_seconds',120.0)
+ with pytest.raises(DecisionError):prepared(lane)
+ assert 'consume' not in state['calls']
+
+
+@pytest.mark.parametrize('original,served',[(1,True),(1,1.0),(1.0,1),(False,0)])
+def test_shared_original_target_subset_is_type_sensitive(lane,original,served):
+ from datetime import datetime,timedelta,timezone
+ from secrets_engine.task_execution_handoff import _task_identity
+ from secrets_engine.approval_consume import _expected_request
+ cfg,e,contract,state=lane
+ request=_expected_request(cfg,e,'execute-ccr',execution_contract=contract)
+ request['resource']['attributes']['requested_number']=original
+ binding=copy.deepcopy(request);binding['resource']['attributes']['requested_number']=served
+ envelope={'binding':binding,'subject':binding['subject'],'resource':binding['resource'],
+ 'provenance':{'caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine',
+ 'audience':'flex-auth','not_after':(datetime.now(timezone.utc)+timedelta(minutes=20)).isoformat()}}}
+ with pytest.raises(DecisionError):_task_identity(request,envelope,contract.as_dict(),datetime.now(timezone.utc))
+ binding['resource']['attributes']['requested_number']=original
+ binding['resource']['attributes']['legitimate_extra_registry_fact']=1.0
+ _task_identity(request,envelope,contract.as_dict(),datetime.now(timezone.utc))
+
+
+def test_old_normalization_record_has_no_permission_window(lane):
+ cfg,e,contract,state=lane
+ def old(envelope):
+ normalize(envelope)
+ envelope['provenance']['decision_time']='2000-01-01T00:00:00Z'
+ state['decision_change']=old
+ normalization=prepare_task_approval(cfg,e,execution_contract=contract)
+ with pytest.raises(DecisionError):consume_task_execution(cfg,normalization)
+ assert state['calls']==['check']
diff --git a/verifier/producer-source.json b/verifier/producer-source.json
index 9a4d5a3..d34b5a6 100644
--- a/verifier/producer-source.json
+++ b/verifier/producer-source.json
@@ -1,6 +1,6 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
- "revision": "B2/B3 R2 temporal source proposal over B1 R2; canonical native-prep base 919ce25",
+ "revision": "B2/B3 R3 retained-registry source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
"pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
@@ -13,8 +13,8 @@
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
},
"source_proposal": {
- "patch_sha256": "ed85a55b54f8380a657c0688d6f216699a0873b1b94b4c0c2e09332620c8ebf4",
- "predecessor_patch_sha256": "6079ed2ff40b131aa2c5efc8d33608ecb3475c8158f0486b44ec4469e9f222c1",
+ "patch_sha256": "46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6",
+ "predecessor_patch_sha256": "ed85a55b54f8380a657c0688d6f216699a0873b1b94b4c0c2e09332620c8ebf4",
"api_sign_bytes_unchanged": true,
"status": "source proposal; no runtime or key admission"
}
diff --git a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
index a8a3845..a7e1e3a 100644
--- a/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
+++ b/workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md
@@ -124,3 +124,19 @@ coercion/idempotence, expiry, signer removal and replay refusal. No service bind
propagation, policy gate flip, identity mapping admission or production trust/key
change was made. Independent source integration/review and actual whole-provider
proof remain required. T01/T03 and WP8-T06 retain their open native obligations.
+
+2026-10-16 retained-registry successor source preparation adds only the existing
+signed Check/DENY preapproval flow. The original typed service proposal is sent
+unchanged; normalized native act and evaluated approval-free digest from the
+admitted producer record are used for normal Approval issuance. The original
+proposal then receives only the original issued claim. Final Check re-normalizes
+against retained owner registry/source facts, so authoritative drift refuses
+before CAS. No local registry algorithm, new endpoint, empty-registry workaround
+or consumer digest waiver is used. Signed DENY preparation has no independent
+permission/freshness window and cannot consume or execute; current caller and
+operation bounds are checked, and final signed ALLOW/source checks remain
+mandatory. Exact original JSON kinds and requested-target subsets refuse
+bool/int/float substitution while allowing extra authoritative registry facts.
+Actual provider proof preparation is separately waiting on frozen Access source
+and Approval's timestamp/transaction-clock repair; source native/trust/custody and
+actual task admission remain open. T01/T03 and WP8-T06 are not closed.
diff --git a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
index 60f9e18..f65ac96 100644
--- a/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
+++ b/workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md
@@ -159,3 +159,19 @@ therefore refuse receipt-before-start even though live calls are correctly
ordered. The positive proof explicitly waits real time until the next whole
second before CAS, without injected clocks or relaxed checks. Fast-CAS failure
is retained for owner resolution; no Secrets runtime change conceals the seam.
+
+2026-10-16 retained-registry successor source preparation adds only the existing
+signed Check/DENY preapproval flow. The original typed service proposal is sent
+unchanged; normalized native act and evaluated approval-free digest from the
+admitted producer record are used for normal Approval issuance. The original
+proposal then receives only the original issued claim. Final Check re-normalizes
+against retained owner registry/source facts, so authoritative drift refuses
+before CAS. No local registry algorithm, new endpoint, empty-registry workaround
+or consumer digest waiver is used. Signed DENY preparation has no independent
+permission/freshness window and cannot consume or execute; current caller and
+operation bounds are checked, and final signed ALLOW/source checks remain
+mandatory. Exact original JSON kinds and requested-target subsets refuse
+bool/int/float substitution while allowing extra authoritative registry facts.
+Actual provider proof preparation is separately waiting on frozen Access source
+and Approval's timestamp/transaction-clock repair; source native/trust/custody and
+actual task admission remain open. T02 remains progress and T03 wait; no native adoption is claimed.

View file

@ -0,0 +1,82 @@
# PostgreSQL group journal source candidate
Prepared against NetKingdom `6cb43c0cc55212363a5406b02db046e2384a7c40` for
NK-WP-0048-T14. No canonical change, native database/credential access, package
installation, Docker call or production admission occurred in preparation.
`source.patch`, exact postimages and `source-manifest.json` bind the source.
The existing reserve/get/claim-revoke/save/check seam is now explicit as
`GroupJournalStore`. The new injection-only PostgreSQL implementation preserves
prestate, intents, confirmed creation inventories and active fences using the
same provisioner paths. SQL updates require the exact context/key/request and
receipt version, plus exactly one affected row; the journal head and exact
transition digest also advance conditionally under a metadata-row lock. Each
new digest binds the prior marker and canonical body/context/key/version. An
independent checkpoint compares and CAS-advances both sequence and content,
refusing a divergent clone even if it reaches the same counter. SQL errors, stale CAS and ambiguous
commit/checkpoint acknowledgment refuse rather than permitting the next LDAP
effect. No exception handler can automatically turn those failures into success.
A frozen explicit tenant/directory/case/source/principal/epoch binding is required;
there is no request-derived authority or default platform-root scope. The owner
must additionally supply independent durable continuity checkpoint callbacks and
renewable connection facts. These are injection seams, not new authorization or
evidence services, and no native implementation or grant is invented. The
ordinary server has no Pg environment switch: missing native callbacks keep the
production Pg group path disabled. The existing SQLite path remains local/test
only under accepted ADR-0015.
The migration creates tables inside the existing consumer schema. It performs
no provisioning, initializes no epoch and admits/clears no quarantine. A journal
restored behind the separately retained checkpoint is refused. Admission must
still bind native restore/continuity, record retention, owner scope and runtime
credentials; an injected callback that merely asserts success proves none of
those boundaries. SQL locking fences only cooperating journal callers and does
not close LLDAP external-writer/ABA races.
The Containerfile includes the module/migration and hash-pinned psycopg3 closure
selected from User Engine's existing lockfile (Linux amd64 CPython3.12 only).
No image was built and no package was fetched. Immutable rebuild, dependency
scanning, platform/cell/consumer acceptance and native storage/backup/lease/TLS
proof remain owner work. The native fixture uses the exact cached User Engine
image's bundled psycopg version and records it; this is compatibility proof,
not verification of a future rebuilt image.
Source checks cover explicit binding/no default, disabled server routing, stale
checkpoint/quarantine/epoch refusal, exact row CAS, commit-before-checkpoint
ordering, missing acknowledgment, cross-context/version import refusal,
active-fence/capacity and connection failure sanitization. Scripted DB-API unit
checks do not prove PostgreSQL semantics. The separately review-gated
`native-fixture` uses real disposable PostgreSQL SQL and the existing synthetic
LDAP Directory, with actual separate-process reopen and pg_dump/pg_restore.
No PostgreSQL fixture execution or native acceptance is counted yet.
The counter-only predecessor and coordinator finding remain under `review-r0`;
it is superseded and never admitted or executed. The successor adds content-bound
continuity rather than accepting a same-counter result.
The source deliverable does not finish T14: runtime package/operating assignment,
CNPG/rapp-postgres consumer/capacity and lease/custody approval, independently
retained native checkpoints, accepted case/source/principal and protected
session/evidence callbacks, LDAP writer fencing/CAS and production rollout all
remain open. Existing native-storage-proposal-r1 records those owner routes.
## R4 native failure refinement
R3 reached `bootstrap_ensure` then failed safely with RuntimeError and no SQLSTATE;
initdb/readiness/migration/runtime grants and checkpoint initialization completed.
Its exact internal exception text was never collected. Frozen fixture/receipt and
source503e3 remain in `native-fixture/revisions/r3` and `review-r3`.
Local diagnosis demonstrated a PostgreSQL save contract defect: save replaced the
nested receipt dictionaries while the real provisioner retained a group-state
reference across its intent save. The returned created ID therefore updated a
detached object; final receipt postimage checking refused. R4 updates only the
acknowledged `_pg_version` after successful commit and independent checkpoint;
it retains nested caller references, with no change to SQL/CAS, digest, ownership
or ambiguous-commit behavior. A regression fails against frozen source, and a
second runs the actual synthetic Directory/provisioner with actual PostgreSQL
save/write/transaction code and mocked SQL transport to verify complete ensure.
82 source tests pass. Actual native compatibility remains unproved until reviewed
R4 execution; this fix does not imply production or tenant admission.

View file

@ -0,0 +1,300 @@
{
"schema": "nk48.synthetic-postgres-journal-result.v1",
"status": "synthetic_postgres_journal_compatibility_passed",
"manifest_sha256": "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051",
"production_contacted": false,
"actual_credentials_used": false,
"native_acceptance_claimed": false,
"session": {
"run_id": "492a7afaec6b43ef92b98eb76025362f",
"containers": [
"nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-postgres",
"nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-probe"
],
"label": "net-kingdom.recovery-run=492a7afaec6b43ef92b98eb76025362f"
},
"driver_stage": "cleanup",
"rpc_trace": [
{
"stage": "anchor_launch",
"operation": "run",
"status": "completed"
},
{
"stage": "anchor_containment",
"operation": "context",
"status": "completed"
},
{
"stage": "anchor_containment",
"operation": "info",
"status": "completed"
},
{
"stage": "anchor_containment",
"operation": "container",
"status": "completed"
},
{
"stage": "anchor_containment",
"operation": "container",
"status": "completed"
},
{
"stage": "postgres_private_import",
"operation": "exec",
"status": "completed"
},
{
"stage": "postgres_initdb",
"operation": "exec",
"status": "completed"
},
{
"stage": "postgres_launch",
"operation": "exec",
"status": "completed"
},
{
"stage": "anchor_identity",
"operation": "container",
"status": "completed"
},
{
"stage": "helper_launch",
"operation": "run",
"status": "completed"
},
{
"stage": "full_containment",
"operation": "context",
"status": "completed"
},
{
"stage": "full_containment",
"operation": "info",
"status": "completed"
},
{
"stage": "full_containment",
"operation": "container",
"status": "completed"
},
{
"stage": "full_containment",
"operation": "container",
"status": "completed"
},
{
"stage": "full_containment",
"operation": "container",
"status": "completed"
},
{
"stage": "helper_interpreter",
"operation": "exec",
"status": "completed"
},
{
"stage": "candidate_import",
"operation": "exec",
"status": "completed"
},
{
"stage": "bootstrap",
"operation": "exec",
"status": "completed"
},
{
"stage": "database_dump",
"operation": "exec",
"status": "completed"
},
{
"stage": "restart",
"operation": "exec",
"status": "completed"
},
{
"stage": "concurrency",
"operation": "exec",
"status": "completed"
},
{
"stage": "fork",
"operation": "exec",
"status": "completed"
},
{
"stage": "lost-ack",
"operation": "exec",
"status": "completed"
},
{
"stage": "database_restore",
"operation": "exec",
"status": "completed"
},
{
"stage": "restored",
"operation": "exec",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "rm",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "rm",
"status": "completed"
},
{
"stage": "cleanup",
"operation": "container",
"status": "completed"
}
],
"postgres_initdb_complete": true,
"containment_verified_before_generated_private_import": true,
"helper_interpreter_verified_before_candidate_import": true,
"helper_psycopg_version": "3.3.6",
"checks": {
"only_loopback_no_external_routes": true,
"actual_postgresql_journal_ensure": true,
"explicit_scope_context_retained": true,
"confirmed_inventory_excludes_preexisting": true,
"runtime_ddl_denied": true,
"separate_process_postgres_restart_and_revoke": true,
"preexisting_shared_state_preserved": true,
"foreign_context_receipt_refused": true,
"concurrent_original_reservation_exactly_once": true,
"stale_version_update_refused_without_head_advance": true,
"concurrent_divergent_clones_same_counter_verified": true,
"independent_exact_transition_cas_selects_one_winner": true,
"reopened_loser_clone_refused_at_same_counter": true,
"lost_checkpoint_ack_retains_pending_reservation": true,
"reopened_uncertainty_refuses_ldap_effects": true,
"active_fence_survives_process_reopen": true,
"lost_synthetic_directory_response_retains_postgres_intent": true,
"lost_sql_commit_reply_retains_committed_head": true,
"reopened_stale_checkpoint_refuses_continuation": true,
"actual_pg_restore_behind_checkpoint_detected": true,
"restored_store_cannot_gain_replay_authority": true
},
"helper_diagnostics": {
"bootstrap": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"bootstrap_connect_wait",
"bootstrap_connected",
"bootstrap_schema",
"bootstrap_migration",
"bootstrap_epoch_insert",
"bootstrap_role_create",
"bootstrap_runtime_grants",
"bootstrap_checkpoint_init",
"bootstrap_directory_init",
"bootstrap_ensure",
"bootstrap_ensure_readback",
"bootstrap_runtime_ddl_refusal",
"bootstrap_complete",
"phase_complete"
]
},
"restart": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"restart_load_snapshot",
"restart_reopen_store",
"restart_replay_ensure",
"restart_revoke",
"restart_revoke_readback",
"restart_prestate_equality",
"restart_foreign_context",
"restart_complete",
"phase_complete"
]
},
"concurrency": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"phase_complete"
]
},
"fork": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"phase_complete"
]
},
"lost-ack": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"phase_complete"
]
},
"restored": {
"status": "passed",
"stages": [
"probe_entry",
"loopback_check",
"phase_dispatch",
"phase_complete"
]
}
},
"psycopg_version": "3.3.6",
"container_cleanup_obligations": [
{
"container": "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-probe",
"status": "absence_verified"
},
{
"container": "nk49-kc-492a7afaec6b43ef92b98eb76025362f-journal-postgres",
"status": "absence_verified"
}
],
"container_cleanup_verified": true
}

View file

@ -0,0 +1,32 @@
{
"schema": "nk48.postgres-source-integration-review.v1",
"status": "prepared_for_coordinator_integration",
"canonical_head_observed": "6cb43c0cc55212363a5406b02db046e2384a7c40",
"base_revision": "6cb43c0cc55212363a5406b02db046e2384a7c40",
"source_patch_sha256": "5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d",
"source_paths": [
"docs/service-group-provenance.md",
"identity-provisioner/Containerfile",
"identity-provisioner/group_provenance.py",
"identity-provisioner/group_provenance_postgres.py",
"identity-provisioner/migrations/group_journal_postgres_v1.sql",
"identity-provisioner/provisioner.py",
"identity-provisioner/requirements-postgres.txt",
"identity-provisioner/tests/test_group_postgres.py"
],
"source_hashes": "../source-manifest.json exact pre/postimages verified",
"source_paths_count": 8,
"workplan_patch_sha256": "af9f720fba1b931679f866a29e8b3bc7d08f24a7f653db48daf485e35cb16eea",
"workplan_preimage_sha256": "08215f6dc91a89706bf456a897d77de4032d47a21cd0252d202249619def135d",
"combined_source_workplan_apply_check": true,
"bounded_SQL_compatibility": "passed_R5",
"raw_safe_receipt_sha256": "d72cc5e0788b7be69fc40c641e20680d29a175181334396b5f7fcecc0c0763d9",
"source_tests": 82,
"fixture_plan_tests": 13,
"snapshot_tests": 2,
"native_admission_claimed": false,
"source_backend_unchanged_since_reviewed_R4": true,
"canonical_worker_mutations": false,
"remaining_gate": "T14progress: actual owner/package, CNPG+lease/TLS/capacity/recovery admission; authoritative protected binding and independently retained continuity callbacks; session/evidence/revocation; all LDAP writers fenced/CAS/ABA; production activation",
"publication_projection_cleanup_followthrough": "coordinator separate receipts"
}

View file

@ -0,0 +1,45 @@
{
"schema": "nk48.postgres-journal-compatibility-evidence.v1",
"status": "bounded_disposable_compatibility_passed",
"source_patch_sha256": "5549d2982b61ff70aba4b3928fecd5639db0f1a732ab2f8630f678e7acf4d34d",
"manifest_sha256": "d51f90816616848b93bcdc482e10a98d07061deb01eeed9e50700fc6e68b6051",
"raw_safe_receipt_sha256": "d72cc5e0788b7be69fc40c641e20680d29a175181334396b5f7fcecc0c0763d9",
"original_safe_receipt": "/tmp/nk48-postgres-native-review-sguj7t8p/result.json",
"phase_oracle_sets_exact": true,
"all_declared_oracles_true": true,
"fixture_reported_cleanup_absence": true,
"independent_coordinator_cleanup_verification": "pending separate coordinator evidence",
"source_checks": {
"passed": 82,
"seconds": 8.46
},
"fixture_plan_checks": {
"passed": 13,
"seconds": 0.019
},
"snapshot_regressions": {
"passed": 2,
"seconds": 0.236
},
"counts_are_native_acceptance": false,
"helper_bundled_psycopg": "3.3.6",
"production_packaged_psycopg": "3.3.5 (pinned closure, not image built here)",
"failed_predecessors": [
"native-fixture/revisions/r2",
"native-fixture/revisions/r3",
"native-fixture/revisions/r4"
],
"refinements": [
"R1 helper cache preflight failed before execute; no raw stderr byte file exists",
"R2 failed helper outcome not retained by old driver; precise failure unknown",
"R3 diagnostics localized ensure failure; local source regression proved detached nested receipt state in Pg save, fixed version-only acknowledgment",
"R4 actual bootstrap verified source fix; restart failed; local fixture regression proved group map alias and empty orphan backing sets; corrected snapshot retains complete current visible prestate and refuses nonempty orphans"
],
"not_claimed": [
"CNPG native storage/admission/HA/backup/lease/TLS/placement",
"authoritative tenant/case/source/principal bindings and independently retained continuity callbacks",
"PDP/session/evidence/revocation owner acceptance",
"all LDAP writer fencing/CAS/ABA exclusion",
"production endpoint activation or owner assent"
]
}