Require declared human control in factory credential delivery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
d1c13b5dd6
commit
2b0d04e8e1
11 changed files with 451 additions and 27 deletions
|
|
@ -41,6 +41,7 @@ delivery_auth:
|
||||||
token_num_uses: 8
|
token_num_uses: 8
|
||||||
approval:
|
approval:
|
||||||
model: ccr
|
model: ccr
|
||||||
|
human_control: true
|
||||||
decision_ref: CCR-2026-0016
|
decision_ref: CCR-2026-0016
|
||||||
notes: Custody only has been completed. Native apply and exec require durable exact-action
|
notes: Custody only has been completed. Native apply and exec require durable exact-action
|
||||||
authorization, engine consume, scoped backend authority and verified delivery
|
authorization, engine consume, scoped backend authority and verified delivery
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
Every live privileged production handler passes `_require_lane_approval`,
|
||||||
which calls `require_production_consume` before `OpenBaoClient.resolve`.
|
which calls `require_production_consume` before `OpenBaoClient.resolve`.
|
||||||
Dry-run and `plan` do not consume. Build/test remain fail-open relative to
|
Dry-run and `plan` do not consume. For undeclared lanes, build/test and the
|
||||||
approval-engine. The three-factor unsafe-demo exception is not a consume
|
three-factor unsafe-demo exception retain their existing behavior. An explicitly
|
||||||
path.
|
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
|
## Fail closed
|
||||||
|
|
||||||
|
|
@ -110,8 +151,9 @@ digest, idempotence, and consumed-at only. No token, secret, or accessor.
|
||||||
|
|
||||||
## Required configuration
|
## Required configuration
|
||||||
|
|
||||||
The join is absent by default, so an unconfigured engine behaves exactly as it
|
The join is absent by default. Undeclared lanes retain their existing behavior;
|
||||||
did before. Production additionally needs:
|
a lane declaring `approval.human_control: true` refuses an absent join.
|
||||||
|
Production and declared human-control paths need:
|
||||||
|
|
||||||
| Variable | Meaning |
|
| Variable | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,16 @@ Before accepting an existing production lane:
|
||||||
lanes. Set `false` when the consumer needs only the KV data GET; generated
|
lanes. Set `false` when the consumer needs only the KV data GET; generated
|
||||||
policy omits the metadata endpoint. Owner metadata verification then needs
|
policy omits the metadata endpoint. Owner metadata verification then needs
|
||||||
separate operator authority, not broader consumer access.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,7 @@ def _validate_observation(
|
||||||
claim: object,
|
claim: object,
|
||||||
*,
|
*,
|
||||||
approval_id: str,
|
approval_id: str,
|
||||||
|
require_human_control: bool = False,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Validate issuer, shape, validity and freshness; no action correspondence."""
|
"""Validate issuer, shape, validity and freshness; no action correspondence."""
|
||||||
|
|
@ -135,6 +136,8 @@ def _validate_observation(
|
||||||
binding = claim.get("binding")
|
binding = claim.get("binding")
|
||||||
if not isinstance(binding, dict):
|
if not isinstance(binding, dict):
|
||||||
raise DecisionError("approval claim binding must be an object")
|
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)
|
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||||||
freshness = claim.get("freshness")
|
freshness = claim.get("freshness")
|
||||||
if not isinstance(freshness, dict):
|
if not isinstance(freshness, dict):
|
||||||
|
|
@ -154,13 +157,15 @@ def _validate_observation(
|
||||||
|
|
||||||
|
|
||||||
def observe_pdp_approval_claim(claim: object, *, approval_id: str,
|
def observe_pdp_approval_claim(claim: object, *, approval_id: str,
|
||||||
|
require_human_control: bool = False,
|
||||||
now: datetime | None = None) -> dict[str, Any]:
|
now: datetime | None = None) -> dict[str, Any]:
|
||||||
"""Validate a fresh fact for submission to the PDP, not authority to consume.
|
"""Validate a fresh fact for submission to the PDP, not authority to consume.
|
||||||
|
|
||||||
Action correspondence cannot be established until the evaluator returns its
|
Action correspondence cannot be established until the evaluator returns its
|
||||||
enriched approval binding. This function deliberately makes no such claim.
|
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"]
|
binding = observed["binding"]
|
||||||
if binding.get("pdp_path") is not True:
|
if binding.get("pdp_path") is not True:
|
||||||
raise DecisionError("approval claim does not declare binding.pdp_path; request an approval bound at issue")
|
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,
|
def validate_approval_claim(claim: object, *, approval_id: str,
|
||||||
expected_binding_digest: str = "", expected_pdp_digest: str = "",
|
expected_binding_digest: str = "", expected_pdp_digest: str = "",
|
||||||
|
require_human_control: bool = False,
|
||||||
now: datetime | None = None) -> dict[str, Any]:
|
now: datetime | None = None) -> dict[str, Any]:
|
||||||
"""Validate the fact and compare an independently supplied binding.
|
"""Validate the fact and compare an independently supplied binding.
|
||||||
|
|
||||||
A PDP digest supplied here must come from the evaluated decision, never a
|
A PDP digest supplied here must come from the evaluated decision, never a
|
||||||
local reconstruction of the unenriched request.
|
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:
|
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:
|
if observed["binding"]["pdp_digest"] != expected_pdp_digest:
|
||||||
raise DecisionError("approval claim pdp digest does not match the request")
|
raise DecisionError("approval claim pdp digest does not match the request")
|
||||||
elif expected_binding_digest:
|
elif expected_binding_digest:
|
||||||
|
|
|
||||||
|
|
@ -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_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.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.decision_check import check_decision
|
||||||
from secrets_engine.authorization import (
|
from secrets_engine.authorization import (
|
||||||
build_action_request,
|
build_action_request,
|
||||||
|
|
@ -40,6 +41,7 @@ class ConsumeBinding:
|
||||||
approval_id: str
|
approval_id: str
|
||||||
request_digest: str
|
request_digest: str
|
||||||
decision_id: str = ""
|
decision_id: str = ""
|
||||||
|
human_control: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -68,6 +70,7 @@ class AuthorizedAction:
|
||||||
"authorization_decision_id": self.decision_id,
|
"authorization_decision_id": self.decision_id,
|
||||||
"authorization_expires_at": self.expires_at,
|
"authorization_expires_at": self.expires_at,
|
||||||
"request_digest": self.binding.request_digest,
|
"request_digest": self.binding.request_digest,
|
||||||
|
"approval_human_control": self.binding.human_control,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -222,13 +225,17 @@ def resolve_approval_observation(
|
||||||
) -> ApprovalObservation | None:
|
) -> ApprovalObservation | None:
|
||||||
"""Observe a fresh fact and prepare the claim-bearing request (step 1).
|
"""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.
|
Correspondence and a consume binding require authorize_action's PDP step.
|
||||||
"""
|
"""
|
||||||
base_url = str(getattr(cfg, "approval_url", "") or "")
|
base_url = str(getattr(cfg, "approval_url", "") or "")
|
||||||
auth_configured = approval_auth_configured(cfg)
|
auth_configured = approval_auth_configured(cfg)
|
||||||
authorization_id = _authorization_id(entry, decision)
|
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 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
|
return None
|
||||||
|
|
||||||
expected_request = _expected_request(
|
expected_request = _expected_request(
|
||||||
|
|
@ -242,7 +249,8 @@ def resolve_approval_observation(
|
||||||
authorization_id=authorization_id,
|
authorization_id=authorization_id,
|
||||||
opener=opener or credential_urlopen,
|
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
|
expected_request["context"]["approval"] = claim
|
||||||
return ApprovalObservation(authorization_id, expected_request, claim)
|
return ApprovalObservation(authorization_id, expected_request, claim)
|
||||||
|
|
||||||
|
|
@ -310,6 +318,7 @@ def authorize_action(
|
||||||
validate_approval_claim(
|
validate_approval_claim(
|
||||||
observation.claim, approval_id=observation.approval_id,
|
observation.claim, approval_id=observation.approval_id,
|
||||||
expected_pdp_digest=envelope["binding"]["approval_binding_digest"],
|
expected_pdp_digest=envelope["binding"]["approval_binding_digest"],
|
||||||
|
require_human_control=expected_request["context"].get("human_control") is True,
|
||||||
)
|
)
|
||||||
if validated.action != action:
|
if validated.action != action:
|
||||||
raise DecisionError("access-engine decision does not bind this action")
|
raise DecisionError("access-engine decision does not bind this action")
|
||||||
|
|
@ -318,6 +327,7 @@ def authorize_action(
|
||||||
approval_id=observation.approval_id,
|
approval_id=observation.approval_id,
|
||||||
request_digest=validated.request_digest,
|
request_digest=validated.request_digest,
|
||||||
decision_id=validated.decision_id,
|
decision_id=validated.decision_id,
|
||||||
|
human_control=observation.claim["binding"].get("human_control") is True,
|
||||||
),
|
),
|
||||||
decision_id=validated.decision_id,
|
decision_id=validated.decision_id,
|
||||||
expires_at=validated.expires_at,
|
expires_at=validated.expires_at,
|
||||||
|
|
@ -413,16 +423,21 @@ def require_production_consume(
|
||||||
evidence: Any = None,
|
evidence: Any = None,
|
||||||
opener: Callable[..., Any] | None = None,
|
opener: Callable[..., Any] | None = None,
|
||||||
) -> ConsumedApproval | 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
|
Undeclared build/test and the unsafe-demo exception retain their existing
|
||||||
unsafe-demo exception is not a consume path. Missing binding, URL, or
|
behavior. Declared human controls require a verified binding and consume
|
||||||
credential fail closed so a stance bypass cannot reach OpenBao.
|
regardless of stage/demo; missing inputs cannot fall back to a lane review.
|
||||||
"""
|
"""
|
||||||
if getattr(entry, "stage", "") != "prod":
|
required_human_control = human_control_required(entry)
|
||||||
return None
|
if required_human_control:
|
||||||
if demo_exception_enabled(cfg):
|
if binding is None or binding.human_control is not True:
|
||||||
return None
|
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:
|
if binding is None:
|
||||||
raise DecisionError(
|
raise DecisionError(
|
||||||
"production OpenBao call requires CAS consume of an approval "
|
"production OpenBao call requires CAS consume of an approval "
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
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
|
from secrets_engine.errors import DecisionError
|
||||||
|
|
||||||
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||||
|
|
@ -105,6 +105,10 @@ def build_action_request(
|
||||||
"context": {"purpose": purpose},
|
"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":
|
if action == "exec":
|
||||||
from secrets_engine.exec_owner import owner_digest
|
from secrets_engine.exec_owner import owner_digest
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,15 @@ class CatalogEntry:
|
||||||
return self.kind == "kv"
|
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:
|
def validate_entry(data: dict[str, Any], *, source: str = "<memory>") -> CatalogEntry:
|
||||||
"""Validate a raw mapping and return a CatalogEntry, or raise CatalogError."""
|
"""Validate a raw mapping and return a CatalogEntry, or raise CatalogError."""
|
||||||
if not isinstance(data, dict):
|
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}"
|
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", {})
|
risk = data.get("risk", {})
|
||||||
if not isinstance(risk, dict):
|
if not isinstance(risk, dict):
|
||||||
raise CatalogError(f"{source}: risk must be a mapping")
|
raise CatalogError(f"{source}: risk must be a mapping")
|
||||||
|
|
|
||||||
170
tests/test_human_control.py
Normal file
170
tests/test_human_control.py
Normal 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"
|
||||||
|
|
@ -355,15 +355,40 @@ def exercise(args):
|
||||||
"files": {str(p): {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "private": True}
|
"files": {str(p): {"sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "private": True}
|
||||||
for p in [executable, script]},
|
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)
|
owner_entry = validate_entry(raw)
|
||||||
req = _expected_request(cfg, owner_entry, "exec", fields=("api_token",))
|
req = _expected_request(cfg, owner_entry, "exec", fields=("api_token",))
|
||||||
owner_request_digest = pdp.bind_request(req)
|
owner_request_digest = pdp.bind_request(req)
|
||||||
operator_request("/v1/approvals", {
|
def issue_owner(ident, human_control):
|
||||||
"id": "synthetic-owner-delivery", "binding": {"action": "secrets.exec", "target": {"id": owner_entry.id, "stage": "prod"},
|
# The requester declares intent on an unapproved object;
|
||||||
"actor": "service:approval-engine-operator", "principal": "synthetic-operator", "purpose": "disposable identity proof"},
|
# only the subsequent human bind discharges the control.
|
||||||
"validity": {"not_before": (now - timedelta(minutes=1)).isoformat(), "expires_at": (now + timedelta(minutes=10)).isoformat()},
|
return operator_request("/v1/approvals", {
|
||||||
"pdp_digest": owner_request_digest, "pdp_path": True}, "approval:create")
|
"id": ident, "binding": {"action": "secrets.exec", "target": {"id": owner_entry.id, "stage": "prod"},
|
||||||
operator_request("/v1/approvals/synthetic-owner-delivery/entries", {}, "approval:approve")
|
"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 = []
|
backend_calls = []
|
||||||
class Backend:
|
class Backend:
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|
@ -380,7 +405,23 @@ def exercise(args):
|
||||||
yield Backend()
|
yield Backend()
|
||||||
command = raw["delivery_config"]["exec_owner"]["command"]
|
command = raw["delivery_config"]["exec_owner"]["command"]
|
||||||
cli_args = SimpleNamespace(catalog=owner_entry.id, field="api_token", mode="exec-env", command=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 = SimpleNamespace(**vars(cli_args))
|
||||||
substituted.command = ["/bin/echo", "substitute"]
|
substituted.command = ["/bin/echo", "substitute"]
|
||||||
try:
|
try:
|
||||||
|
|
@ -411,6 +452,11 @@ def exercise(args):
|
||||||
"exec_owner_actual_child_excludes_parent_credentials": True,
|
"exec_owner_actual_child_excludes_parent_credentials": True,
|
||||||
"exec_owner_digest_preserved_by_real_evaluator": 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"
|
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:
|
finally:
|
||||||
if api_server:
|
if api_server:
|
||||||
|
|
@ -436,7 +482,10 @@ def main():
|
||||||
parser.add_argument("--flex-auth-source", required=True, type=Path)
|
parser.add_argument("--flex-auth-source", required=True, type=Path)
|
||||||
parser.add_argument("--receipt", 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("--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()
|
args = parser.parse_args()
|
||||||
|
if args.human_control and not args.exec_owner:
|
||||||
|
parser.error("--human-control requires --exec-owner")
|
||||||
if args.receipt.exists():
|
if args.receipt.exists():
|
||||||
raise SystemExit("receipt path must be new")
|
raise SystemExit("receipt path must be new")
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ synthetic exec-env transport proof passed; no real secret was read.
|
||||||
id: SECRETS-WP-0009-T03
|
id: SECRETS-WP-0009-T03
|
||||||
status: wait
|
status: wait
|
||||||
priority: high
|
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"
|
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
|
or live reader proof is claimed. The exact group remains NetKingdom/KeyCape's
|
||||||
return, followed by reviewed attended admission and positive/negative evidence.
|
return, followed by reviewed attended admission and positive/negative evidence.
|
||||||
T03 stays wait; no identity, role, secret, runtime or route readiness changed.
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue