Require declared human control in factory credential delivery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 20:16:55 +02:00
parent d1c13b5dd6
commit 2b0d04e8e1
11 changed files with 451 additions and 27 deletions

View file

@ -41,6 +41,7 @@ delivery_auth:
token_num_uses: 8
approval:
model: ccr
human_control: true
decision_ref: CCR-2026-0016
notes: Custody only has been completed. Native apply and exec require durable exact-action
authorization, engine consume, scoped backend authority and verified delivery

View file

@ -86,9 +86,50 @@ authority is approval-engine (checked as `issuer`); the decision's is flex-auth.
Every live privileged production handler passes `_require_lane_approval`,
which calls `require_production_consume` before `OpenBaoClient.resolve`.
Dry-run and `plan` do not consume. Build/test remain fail-open relative to
approval-engine. The three-factor unsafe-demo exception is not a consume
path.
Dry-run and `plan` do not consume. For undeclared lanes, build/test and the
three-factor unsafe-demo exception retain their existing behavior. An explicitly
declared human control requires observation, PDP ALLOW and CAS consumption at
every stage; no demo or lane-review fallback discharges it.
## Declared human-control workflow
The factory lane explicitly declares `approval.human_control: true`. This
implements its existing human action-review requirement and GH-DEC-2026-016;
it does not infer human control from a CCR model, stage, actor, or approver entry,
and does not make a local authorization decision. Other catalog entries default
to undeclared. The catalog value must be a boolean, and `bootstrap-only` cannot
be combined with a declared human control.
The generated CheckRequest includes `context.human_control: true`. This changes
its evaluator-bound identity. The separate request-creation caller must issue an
unapproved Approval Engine object with **top-level `human_control: true`** and
`pdp_path: true`, using the actual evaluator-origin digest of that exact claim-free
request. A human subsequently supplies `/entries` through the admitted approver
surface. Do not copy an old request digest or retrofit an existing approval.
Approval Engine retains its unchanged five-field native act digest; the consumer
joins through the distinct evaluator-origin PDP digest as before.
At observation, this engine requires **`claim.binding.human_control is true`**
in addition to current validity, freshness and the PDP path. Missing, false,
string or numeric declarations refuse before the PDP call. The declaration is
checked again after PDP validation and carried in the internal consume binding
and non-secret `approval_human_control` evidence. A declared lane cannot skip
consumption through a build/test or unsafe-demo fallback. The consumer does not
reconstruct approver identities or counts: Approval Engine owns that evidence.
The repeatable `tools/exercise_approval_identity.py --exec-owner --human-control`
exercise creates a declared request through the actual engine API, proves a real
KeyCape service JWT cannot bind it, proves an undeclared claim cannot reach the
PDP/consume/backend path, and proves the real evaluator preserves the requirement.
The positive human entry is **seeded in the disposable store**, while requester,
claim and consume use real local service authentication. The backend and child
payload are synthetic; this is not human JWT/PKCE or live custody evidence.
See [the component receipt](evidence/2026-09-10-human-control-consumer-exercise.json).
Native narrow requester identity, browser human approval, admitted custody and
current deployed services remain APPROVAL-WP-0002-T01/T03/T05,
INFD-WP-0001-T07/T08 and SECRETS-WP-0009-T03. The factory catalog's exec owner
remains pending; adding this requirement grants no runtime or credential access.
## Fail closed
@ -110,8 +151,9 @@ digest, idempotence, and consumed-at only. No token, secret, or accessor.
## Required configuration
The join is absent by default, so an unconfigured engine behaves exactly as it
did before. Production additionally needs:
The join is absent by default. Undeclared lanes retain their existing behavior;
a lane declaring `approval.human_control: true` refuses an absent join.
Production and declared human-control paths need:
| Variable | Meaning |
| --- | --- |

View file

@ -127,3 +127,16 @@ Before accepting an existing production lane:
lanes. Set `false` when the consumer needs only the KV data GET; generated
policy omits the metadata endpoint. Owner metadata verification then needs
separate operator authority, not broader consumer access.
## Explicit human-control requirement
`approval.human_control` is an optional boolean, defaulting to undeclared. Set it
to `true` only for a workflow whose existing contract requires human review. It
is not inferred from `approval.model`, stage or principal type. A declared lane
cannot use `bootstrap-only`; its live action requires an Approval Engine claim
explicitly declared human-controlled, current PDP ALLOW and CAS consumption.
Missing services cannot fall back to ordinary lane review or an unsafe demo.
See [the requester and consumer contract](approval-consumption.md#declared-human-control-workflow).
The HelixForge native-delivery catalog declares this requirement and remains
pending owner admission. No OpenBao policy, role, credential or readiness state
is activated by this source declaration.

View file

@ -0,0 +1,80 @@
{
"schema_version": 1,
"target": "disposable local processes; synthetic credentials",
"started_at": "2026-09-10T18:15:35.247006+00:00",
"keycape_image": "forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611",
"approval_engine_commit": "c8f85c6d7691f2319ad462791e6d385515d7e70d",
"flex_auth_commit": "88b354377c8e26b162f1234e673072f1c06dcd89",
"keycape_contract_commit": "f9812ab3b2bfe8f0817185f44071e612264ec3ee",
"consumer_source_sha256": {
"approval_auth.py": "6f3b033e7928e1c527bc19f5a1e01bf243540769a8646bd8cd47268da25637b5",
"approval_consume.py": "4515c74dbe1cea030cdccebf17b27642ad4f67f2aaee4f3a87e4bb2b9cfad295",
"approval_claim.py": "20b728d2d3e14ffc89fa21a137716015875f9a05c6af7ff48d07cf48d89503a9",
"authorization.py": "c2b6679b4235611c4fd39dad8964a522378df42c0658f0a02dcc31f05141d6a0",
"config.py": "2f2f1b60664d89bbb806aed0c768378923062077740e1f96e4c359238eb06955",
"service_auth.py": "653723ef5babce2157771416d93cb15cdbb7caca3c45509c3730520c55e4515c",
"cli.py": "1147d0e60850cfc38ee21e8564f51656d364ddc3cd8d6353a0c2dd6a0b78b8ec",
"catalog.py": "247c29314bcf86b3439d756ed911df62fb43c3c498b18ce5403704986a501b18",
"exec_owner.py": "10c21e7e37dd3d0aad41c4bb15df8f4a69c22d9046105a981a9613a91cd4c145",
"exec_delivery.py": "b142b60d875acf3dea1ba3ae99182a7ed7328d955dee5be2d12c977fc5646e8e",
"routing.py": "9d5050d7494a207394ce8251d8aac574ca0c08e3252639b5f05b373d6e35a076"
},
"limitations": [
"standalone Flex Auth source, not deployed pin",
"local Approval Engine source, not deployed image",
"no live custody or client-side read grant",
"no OpenBao effect or model execution",
"positive human entry seeded in disposable store; no human JWT/PKCE proof"
],
"checks": {
"operator_issued_and_approved_via_verified_jwt": true,
"wrong_action_refused_before_consume": true,
"producer_origin_join_with_carried_claim": true,
"actual_consumer_claim_check_consume": true,
"same_digest_retry_idempotent": true,
"different_digest_refused": true,
"spent_claim_refused": true,
"operator_consume_scope_denied_by_issuer": true,
"wrong_secret_refused": true,
"no_access_token_file_created": true,
"registry_override_accepts_exact_submission": true,
"same_enriched_result_different_submission_refused": true,
"real_dual_control_denial_then_claim_check_consume": true,
"declared_control_refuses_real_keycape_service_bind": true,
"undeclared_control_refused_before_real_pdp_consume_backend": true,
"exec_owner_substitution_refused_before_consume_backend": true,
"exec_owner_changed_environment_replay_refused_by_real_pdp_join": true,
"exec_owner_real_cli_consumes_before_fixture_backend": true,
"exec_owner_actual_child_excludes_parent_credentials": true,
"exec_owner_digest_preserved_by_real_evaluator": true,
"declared_human_intent_preserved_by_real_evaluator": true,
"declared_human_fixture_consumed_before_owner_delivery": true
},
"flex_auth_binary_sha256": "c7a1f35aa2cd7cf2733272923ace7fc4b4e55171db4ef8f3d0d6629183dcf1fa",
"producer_input_sha256": {
"policy_package.md": "657fa9312c9dcabee059ec31c89ee13396fe9aa54d852edc9e6559a74bbc139a",
"registry_snapshot.json": "37fba44ec0e0fd9b1e17a05ad1b7e4f9bb6ec9e0fc0bb3133e8af2ac6f832d7c"
},
"exec_owner_scope": "Synthetic recipient/backend with real KeyCape, Approval Engine, Flex Auth and Secrets Engine CLI; not native custody or human approval proof",
"status": "passed",
"cleanup_complete": true,
"finished_at": "2026-09-10T18:15:47.092615+00:00",
"consumer_base_commit": "d1c13b5dd667bcad453c94564f65bdf5f172ba3b",
"source_suite": {
"passed": 388,
"new_cases": 27,
"first_25_cases_failed_before": 25,
"log_sha256": "e676747dc0f2547e42bf0f3d7f97a3e1a6da4d413428b44afe28d725abd7a4c7"
},
"factory_attempts": 0,
"paid_model_calls": 0,
"native_human_approval_proof": false,
"remaining_records": [
"SECRETS-WP-0009-T03",
"APPROVAL-WP-0002-T01/T03/T05",
"INFD-WP-0001-T07/T08",
"RPF-WP-0035-T06 / CCR-2026-0019",
"HFACT-WP-0001-T01/T03/T04/T05"
],
"command": "python tools/exercise_approval_identity.py --keycape-source PATH --approval-engine-source PATH --flex-auth-source PATH --exec-owner --human-control --receipt NEW_PATH"
}

View file

@ -102,6 +102,7 @@ def _validate_observation(
claim: object,
*,
approval_id: str,
require_human_control: bool = False,
now: datetime | None = None,
) -> dict[str, Any]:
"""Validate issuer, shape, validity and freshness; no action correspondence."""
@ -135,6 +136,8 @@ def _validate_observation(
binding = claim.get("binding")
if not isinstance(binding, dict):
raise DecisionError("approval claim binding must be an object")
if require_human_control and binding.get("human_control") is not True:
raise DecisionError("approval claim does not declare binding.human_control true; request a human-controlled approval at issue")
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
freshness = claim.get("freshness")
if not isinstance(freshness, dict):
@ -154,13 +157,15 @@ def _validate_observation(
def observe_pdp_approval_claim(claim: object, *, approval_id: str,
require_human_control: bool = False,
now: datetime | None = None) -> dict[str, Any]:
"""Validate a fresh fact for submission to the PDP, not authority to consume.
Action correspondence cannot be established until the evaluator returns its
enriched approval binding. This function deliberately makes no such claim.
"""
observed = _validate_observation(claim, approval_id=approval_id, now=now)
observed = _validate_observation(claim, approval_id=approval_id,
require_human_control=require_human_control, now=now)
binding = observed["binding"]
if binding.get("pdp_path") is not True:
raise DecisionError("approval claim does not declare binding.pdp_path; request an approval bound at issue")
@ -172,15 +177,18 @@ def observe_pdp_approval_claim(claim: object, *, approval_id: str,
def validate_approval_claim(claim: object, *, approval_id: str,
expected_binding_digest: str = "", expected_pdp_digest: str = "",
require_human_control: bool = False,
now: datetime | None = None) -> dict[str, Any]:
"""Validate the fact and compare an independently supplied binding.
A PDP digest supplied here must come from the evaluated decision, never a
local reconstruction of the unenriched request.
"""
observed = _validate_observation(claim, approval_id=approval_id, now=now)
observed = _validate_observation(claim, approval_id=approval_id,
require_human_control=require_human_control, now=now)
if expected_pdp_digest:
observe_pdp_approval_claim(observed, approval_id=approval_id, now=now)
observe_pdp_approval_claim(observed, approval_id=approval_id,
require_human_control=require_human_control, now=now)
if observed["binding"]["pdp_digest"] != expected_pdp_digest:
raise DecisionError("approval claim pdp digest does not match the request")
elif expected_binding_digest:

View file

@ -20,6 +20,7 @@ from urllib.request import Request, urlopen
from secrets_engine.approval_auth import approval_auth_configured, approval_token, credential_urlopen
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
from secrets_engine.authorization import (
build_action_request,
@ -40,6 +41,7 @@ class ConsumeBinding:
approval_id: str
request_digest: str
decision_id: str = ""
human_control: bool = False
@dataclass(frozen=True)
@ -68,6 +70,7 @@ class AuthorizedAction:
"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,
}
@ -222,13 +225,17 @@ def resolve_approval_observation(
) -> ApprovalObservation | None:
"""Observe a fresh fact and prepare the claim-bearing request (step 1).
Missing serving coordinates return None, preserving the production refusal.
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.
"""
base_url = str(getattr(cfg, "approval_url", "") or "")
auth_configured = approval_auth_configured(cfg)
authorization_id = _authorization_id(entry, decision)
required_human_control = 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 = _expected_request(
@ -242,7 +249,8 @@ def resolve_approval_observation(
authorization_id=authorization_id,
opener=opener or credential_urlopen,
)
observe_pdp_approval_claim(claim, approval_id=authorization_id)
observe_pdp_approval_claim(claim, approval_id=authorization_id,
require_human_control=required_human_control)
expected_request["context"]["approval"] = claim
return ApprovalObservation(authorization_id, expected_request, claim)
@ -310,6 +318,7 @@ def authorize_action(
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,
)
if validated.action != action:
raise DecisionError("access-engine decision does not bind this action")
@ -318,6 +327,7 @@ def authorize_action(
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_id=validated.decision_id,
expires_at=validated.expires_at,
@ -413,16 +423,21 @@ def require_production_consume(
evidence: Any = None,
opener: Callable[..., Any] | None = None,
) -> ConsumedApproval | None:
"""CAS-consume before a production OpenBao call. No-op off the prod path.
"""CAS-consume before production or an explicitly human-controlled action.
Build/test remain fail-open relative to approval-engine. The three-factor
unsafe-demo exception is not a consume path. Missing binding, URL, or
credential fail closed so a stance bypass cannot reach OpenBao.
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.
"""
if getattr(entry, "stage", "") != "prod":
return None
if demo_exception_enabled(cfg):
return None
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 "

View file

@ -14,7 +14,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from secrets_engine.catalog import CatalogEntry
from secrets_engine.catalog import CatalogEntry, human_control_required
from secrets_engine.errors import DecisionError
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@ -105,6 +105,10 @@ def build_action_request(
"context": {"purpose": purpose},
}
)
if human_control_required(entry):
# Bind the requested control to the exact PDP submission. This is a
# requirement on the observed approval, not an authorization verdict.
request["context"]["human_control"] = True
if action == "exec":
from secrets_engine.exec_owner import owner_digest

View file

@ -199,6 +199,15 @@ class CatalogEntry:
return self.kind == "kv"
def human_control_required(entry: Any) -> bool:
"""Declared workflow requirement; never inferred from stage or approvers."""
approval = getattr(entry, "approval", {})
value = approval.get("human_control", False)
if not isinstance(value, bool):
raise CatalogError("approval.human_control must be a boolean")
return value
def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> CatalogEntry:
"""Validate a raw mapping and return a CatalogEntry, or raise CatalogError."""
if not isinstance(data, dict):
@ -339,6 +348,12 @@ def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> Catalog
f"allowed {VALID_APPROVAL_MODELS}"
)
human_control = approval.get("human_control", False)
if not isinstance(human_control, bool):
raise CatalogError(f"{source}: approval.human_control must be a boolean")
if human_control and approval["model"] == "bootstrap-only":
raise CatalogError(f"{source}: human_control cannot use bootstrap-only approval")
risk = data.get("risk", {})
if not isinstance(risk, dict):
raise CatalogError(f"{source}: risk must be a mapping")

170
tests/test_human_control.py Normal file
View file

@ -0,0 +1,170 @@
"""A declared human-control lane cannot use an undeclared approval fact."""
import copy
import json
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
from secrets_engine import cli
from secrets_engine.approval_consume import (
ConsumeBinding, authorize_action, require_production_consume,
)
from secrets_engine.authorization import build_action_request, request_digest
from secrets_engine.catalog import load_entry, validate_entry
from secrets_engine.errors import CatalogError, DecisionError
from tests.authorization_stub import AuthorizationStub
from tests.test_catalog import VALID
from tests.test_consume_binding_join import (
AUTH_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
)
from tests.test_exec_owner import bound
from tests.test_privileged_cli_evidence import _config, _records
def controlled_entry():
entry = _entry()
entry.approval.update(model="decision", human_control=True)
return entry
@pytest.mark.parametrize("value", [None, 0, 1, "true", "false", [], {}])
def test_catalog_declaration_is_strictly_boolean(value):
raw = copy.deepcopy(VALID)
raw["approval"].update(model="decision", human_control=value)
with pytest.raises(CatalogError, match="human_control"):
validate_entry(raw)
def test_bootstrap_only_cannot_declare_a_human_control():
raw = copy.deepcopy(VALID)
raw["approval"]["human_control"] = True
with pytest.raises(CatalogError, match="human_control"):
validate_entry(raw)
@pytest.mark.parametrize("value", ["missing", False, None, 1, "true", [], {}])
def test_undeclared_or_malformed_claim_refuses_before_pdp(tmp_path, value):
entry = controlled_entry()
claim = _served(entry)
if value != "missing":
claim["binding"]["human_control"] = value
claim["entries"] = [{"subject_id": "human:fixture", "principal_type": "human"}]
with pytest.raises(DecisionError, match="human_control"):
authorize_action(_Cfg(_token(tmp_path)), entry, "deactivate", fields=("api_token",),
opener=_opener(claim), pdp_opener=lambda *a, **k: pytest.fail("must not call PDP"))
def test_human_control_intent_changes_the_submitted_request_binding():
ordinary = _entry()
controlled = controlled_entry()
def request(entry):
return build_action_request(entry, "deactivate", subject_id="service:fixture",
subject_type="Service", purpose="same-action", fields=("api_token",))
before, after = request(ordinary), request(controlled)
assert "human_control" not in before["context"]
assert after["context"]["human_control"] is True
assert request_digest(before) != request_digest(after)
def test_declared_fact_yields_a_human_control_consume_binding(tmp_path):
entry = controlled_entry()
claim = _served(entry)
claim["binding"]["human_control"] = True
authorized = _resolve(_Cfg(_token(tmp_path)), entry, claim)
assert authorized.binding.human_control is True
assert authorized.as_evidence()["approval_human_control"] is True
def test_legacy_lane_still_accepts_undeclared_claim(tmp_path):
entry = _entry()
result = _resolve(_Cfg(_token(tmp_path)), entry, _served(entry))
assert result is not None
assert result.binding.human_control is False
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_missing_serving_path_cannot_fall_back_to_lane_or_demo_approval(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
authorize_action(_Cfg(None, approval_url="", approval_token_file=None), entry, "exec")
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_consume_requires_observed_human_control_even_with_demo_enabled(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
require_production_consume(_config(tmp_path), entry,
binding=ConsumeBinding(AUTH_ID, "sha256:"+"a"*64),
opener=lambda *a, **k: pytest.fail("must not consume"))
@pytest.mark.parametrize("stage", ["test", "prod"])
def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch, stage):
entry = replace(controlled_entry(), stage=stage)
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path))
binding = ConsumeBinding(AUTH_ID, "sha256:"+"a"*64, human_control=True)
calls = []
def consume(request, **kwargs):
calls.append(request.full_url)
response = _opener({"status": "consumed", "request_digest": binding.request_digest})(request)
response.getcode = lambda: 200
return response
result = require_production_consume(cfg, entry, binding=binding, opener=consume)
assert result.approval_id == AUTH_ID
assert calls == [f"https://approval.example/v1/approvals/{AUTH_ID}/consume"]
def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
from secrets_engine import approval_consume
entry = controlled_entry()
claim = _served(entry)
claim["binding"]["human_control"] = True
monkeypatch.setattr(approval_consume, "fetch_approval_claim", lambda **k: claim)
cfg = _Cfg(_token(tmp_path))
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package,
version=cfg.authorization_policy_version)
original_validate = approval_consume.validate_decision_envelope
def invalidate_after_check(*args, **kwargs):
result = original_validate(*args, **kwargs)
claim["binding"]["human_control"] = False
return result
monkeypatch.setattr(approval_consume, "validate_decision_envelope", invalidate_after_check)
def check(request, **kwargs):
return _opener(pdp.decision(json.loads(request.data)))(request)
with pytest.raises(DecisionError, match="human_control"):
authorize_action(cfg, entry, "deactivate", fields=("api_token",),
policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,), pdp_opener=check)
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=AUTH_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"]))
records = _records(tmp_path)
assert [r["result"] for r in records] == ["attempt", "failed-DecisionError"]
def test_factory_catalog_declares_human_control_and_keeps_owner_pending():
entry = load_entry(Path(__file__).resolve().parents[1]/"catalog/glas-claude-agent-dev-anthropic.yaml")
assert entry.approval["human_control"] is True
assert entry.delivery_config["exec_owner"]["status"] == "pending"

View file

@ -355,15 +355,40 @@ def exercise(args):
"files": {str(p): {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "private": True}
for p in [executable, script]},
}}
if args.human_control:
raw["approval"].update(model="decision", human_control=True,
decision_ref="synthetic-human-lane-review")
owner_entry = validate_entry(raw)
req = _expected_request(cfg, owner_entry, "exec", fields=("api_token",))
owner_request_digest = pdp.bind_request(req)
operator_request("/v1/approvals", {
"id": "synthetic-owner-delivery", "binding": {"action": "secrets.exec", "target": {"id": owner_entry.id, "stage": "prod"},
"actor": "service:approval-engine-operator", "principal": "synthetic-operator", "purpose": "disposable identity proof"},
"validity": {"not_before": (now - timedelta(minutes=1)).isoformat(), "expires_at": (now + timedelta(minutes=10)).isoformat()},
"pdp_digest": owner_request_digest, "pdp_path": True}, "approval:create")
operator_request("/v1/approvals/synthetic-owner-delivery/entries", {}, "approval:approve")
def issue_owner(ident, human_control):
# The requester declares intent on an unapproved object;
# only the subsequent human bind discharges the control.
return operator_request("/v1/approvals", {
"id": ident, "binding": {"action": "secrets.exec", "target": {"id": owner_entry.id, "stage": "prod"},
"actor": "service:approval-engine-operator", "principal": "synthetic-operator", "purpose": "disposable identity proof"},
"validity": {"not_before": (now - timedelta(minutes=1)).isoformat(), "expires_at": (now + timedelta(minutes=10)).isoformat()},
"pdp_digest": owner_request_digest, "pdp_path": True,
"human_control": human_control}, "approval:create")
issue_owner("synthetic-owner-delivery", args.human_control)
if args.human_control:
try:
operator_request("/v1/approvals/synthetic-owner-delivery/entries", {}, "approval:approve")
except HTTPError as error:
assert error.code == 403
assert engine.get("synthetic-owner-delivery").entries == []
else:
raise AssertionError("service bound a declared human control")
# Explicit local fixture: the positive human entry is seeded
# through the store, not a native human login/JWT claim.
engine.add_entry("synthetic-owner-delivery", "fixture:human",
principal_type="human", evidence_ref="synthetic-store-fixture")
issue_owner("synthetic-owner-undeclared", False)
operator_request("/v1/approvals/synthetic-owner-undeclared/entries", {}, "approval:approve")
receipt["checks"]["declared_control_refuses_real_keycape_service_bind"] = True
receipt["limitations"].append("positive human entry seeded in disposable store; no human JWT/PKCE proof")
else:
operator_request("/v1/approvals/synthetic-owner-delivery/entries", {}, "approval:approve")
backend_calls = []
class Backend:
@contextmanager
@ -380,7 +405,23 @@ def exercise(args):
yield Backend()
command = raw["delivery_config"]["exec_owner"]["command"]
cli_args = SimpleNamespace(catalog=owner_entry.id, field="api_token", mode="exec-env", command=command)
with patch.object(cli, "get_entry", return_value=owner_entry), patch.object(cli, "_open_backend", open_fixture_backend):
from secrets_engine.decisions import Decision
lane_review = Decision("synthetic-human-lane-review", "Synthetic lane review", "approved", None, "local-fixture")
with patch.object(cli, "get_entry", return_value=owner_entry), patch.object(cli, "_open_backend", open_fixture_backend), patch.object(cli, "resolve_decision", return_value=lane_review):
if args.human_control:
owner_entry.approval["authorization_id"] = "synthetic-owner-undeclared"
before_checks = len(pdp.calls)
try:
cli.cmd_exec(cfg, cli_args)
except DecisionError as error:
assert "human_control" in str(error)
assert not backend_calls and len(pdp.calls) == before_checks
assert not engine.claim("synthetic-owner-undeclared")["consumed"]
else:
raise AssertionError("undeclared control reached the protected action")
finally:
owner_entry.approval["authorization_id"] = "synthetic-owner-delivery"
receipt["checks"]["undeclared_control_refused_before_real_pdp_consume_backend"] = True
substituted = SimpleNamespace(**vars(cli_args))
substituted.command = ["/bin/echo", "substitute"]
try:
@ -411,6 +452,11 @@ def exercise(args):
"exec_owner_actual_child_excludes_parent_credentials": True,
"exec_owner_digest_preserved_by_real_evaluator": True,
})
if args.human_control:
assert pdp.last_decision["binding"]["context"]["human_control"] is True
assert engine.claim("synthetic-owner-delivery")["binding"]["human_control"] is True
receipt["checks"]["declared_human_intent_preserved_by_real_evaluator"] = True
receipt["checks"]["declared_human_fixture_consumed_before_owner_delivery"] = True
receipt["exec_owner_scope"] = "Synthetic recipient/backend with real KeyCape, Approval Engine, Flex Auth and Secrets Engine CLI; not native custody or human approval proof"
finally:
if api_server:
@ -436,7 +482,10 @@ def main():
parser.add_argument("--flex-auth-source", required=True, type=Path)
parser.add_argument("--receipt", required=True, type=Path)
parser.add_argument("--exec-owner", action="store_true", help="also prove catalog-bound child delivery against the actual approval/PDP chain")
parser.add_argument("--human-control", action="store_true", help="require declared human control in the exec-owner exercise; positive human entry is an explicit local store fixture")
args = parser.parse_args()
if args.human_control and not args.exec_owner:
parser.error("--human-control requires --exec-owner")
if args.receipt.exists():
raise SystemExit("receipt path must be new")
try:

View file

@ -49,7 +49,7 @@ synthetic exec-env transport proof passed; no real secret was read.
id: SECRETS-WP-0009-T03
status: wait
priority: high
blocking_reason: "Recipient binding now implemented and proved with the real local approval/PDP chain. Complete exact private owner configuration and native MessagesOwner holder admission, CCR-2026-0019 operator reader, human/audit/service path and scoped live delivery. CCR-2026-0020 was cancelled by its owner."
blocking_reason: "Recipient binding and declared human-control request/PEP enforcement implemented and proved with the real local approval/PDP chain. Complete exact private owner configuration and native MessagesOwner holder admission, CCR-2026-0019 operator reader, human/audit/service path and scoped live delivery. CCR-2026-0020 was cancelled by its owner."
state_hub_task_id: "f8069c8a-ad6b-5d0b-9a36-c2326699437d"
```
@ -238,3 +238,30 @@ from operator-owned creation, parent custody and cleanup. No automatic cleanup
or live reader proof is claimed. The exact group remains NetKingdom/KeyCape's
return, followed by reviewed attended admission and positive/negative evidence.
T03 stays wait; no identity, role, secret, runtime or route readiness changed.
### 2026-09-10 declared human-control consumer return
Consumed APPROVAL-WP-0002's GH-DEC-2026-016 implementation and schema-v5 release.
The existing human action-review requirement is explicit on the factory catalog:
`approval.human_control: true`. The CheckRequest carries that intent in its bound
context; the consumer requires exact true in the served claim before PDP and
again afterward. The internal consume binding carries the observed declaration.
Missing service coordinates, ordinary lane approval, stage and unsafe-demo paths
cannot substitute for the declared control. Undeclared lanes retain their behavior.
The catalog's exec owner stays pending; no policy, role or credential was changed.
Validation: 388 tests pass, including 27 new strict-type, missing/false claim,
request binding, post-PDP recheck, fallback and actual exec-handler cases.
The real local KeyCape/Approval Engine/Flex Auth exercise passes 22 checks.
The request API declares true; KeyCape service binding is refused; an undeclared
claim reaches no PDP/consume/backend; declared positive delivery consumes before
the synthetic backend/child. Positive human evidence is an explicit store fixture,
not a native human JWT or browser login. Evidence and repeatable command:
`docs/evidence/2026-09-10-human-control-consumer-exercise.json`,
`tools/exercise_approval_identity.py --exec-owner --human-control`.
This closes consumer source adoption. T03 still requires the narrow native
requester, INFD T07/T08 functional browser and human approval, audit custody,
CCR-2026-0019 operator binding/attended read, current deployed service/PDP and
configured MessagesOwner/native delivery. HFACT T01/T04/T05 retains factory
config/G0 and natural queue/model proof. No native secret read or paid attempt.