diff --git a/docs/reviews/2026-10-11-task-execution-pdp-source.md b/docs/reviews/2026-10-11-task-execution-pdp-source.md new file mode 100644 index 0000000..78aa961 --- /dev/null +++ b/docs/reviews/2026-10-11-task-execution-pdp-source.md @@ -0,0 +1,88 @@ +# Access owner review: exact task-execution PDP source preparation + +reviewer: access-engine-owner (agent access-engine, codex) +decision: approved +work_item: FLEX-WP-ADHOC-2026-10-11-T02 +scope: proposed integration direction and bounded source characterization only +anchor_commit: f30e08cc96634d48987fb7203b71961437119535 + +Comment: Access INTENT.md, Responsibility Boundary / Flex-Auth Owns the Decision +and Protected Systems Own Enforcement, reserves policy evaluation exclusively to +Access and places enforcement/lifecycle/custody downstream. The reviewed design +uses the existing CheckRequest/DecisionEnvelope and served approval interfaces; +Access does not mint OpenBao entitlements or operate a new grant service. The +source contract accurately distinguishes digest binding from policy authority, +transport caller authentication from initiating-principal identity, and source +preparation from actual native lifecycle adoption. I approve that proposed +integration direction and the exact characterization artifacts below, under the +completed own-repo task above persisted before this review. Only Access's owner +role is reviewed here. + +## Exact reviewed revisions + +| Artifact | SHA256 | +| --- | --- | +| Access candidate docs/task-execution-pdp-consumer-contract.md | 6a08b5cf91cf4507865aeb950cd448da93778772530b45861bf18f09cddf6263 | +| Access candidate pkg/api/execution_contract_test.go | 9e5f0df4b8cdd526747d2cffb31492c6c6fc1ecc68675b505c3ba0f432a5fa25 | +| Access candidate internal/policy/secrets_execution_gap_test.go | 643e7b85b6a0cb9fb85d02b5f2b237d97c9a0c872f315208cf100f82d9936739 | +| Platform docs/credential-lane-designs/secrets-engine-task-execution-entitlement.md | 180b979d18b3f145993575899fac8681fc9865b8c218638bb62b068ba5337bf4 | +| Secrets docs/task-scoped-openbao-execution-contract.md | e4b72d425491530aa9ebc7c9f3cec5f4408a247cba46ae38e94721e69887c5e6 | + +Access source behavior tested from baseline +c829e530309d4a885a7dbe34ddc316e0c25d205f in isolated /tmp staging. +`go test ./pkg/api ./internal/policy -run ExecutionContract -count=1` passed. +The candidates have not been applied to the actual checkout or published. +The completed review anchor is not a runtime admission field. + +## Exact consumer builder integration required + +Implement one keyword-only `execution_contract` argument on +`authorization.build_action_request`, `approval_consume._expected_request`, +`resolve_approval_observation`, and `authorize_action`. The owner compiler returns +one schema-validated, independently source-reconstructed contract. Each function +must pass that same semantic value unchanged; `build_action_request` deep-copies +it into `context.execution_contract` before any claim lookup or Check. +No caller-written arbitrary CheckRequest parameter replaces reconstruction. + +The new CLI sends PDP action `execute-ccr`, not `apply`. Reject execute-ccr without +its complete contract; reject attaching this contract to existing apply or any +other legacy action. Force `context.human_control = true` for execute-ccr even +when a legacy catalog does not declare it. Retain the complete existing +`context.catalog_target`, catalog purpose, resource targets and stage. Production +owner boundary supplies verified identity; environment/cfg subject strings alone +cannot satisfy that requirement. A separate policy publication/identity review is +required before a human initiating-principal mapping becomes executable. + +All request-semantic fields, including operation UUID and exact target/body/ +catalog/source hashes, must exist before claim-free issue Check. Only top-level +`context.approval` is excluded by ApprovalBindingDigest. Validate local submitted +request against `binding.submitted_request_digest`; validate the enriched returned +binding's full digest; join served `claim.binding.pdp_digest` to evaluated +`binding.approval_binding_digest`. Pass the full enriched request digest and exact +decision ID to existing consume, and independently confirm the matching durable +consumption triple inside the admitted one-shot execution boundary. Do not use +locally rehashed unenriched approval-free request as that join. + +Required consumer regression tests must intercept actual claim and PDP requests +and prove the compiled contract survives every builder unchanged; changed nested +operation/body/source/catalog fields fail the approval join before consume; missing +contract, wrong action or forged subject causes zero claim/PDP/consume/backend +calls; caller mutation after compilation cannot mutate the bound request. +Existing staged Access tests establish canonical API behavior and old-package +refusal/gaps, not those future consumer tests. + +## Explicit limits on this review + +This review approves no new executable package/action grant, representation +identity, service token, mediator, ledger, task-token ACL, native CCR, credential +request, production change, or lifecycle adoption. Shipped v2 admits only service +subject secrets-engine and its twelve original actions; it denies execute-ccr and +human principals, and existing apply ignores execution-contract contents. A new +exact owner-reviewed package/version and authenticated initiating-principal +correspondence must be reviewed and published separately. Historic enforced caller +route receipts are not current reachability evidence. No native calls were made. + +Secrets T06 and Platform RPF35-T02 remain open. Later entitlement acceptance needs +the actual selected production workflow through its exact reviewed compiler, +consume/admission and restricted backend, with native effect, refusal/no-fallback, +and verified cleanup. A metadata trial does not adopt provision, rotation or exec. diff --git a/docs/task-execution-pdp-consumer-contract.md b/docs/task-execution-pdp-consumer-contract.md new file mode 100644 index 0000000..e32644a --- /dev/null +++ b/docs/task-execution-pdp-consumer-contract.md @@ -0,0 +1,116 @@ +# Exact execute-ccr PDP consumer integration (source preparation) + +Access source baseline: c829e530309d4a885a7dbe34ddc316e0c25d205f. +No native readiness or formal owner assent is established by this staged artifact. + +## Existing interface, without a new grant API + +Use POST /v1/check with CheckRequest, DecisionEnvelope and the existing served +approval claim/consume APIs. pkg/api/canonical.go already hashes nested context +values, including an execution_contract object. No new canonical digest function +or access-engine execution ledger is needed. Access owns policy decisions only; +Secrets owns PEP lifecycle and one-shot admission, Platform owns OpenBao custody. + +The published request action for the new CLI must be execute-ccr, resource +secret-catalog-lane under system secrets-engine, id catalog:, stage +prod and tenant tenant:platform. CLI execute-ccr must never send apply: shipped +lifecycle v2 accepts apply while ignoring all execution_contract contents. That +existing grant is not the reviewed metadata execution contract. Preserve existing +12-action behavior and add a separate owner-reviewed exact execute-ccr policy +version/action declaration and denial tests before publication. + +The proposed policy must enforce human_control true, served approval kind/issuer/ +valid_now/human_control/pdp_path, closed contract_version and metadata-apply-ccr, +nonempty exact operation_id, bounded deadline/budget, explicit target/body/catalog/ +CCR/grant/source hashes and trusted principal representation. Hash existence is +shape, not provenance: body equality and owner-source reconstruction remain PEP +obligations. Policy cannot compare its own post-evaluation request digest. +Unknown action, missing contract/approval, foreign principal and older package +must refuse. A package/version pin is validated in envelope provenance, not hashed +as an authority field: api.RequestDigest excludes request.policy_version. + +## Mandatory digest joins + +1. Compile owner sources and catalog into the complete claim-free request. + All execution fields are inside context.execution_contract before issue Check. + Use context.purpose for consumer purpose. Top-level purpose is reserved for + advisory readback and enforcement /v1/check refuses it. +2. Issue approval against claim-free Check's enriched binding.request_digest. + Approval Engine records that value as claim.binding.pdp_digest. +3. Fetch the live served claim, attach it only at context.approval, and Check the + exact same request again. Retain the submitted request in owner memory. +4. PEP validates binding.submitted_request_digest against its submitted request; + binding.request_digest against the returned enriched binding material; and + claim.binding.pdp_digest against binding.approval_binding_digest. The latter + is the enriched request with only top-level context.approval removed. It does + not remove execution_contract, human_control, catalog_target or purpose. +5. Validate signature/served authenticity using existing configured PEP path, + provenance package/version, caller record, registry snapshot, allow lifetime, + live claim freshness, and exact subject/action/resource/tenant/context. Registry + enrichment can alter subject/resource attributes: do not compare locally + computed claim-free digest to claim.pdp_digest. +6. Consume once with approval_id, full enriched binding.request_digest and exact + decision_id. Approval Engine's consume checks request digest format/CAS, not + semantic correspondence to issue pdp_digest; steps 1–5 are mandatory. Independently + read the durable approval consumption triple through the authenticated owner + route and match it before OpenBao. Idempotent response is never fresh admission. +7. A changed operation UUID/method/path/body/source/catalog/subject yields a + different approval-free digest; require a fresh exact approval. Retries only + return final stored receipt. Unknown consume/backend outcomes require recovery. + +## Minimal consumer code delta + +Secrets authorization.build_action_request gains a strict optional keyword-only +execution_contract parameter. validate/compile it in Secrets' owner compiler, +then deep-copy it into context. Reject a contract on any action except execute-ccr, +reject execute-ccr without the contract. Set human_control true on this action +regardless of legacy catalog default. Catalog_target remains included. + +Thread the SAME validated object through approval_consume._expected_request, +resolve_approval_observation and authorize_action. Today _expected_request rebuilds +from cfg/entry and drops any caller-added context; merely adding context in CLI +would not reach the claim/PDP join. No arbitrary caller request/body injection. +Require the execution boundary's verified initiator identity, not cfg/environment +strings, and exact package/version configuration for execute-ccr. Return the +existing ConsumeBinding and confirmed ConsumedApproval, never an opaque approval +label. require_production_consume runs only in owner child after admission reserve. + +Platform independently recompiles exact operations and compares full digest, +then invokes the reviewed Secrets consume path at the actual effect boundary. +Its persistent one-shot ledger requires unique approval+digest and durable states +before each external call. This ledger and restricted backend do not currently +exist; audit outbox and bare consumed bool do not substitute. No unrestricted +subprocess, generic OpenBao apply, caller ACL/HCL, path override or bearer return. + +## Principal and route truth + +values/secrets-engine.yaml enforces Kubernetes TokenReview for +system:serviceaccount:secrets-engine:secrets-engine representing protected system +secrets-engine. This authenticates transport caller/system correspondence only: +internal/callerauth/auth.go does not bind CheckRequest.subject to that identity. +The v2 policy separately admits only subject secrets-engine/type service. A +caller-authored initiating human is neither authenticated by this path nor an +admitted v2 subject. An explicit identity-owner verified representation contract +plus policy grant is required before actual initiating-principal design can ship. +Do not rename service:secrets-engine to secrets-engine silently; authentication +service subject and PDP principal have different published vocabularies. + +FLEX-WP-0023 records historic enforce-mode positive/negative native caller proof. +This is source evidence of an existing route, not a current live probe. Operator +CLI access needs attended admitted TokenRequest/tunnel/network path; in-cluster +DNS is not a workstation route. No token was requested and no native query ran. +FLEX-WP-0036 runtime rename remains active: keep existing flex-auth coordinates +until exact consumer transition is separately admitted. No new executor route, +metadata CCR, task execution grant, source pin, real native effect, cleanup proof, +principal representation or steady-state workflow adoption is supplied by this +stage. RPF35-T02 and Secrets T06 must remain open. + +## Verified source tests + +Run go test ./pkg/api ./internal/policy -run ExecutionContract -count=1 in staged +source. API tests cover ten changed semantic fields, claim attach/removal, +registry enrichment and correlation ID irrelevance. Policy test runs actual v2 +Rego: execute-ccr denies unknown_action, fake execution context on apply still +allows (demonstrating the semantic gap), human principal denies wrong_subject_type. +These characterize shipped behavior and digest integration; they do not claim +new entitlement policy or executor is shipped. diff --git a/internal/policy/secrets_execution_gap_test.go b/internal/policy/secrets_execution_gap_test.go new file mode 100644 index 0000000..60a670e --- /dev/null +++ b/internal/policy/secrets_execution_gap_test.go @@ -0,0 +1,25 @@ +package policy_test + +import ( + "context" + "path/filepath" + "testing" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/pkg/api" +) + +func TestExecutionContractShippedPolicyDoesNotAdmitNewActionOrPrincipal(t *testing.T) { + pkg,err:=policy.LoadAndValidateFile(context.Background(),filepath.Join("..","..","examples","secrets-engine","policy_package.md")) + if err!=nil || !pkg.Valid {t.Fatalf("policy load: %v",err)} + r:=api.CheckRequest{Tenant:"tenant:platform",Subject:api.SubjectRef{ID:"secrets-engine",Type:"service"},Action:"execute-ccr",Resource:api.ResourceRef{ID:"catalog:test",Type:"secret-catalog-lane",System:"secrets-engine"},Context:map[string]any{"execution_contract":map[string]any{"operation":"metadata-apply-ccr"}}} + result,err:=pkg.Evaluate(context.Background(),r) + if err!=nil || result.Effect!=api.DecisionEffectDeny || result.Reason!="unknown_action" {t.Fatalf("new action: %+v %v",result,err)} + // Existing apply ignores execution_contract content; its allow is not exact task authority. + r.Action="apply" + r.Context["execution_contract"]=map[string]any{"operation":"arbitrary-mutation","payload_sha256":"forged"} + result,err=pkg.Evaluate(context.Background(),r) + if err!=nil || result.Effect!=api.DecisionEffectAllow {t.Fatalf("existing apply gap characterization: %+v %v",result,err)} + r.Subject=api.SubjectRef{ID:"founder-unregistered",Type:"human"} + result,err=pkg.Evaluate(context.Background(),r) + if err!=nil || result.Effect!=api.DecisionEffectDeny || result.Reason!="wrong_subject_type" {t.Fatalf("human principal: %+v %v",result,err)} +} diff --git a/pkg/api/execution_contract_test.go b/pkg/api/execution_contract_test.go new file mode 100644 index 0000000..1f21925 --- /dev/null +++ b/pkg/api/execution_contract_test.go @@ -0,0 +1,52 @@ +package api + +import ( + "encoding/json" + "testing" +) + +func executionRequest(t *testing.T) CheckRequest { + t.Helper() + var r CheckRequest + err := json.Unmarshal([]byte(`{"tenant":"tenant:platform","subject":{"id":"secrets-engine","type":"service"},"action":"execute-ccr","resource":{"id":"catalog:test","type":"secret-catalog-lane","system":"secrets-engine","attributes":{"stage":"prod"}},"context":{"purpose":"metadata-apply-ccr","human_control":true,"execution_contract":{"contract_version":"SECRETS-EXEC-20261011-v1","operation_id":"018b3ff9-7470-470b-bf37-36d8b9b67eb1","catalog_sha256":"catalog-original","ccr_immutable_sha256":"ccr-original","source_bundle":[{"path":"/owner/mediator.py","sha256":"source-original"}],"operations":[{"method":"POST","path":"/v1/sys/policies/acl/exact","payload_sha256":"body-original"}]}}}`), &r) + if err != nil { t.Fatal(err) } + return r +} + +func TestExecutionContractExactSemanticChangesInvalidateBinding(t *testing.T) { + original := executionRequest(t) + digest := RequestDigest(original) + cases := map[string]func(*CheckRequest){ + "operation id":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["operation_id"]="other"}, + "catalog":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["catalog_sha256"]="other"}, + "ccr":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["ccr_immutable_sha256"]="other"}, + "source":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["source_bundle"].([]any)[0].(map[string]any)["sha256"]="other"}, + "method":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["operations"].([]any)[0].(map[string]any)["method"]="DELETE"}, + "target":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["operations"].([]any)[0].(map[string]any)["path"]="/v1/sys/policies/acl/sibling"}, + "body":func(r *CheckRequest){r.Context["execution_contract"].(map[string]any)["operations"].([]any)[0].(map[string]any)["payload_sha256"]="other"}, + "principal":func(r *CheckRequest){r.Subject.ID="other"}, + "action":func(r *CheckRequest){r.Action="apply"}, + "tenant":func(r *CheckRequest){r.Tenant="other"}, + } + for name, change := range cases {t.Run(name,func(t *testing.T){r:=executionRequest(t);change(&r);if RequestDigest(r)==digest || ApprovalBindingDigest(r)==digest {t.Fatal("semantic mutation retained binding")}})} +} + +func TestExecutionContractApprovalJoinAndEnrichment(t *testing.T) { + issue:=executionRequest(t) + // Model the actual evaluator's tenant/registry overlay before issue Check. + issue.Subject.Tenant=issue.Tenant + issue.Subject.Attributes=map[string]any{"display_name":"Engine owner"} + issue.Resource.Tenant=issue.Tenant + submitted:=executionRequest(t) + submitted.Context["approval"]=map[string]any{"kind":"approval-claim","binding":map[string]any{"pdp_digest":RequestDigest(issue)}} + evaluated:=issue + evaluated.Context=map[string]any{} + for k,v:=range issue.Context {evaluated.Context[k]=v} + evaluated.Context["approval"]=submitted.Context["approval"] + binding:=NewDecisionBindingFor(evaluated,submitted) + if binding.SubmittedRequestDigest!=RequestDigest(submitted) {t.Fatal("submitted request not bound")} + if binding.ApprovalBindingDigest!=RequestDigest(issue) {t.Fatal("claim-free issue digest does not join")} + if binding.RequestDigest==binding.SubmittedRequestDigest {t.Fatal("test did not exercise enriched request")} + submitted.ID="different-correlation-id" + if RequestDigest(submitted)!=binding.SubmittedRequestDigest {t.Fatal("correlation id unexpectedly authorizes")} +} diff --git a/workplans/ADHOC-2026-10-11.md b/workplans/ADHOC-2026-10-11.md index 68b0de1..7d4b85d 100644 --- a/workplans/ADHOC-2026-10-11.md +++ b/workplans/ADHOC-2026-10-11.md @@ -64,4 +64,8 @@ record does not establish owner assent, a native entitlement, an authenticated initiator representation, a mediator/ledger, exact native CCR/grant, or lifecycle adoption. No native call, credential request, DB write, commit or source-policy activation ran. Existing Secrets T06 and Platform RPF35-T02 remain open. -Formal own-role review can follow persistence of this completed work record. +Formal Access-own-role source review prepared after this completed anchor was +persisted in commit `f30e08cc96634d48987fb7203b71961437119535`: candidate +`docs/reviews/2026-10-11-task-execution-pdp-source.md`. The review approves only +the exact proposed integration direction and source characterization; it grants +no package/action, native entitlement, representation identity or lifecycle adoption.