From 5ae70642a77a9d286ca09916548c467339ee390b Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 11 Oct 2026 19:35:31 +0200 Subject: [PATCH] Add source-only owner-derived task execution plan compiler Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade --- ...n-plan-v2-platform-contract-readiness.json | 71 ++++++ docs/task-execution-plan-compiler.md | 98 +++++++++ scripts/task_execution_plan.py | 206 ++++++++++++++++++ tests/test_task_execution_plan.py | 146 +++++++++++++ ...-WP-0035-credential-lane-implementation.md | 22 +- 5 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 docs/reviews/2026-10-11-task-execution-plan-v2-platform-contract-readiness.json create mode 100644 docs/task-execution-plan-compiler.md create mode 100644 scripts/task_execution_plan.py create mode 100644 tests/test_task_execution_plan.py diff --git a/docs/reviews/2026-10-11-task-execution-plan-v2-platform-contract-readiness.json b/docs/reviews/2026-10-11-task-execution-plan-v2-platform-contract-readiness.json new file mode 100644 index 0000000..d19401e --- /dev/null +++ b/docs/reviews/2026-10-11-task-execution-plan-v2-platform-contract-readiness.json @@ -0,0 +1,71 @@ +{ + "author": "railiance-platform-owner (component maker, codex)", + "completed_owner_record_precedes_receipt": { + "path": "workplans/ADHOC-2026-10-11.md", + "persisted_commit": "7adbcb4", + "sha256": "2ed8e1c1f91351f26383d10c14b29d02c0cf8332f71183bb5c773a08645ad3f0", + "task": "RPF-WP-ADHOC-2026-10-11-T02" + }, + "disposition": "qualified_source_component_ready_for_independent_integration_review", + "exact_component_manifest": "/tmp/platform-typed-plan-stage-v2/manifest.json", + "exact_component_manifest_sha256": "41cda74ab32384c789ea8a760475671aadad4e5121335c8f60567ff53d66665b", + "exact_entries": { + "docs/task-execution-plan-compiler.md": "85e00093380ef7bbee4e2239679aafe6b798189a9b754962ea4cac3f1cf14ac6", + "scripts/task_execution_plan.py": "0a96c39a71c6fcca5ea695d1a46ce20d7345b27a507851d4d46e1543fd97e656", + "tests/test_task_execution_plan.py": "432f2a316a94b586286426906504cae9368e74cc6ba7a32d788c37924cad8224", + "workplans/ADHOC-2026-10-11.md": "2ed8e1c1f91351f26383d10c14b29d02c0cf8332f71183bb5c773a08645ad3f0" + }, + "formal_owner_contract_receipt": true, + "independent_technical_review_claimed": false, + "maker_receipt_sha256": "5787c05387e8a89c0cdb10adae9867aef1cb6c6173d1617151e88916611fcbf1", + "native_executed": false, + "not_accepted": [ + "Independent technical review of maker code by its own author", + "full mediator/typed-descriptor/authority/native-readiness acceptance", + "caller-provided authority/source configuration or source-label admission", + "sourceapplication/livegrant/tokenexport or defaultflip", + "KEY39 native authority from source-founder memo or owner receipts" + ], + "owner": "railiance-platform", + "owner_intent_binding": "Platform stable interfaces and secure custody: independently owned source/body compilation keeps service-supplied bodies and alternate paths out of metadata execution. This does not create an identity/authorization engine or native authority.", + "parent_task": "RPF-WP-0035-T12", + "parent_task_closed": false, + "privileged_authority_granted": false, + "provided_contract": [ + "OwnerConfig is owner-installed, never caller-deserialized. Closed public source set fixes compiler, current CCR validator/generator, CCR, one generated policy artifact and separately pinned actual Secrets catalogue source/loader/package/errors/redactor.", + "Existing validator/readiness must accept freshlyapproved confirmed production workload-kv-read; applied/verified/active or alternate types refuse. Existing netkingdom OIDC/Kubernetes roles only; no arbitrarypath/body/argv/KV/mount/AppRole mutation.", + "Actual Secrets catalogue validator/defaults derive the thirteen-field catalog_target; CCR/catalogueid/mount/logicalpath/field/ref equality and existing management enforced. CurrentCCR42 incomplete/noeligiblecatalogue refuses; positivefixtures are synthetic only.", + "Closed local request binds execute-ccr/kind, catalogue/CCR hashes, sixorderedGETPUTGET ACL+GETPUTGET role hashes, UUID and opaque sha256-prefixed canonical requestdigest. Caller bodies/configuration cannot supply authoritativeinputs.", + "Serialized Plan content and Call digests use explicit bare64hex for Journal. GET empty body/PUT generated JSON payload hashes retainprefix. before_call recompiles sources/readiness and rederives body bytes and compares immutable Plan before returning a frozen local Call." + ], + "retained_earlier_component_manifest_sha256": "7838549f939e73981db80022dc304aed4e4b296653a466ee1c3b9b5c7f84e48e", + "review_dependency": "Access worker independent technical integration review is distinct and pending; no Platform owner formal role is attributed to that worker. Root will evaluate exact source for later integration; this qualified ownership receipt does not preapprove it.", + "schema": "owner-source-contract-readiness/v1", + "source_applied": false, + "source_only": true, + "unimplemented_enforced_integration_requirements": [ + "Exact full ExecutionContract comparison against authoritative owner grant/baseline/estate/source/deadline inputs. Local component request is not full descriptor; missing metadata cannot be filled by callerconfiguration.", + "Authenticated initiating human/PIP/representation and independently trusted service identity. No projection/member/MFA/verified/admission flags produced; current Access hard refusal and Secrets readiness refusal remain.", + "Canonical full CheckRequest/PDP/claim/approval-free correspondence and authenticated consume confirmation; digestformat correctness is not authentication.", + "Durable Journal reservation/call-intent/state integration and restricted native adapter that dispatches exactly independently derived bytes with no retry after uncertain effects.", + "Native expected-baseline/readback normalization, before-write conflict checks and source/time enforcement across revalidation-to-dispatch. Observed filesystem changes are detected; hostile sameUID swap/restore/memory mutation not covered by sourcehashes.", + "Full runtime source/config/code/dependency authenticity and private isolation; hard elapsed/remaining action budget enforcement over parser/import/filesystem/compiler work. Component has no hardtimer.", + "Owner-held task token grant validation/TTL120s/use12 and cleanupreserve enforcement, no export, real deniedscope probes/revoke/temp removal and bounded owner-private recovery afterunknown.", + "Reviewed eligible disposable pilot catalogue/approvedCCR, fresh native/go/estate proof and functional success once; full lifecycle/genericapply/defaultmigration/SecretsT06/RPF35T02/T12 closure require separate evidence." + ], + "validation": { + "actual_tested_code_sha256": "0a96c39a71c6fcca5ea695d1a46ce20d7345b27a507851d4d46e1543fd97e656", + "actual_tested_tests_sha256": "432f2a316a94b586286426906504cae9368e74cc6ba7a32d788c37924cad8224", + "maker_tests_passed": 40, + "retained_identical_v1_code_tests": true, + "scenarios": [ + "realowner K8s/OIDC generatedpayloads", + "actualconsumer UTF8catalog fingerprint equality", + "action/kind/path/body/ordinal substitution refusal", + "notapproved/applied/unconfirmed CCR refusal", + "repinnedrootpolicy/engine-managedcatalogue refusal", + "forgedPlan/source drift/duplicates/unsafealias and modes refusal" + ], + "scope": "offline synthetic private temp metadata fixtures and public owner-source reads; no native/Secret/credential operations" + } +} diff --git a/docs/task-execution-plan-compiler.md b/docs/task-execution-plan-compiler.md new file mode 100644 index 0000000..e4c9d7e --- /dev/null +++ b/docs/task-execution-plan-compiler.md @@ -0,0 +1,98 @@ +# Owner-derived typed metadata plan compiler + +Prepared source component for RPF-WP-0035-T12, with completed bounded source +preparation RPF-WP-ADHOC-2026-10-11-T02. This component does not authorize, +dispatch, authenticate or mint anything. CCR38 stays identity/self-only. + +`OwnerConfig` is independently installed by the Platform owner. It is never +parsed from an HTTP body, CheckRequest or a service-supplied configuration. +It fixes the Platform repository, Secrets source repository, owner-admitted local +catalogue directory and exact CCR/catalogue filenames and reviewed source hashes. +The closed source set contains this compiler, existing credential-change validator, +CCR, its one reader HCL artifact, catalogue entry and the actual Secrets catalogue +loader/package/errors/redactor files. Arbitrary additional files are refused. +Paths/hashes are public provenance, not credentials or authority. Source authenticity +and admitted runtime/continuous exclusive source custody remain external gates; +before/after filesystem hashes detect observed changes, not adversarial same-UID +swap-and-restore or arbitrary mutation of Python memory. + +The compiler reuses `validate_ccr`, `applier_readiness_blockers`, `apply_blockers`, +`generated_policy_hcl` and `auth_payload`. It only accepts freshly approved, +confirmed production `workload-kv-read` CCRs whose reviewed HCL matches generated +read-only policy, with existing netkingdom OIDC or Kubernetes auth mount. Already +applied/verified/active CCRs refuse rather than silently become replay authority. +No other lane module, mount enable/config, KV read/write, token/SecretID delivery, +AppRole mutation, caller HCL/JSON body, paths or command argv can be compiled. + +The catalogue comes from a separately owner-pinned local Secrets catalogue file, +loaded by the actual installed/pinned `secrets_engine.catalog.validate_entry`. +Its catalogue id, existing mount, logical path, fields, production stage and +approval CCR reference must match the owner CCR. Engine-managed mounts and auth +are refused: existing delivery-auth metadata can be carried, but is never applied +or authenticated by this action. This is not a new credential-grants catalogue. +The exact thirteen-field `CatalogEntry` projection matches +`build_action_request.context.catalog_target`; its digest uses sorted compact UTF8 +JSON with `ensure_ascii=False`, matching the actual ExecutionContract helper. +Actual Forgejo CCR42 is in_flight and no matching Secrets catalogue exists. Neither +is invented. Positive tests only make explicitly synthetic approved/pilot copies +of existing public owner fixtures in temporary directories; they grant nothing. + +## Concrete interfaces + +`Compiler.proposal()` locally derives metadata: action `execute-ccr`, kind +`workload-kv-read`, catalogue id, CCR bytes digest, exact catalogue-target projection +and digest, and six ordered operation method/path/payload hashes. It is preparation, +not an admission or a complete ExecutionContract. Order matches the proposed Access +package: GET/PUT/GET ACL, then GET/PUT/GET OIDC/Kubernetes role. GET payload is empty; +PUT JSON bytes are deterministic, sorted compact ASCII JSON containing generated +HCL or `auth_payload`, with no caller-selected body. + +`Compiler.compile(request)` accepts exactly that metadata, operation UUID and the +opaque served canonical PDP request digest `sha256:<64 lowercase hex>`. +It compares the exact owner-derived proposal and returns a frozen serialized Plan. +It does not recompute Access canonicalization, strip the prefix, validate a PDP or +accept an unsigned digest as authority. The authenticated mediator must establish +correspondence with exact full CheckRequest/claim/consume bytes independently. +Plan content digest is explicitly bare64hex for Journal.Binding.plan_digest; +request and catalogue/payload hashes retain their required sha256 prefix. + +`Compiler.before_call(plan, ordinal)` accepts exact integer0..5, recompiles the +immutable plan and source bundle, re-derives fresh owner payload bytes and compares +again before returning one frozen local Call. A forged Plan constructor, extra +field, body hash/path/method/ordinal substitution, source drift or readiness change +refuses. Call.call_digest is the bare hash of exact ordinal/kind/method/path/payload +hash for the durable journal. The future mediator must validate this call immediately +before persisting its journal intent and dispatching these same bytes; this API +has no native callback, no backend opener and no credential. + +## Enforced integration gates still missing + +The compiler only binds its closed local component request. It does not construct +or approve the complete `SECRETS-EXEC-20261011-v1` descriptor. The mediator must +compare the exact owner-derived CCR/catalogue/six operations and full independently +pinned source bundle, authoritative grant CCR/hash, origin/cluster, approved native +before/after state expectations, deadline and TTL/use ceilings against locally +admitted inputs. Caller-selected grant/baseline/estate/configuration cannot supply +these. Missing authoritative inputs or whole-descriptor correspondence must refuse +before protected backend effects. Expected native baseline/readback normalization, +conflict handling and source/time enforcement spanning revalidation-to-dispatch +remain unimplemented; source comparison alone is not native atomic CAS. + +The proposed Access package still hard-refuses owner admission. No initiator +projection or verified/member/MFA flags are produced here; existing readiness +refusal remains. Actual identity/PIP/representation/service/PDP/claim/consume, +Journal admission/replay/unknown state handling, independent provider confirmation, +restricted native adapter and whole-request/source/deadline enforcement must be +integrated. The proposed task token ceilings are120s/12uses, with at least one +cleanup use; this component records limits, it does not enforce native expiry/uses +or provide a hard elapsed timer. Trusted owner runtime must bound parser/import/ +filesystem/compiler work by remaining action budget before any actual call. +Token custody/revoke, temporary object removal, owner-private recovery after unknown +and truthful native readback remain gates. General lifecycle/apply/exec/default +migration and SecretsT06/RPF35T02/T12 cannot close from this source component. + +Validation:40 offline tests pass, covering real owner K8s/OIDC body generation, +exact catalogue unicode digest interoperability, refusal of alternate action/kind/ +path/body/ordinal, re-pinned root policy, nonapproved/unconfirmed/replayed CCR, +engine-managed catalogue, forged immutable plan, source drift/duplicates and +symlink/hardlink/writable source. No native action, credential/Secret access or owner checkout writes. diff --git a/scripts/task_execution_plan.py b/scripts/task_execution_plan.py new file mode 100644 index 0000000..9b33263 --- /dev/null +++ b/scripts/task_execution_plan.py @@ -0,0 +1,206 @@ +"""Owner-derived metadata pilot compiler. Source-only; never authorizes or dispatches. + +OwnerConfig is installed by the trusted owner, never deserialized from a request. +Six fixed calls on two structure objects. Recompile immediately before each call. +Canonical PDP request digest is opaque: the authenticated mediator verifies it. +""" +from __future__ import annotations +import hashlib +import json +import os +from pathlib import Path +import re +import stat +import types +from dataclasses import dataclass +from uuid import UUID +import yaml + +class Refused(Exception): pass + +def need(value, reason): + if not value: raise Refused(reason) + +def encoded(value): + return json.dumps(value,sort_keys=True,separators=(',', ':'),ensure_ascii=True,allow_nan=False).encode() + +def content_hash(raw): return hashlib.sha256(raw).hexdigest() +def tagged(raw): return 'sha256:'+content_hash(raw) +def digest_ok(value): return type(value) is str and re.fullmatch(r'sha256:[0-9a-f]{64}',value) is not None + +class UniqueLoader(yaml.SafeLoader): pass +def unique(loader,node): + result={} + for key,val in node.value: + k=loader.construct_object(key) + need(k not in result,'duplicate_owner_key') + result[k]=loader.construct_object(val) + return result +UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,unique) + +TARGET_KEYS=('kind','org','repo','mount','path','fields','mount_management','consumers', + 'delivery_modes','delivery_auth','delivery_config','auth_capability','workload_delivery') +REQUEST_KEYS={'action','kind','catalog_id','operation_id','request_digest','ccr_sha256','catalog_sha256','operations'} + +@dataclass(frozen=True) +class OwnerConfig: + platform_repo: Path + secrets_repo: Path + ccr_filename: str + catalog_filename: str + catalog_directory: Path + # Exact path/hash pairs, configured independently by the owner. No request paths. + source_pins: tuple[tuple[str,str], ...] + +@dataclass(frozen=True) +class Plan: + serialized: bytes + @property + def content_digest(self): return content_hash(self.serialized) # Journal uses bare content hash. + def metadata(self): + try: return json.loads(self.serialized) + except Exception: raise Refused('invalid_plan') from None + +@dataclass(frozen=True) +class Call: + ordinal: int + kind: str + method: str + path: str + payload: bytes + @property + def payload_sha256(self): return tagged(self.payload) + @property + def call_digest(self): + return content_hash(encoded({'ordinal':self.ordinal,'kind':self.kind,'method':self.method, + 'path':self.path,'payload_sha256':self.payload_sha256})) + +class Compiler: + def __init__(self, config: OwnerConfig): + need(type(config) is OwnerConfig,'owner_configuration_required') + self.config=config + for root in (config.platform_repo,config.secrets_repo,config.catalog_directory): + need(type(root) is Path or isinstance(root,Path),'owner_root_invalid') + need(root.is_absolute() and root.resolve()==root,'owner_root_invalid') + for name in (config.ccr_filename,config.catalog_filename): + need(type(name) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*\.yaml',name),'owner_filename_invalid') + self.ccr=config.platform_repo/'credential-change-requests'/config.ccr_filename + self.catalog=config.catalog_directory/config.catalog_filename + self.validator=config.platform_repo/'scripts/credential-change.py' + self.catalog_code=config.secrets_repo/'src/secrets_engine/catalog.py' + self.dependencies=[config.secrets_repo/'src/secrets_engine'/name for name in ('__init__.py','errors.py','redact.py')] + pins=dict(config.source_pins) + self.pins=types.MappingProxyType(pins) + need(len(self.pins)==len(config.source_pins) and all(type(k) is str and digest_ok(v) for k,v in self.pins.items()),'owner_pins_invalid') + # Compiler source is part of the owner plan, not just generated body hashes. + required={str(p) for p in [Path(__file__).resolve(),self.ccr,self.catalog,self.validator,self.catalog_code,*self.dependencies]} + need(required <= set(self.pins),'owner_source_pin_missing') + extra=set(self.pins)-required + need(len(extra)==1,'closed_owner_source_set_required') + artifact=Path(next(iter(extra))) + need(artifact.parent==config.platform_repo/'openbao/policies' + and re.fullmatch(r'workload-kv-read-[a-z0-9-]+\.hcl',artifact.name), + 'closed_owner_policy_source_required') + + def read(self,path): + need(str(path) in self.pins and path.is_absolute() and path.resolve()==path,'owner_source_path_invalid') + fd=None + try: + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + before=os.fstat(fd) + need(stat.S_ISREG(before.st_mode) and before.st_nlink==1 and not before.st_mode & 0o022 + and 0') + need(entry.id==ccr['access_frontdoor']['catalog_id'] and entry.stage=='prod' + and ccr['target']['environment']=='production' and entry.kind=='kv' + and entry.mount_management=='existing' + and entry.delivery_auth_management in {'none','existing'},'catalogue_not_metadata_pilot') + need(entry.mount==ccr['openbao']['mount'] and entry.path==ccr['openbao']['kv_path'][len(entry.mount)+1:] + and sorted(entry.fields)==sorted(ccr['openbao']['fields']) + and entry.approval.get('decision_ref')==ccr['id'],'catalogue_ccr_mismatch') + target={k:getattr(entry,k) for k in TARGET_KEYS} + auth=ccr['openbao']['auth'];role_path=f"auth/{auth['mount']}/role/{auth['role']}" + policy_path=f'sys/policies/acl/{policy_name}' + bodies=[b'',encoded({'policy':generated.rstrip()}),b'',b'',encoded(owner.auth_payload(ccr)),b''] + paths=[policy_path,policy_path,policy_path,role_path,role_path,role_path] + methods=['GET','PUT','GET','GET','PUT','GET'] + kinds=['policy_baseline','policy_write','policy_readback','role_baseline','auth_role_write','role_readback'] + calls=tuple(Call(i,kinds[i],methods[i],paths[i],bodies[i]) for i in range(6)) + need(all(re.fullmatch(r'(sys/policies/acl/workload-kv-read-[a-z0-9-]+|auth/(netkingdom|kubernetes)/role/[a-z0-9-]+)',c.path) for c in calls),'unsafe_structure_target') + # Detect source changes during existing validator/generator reads and catalog validation. + for p,raw in snapshot.items(): need(self.read(Path(p))==raw,'owner_source_changed') + return ccr,target,calls + except Refused: raise + except BaseException: raise Refused('owner_compilation_refused') from None + + def proposal(self): + ccr,target,calls=self._derive() + return {'action':'execute-ccr','kind':'workload-kv-read','catalog_id':ccr['access_frontdoor']['catalog_id'], + 'ccr_sha256':self.pins[str(self.ccr)],'catalog_sha256':tagged(json.dumps(target,sort_keys=True,separators=(',', ':'),ensure_ascii=False,allow_nan=False).encode('utf-8')), + 'catalog_target':json.loads(encoded(target)), + 'operations':[{'method':c.method,'path':c.path,'payload_sha256':c.payload_sha256} for c in calls]} + + def compile(self,request): + need(type(request) is dict and set(request)==REQUEST_KEYS,'closed_plan_request_required') + need(digest_ok(request['request_digest']),'canonical_request_digest_required') + try: need(str(UUID(request['operation_id']))==request['operation_id'],'operation_id_invalid') + except (ValueError,AttributeError,TypeError): raise Refused('operation_id_invalid') from None + expected=self.proposal() + need(all(request[k]==expected[k] for k in REQUEST_KEYS-{'operation_id','request_digest'}),'owner_plan_binding_mismatch') + return Plan(encoded({'schema':'platform-owner-typed-plan/v1','action':'execute-ccr','operation':'metadata-apply-ccr', + 'request':request,'catalog_target':expected['catalog_target'], + 'source_pins':dict(self.pins),'task_token_max_ttl':120,'task_token_max_uses':12, + 'cleanup_uses_reserved':1})) + + def before_call(self,plan,ordinal): + need(type(plan) is Plan and type(ordinal) is int and 0<=ordinal<6,'fixed_plan_ordinal_required') + try: meta=plan.metadata();fresh=self.compile(meta['request']) + except Refused: raise + except BaseException: raise Refused('invalid_plan') from None + need(fresh.serialized==plan.serialized,'immutable_plan_changed') + # Immediately re-derive owner payload bytes, and reconfirm immutable snapshot after derivation. + _,_,calls=self._derive();need(self.compile(meta['request']).serialized==plan.serialized,'immutable_plan_changed') + return calls[ordinal] diff --git a/tests/test_task_execution_plan.py b/tests/test_task_execution_plan.py new file mode 100644 index 0000000..ca43408 --- /dev/null +++ b/tests/test_task_execution_plan.py @@ -0,0 +1,146 @@ +import importlib.util,json,hashlib,shutil,sys,uuid,os +from pathlib import Path +import pytest,yaml +SOURCE=Path(__file__).resolve().parents[1]/'scripts/task_execution_plan.py' +spec=importlib.util.spec_from_file_location('task_execution_plan',SOURCE) +m=importlib.util.module_from_spec(spec);sys.modules[spec.name]=m;spec.loader.exec_module(m) +PLATFORM=Path(os.environ.get('PLATFORM_PLAN_TEST_SOURCE_ROOT',str(SOURCE.parents[1]))) +SECRETS=Path(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT',str(PLATFORM.parent/'secrets-engine'))) +@pytest.fixture +def compiler(tmp_path): + repo=tmp_path/'platform';(repo/'scripts').mkdir(parents=True);(repo/'credential-change-requests').mkdir();(repo/'openbao/policies').mkdir(parents=True) + shutil.copyfile(PLATFORM/'scripts/credential-change.py',repo/'scripts/credential-change.py') + ccr=yaml.safe_load((PLATFORM/'credential-change-requests/CCR-2026-0002-issue-core-ingestion-api-key.yaml').read_text()) + ccr['status']='approved';ccr['access_frontdoor'].update(readiness='approved-pending-apply',resolvable=False) + ccrfile=repo/'credential-change-requests/CCR-2026-0002-fixture.yaml';ccrfile.write_text(yaml.safe_dump(ccr)) + policy=repo/ccr['openbao']['policy_file'];shutil.copyfile(PLATFORM/ccr['openbao']['policy_file'],policy) + catalogdir=tmp_path/'catalog';catalogdir.mkdir();catalog=yaml.safe_load((SECRETS/'catalog/issue-core-ingestion-api-key.yaml').read_text()) + catalog['delivery_auth']['management']='existing' + catalogfile=catalogdir/'issue-core-ingestion-api-key.yaml';catalogfile.write_text(yaml.safe_dump(catalog)) + paths=[SOURCE,repo/'scripts/credential-change.py',ccrfile,policy,catalogfile,*[SECRETS/'src/secrets_engine'/name for name in ['catalog.py','__init__.py','errors.py','redact.py']]] + pins=tuple((str(p),m.tagged(p.read_bytes())) for p in paths) + return m.Compiler(m.OwnerConfig(repo,SECRETS,ccrfile.name,catalogfile.name,catalogdir,pins)) + +def request(c): + value=c.proposal();value.pop('catalog_target');value.update(operation_id=str(uuid.uuid4()),request_digest='sha256:'+'a'*64);return value + +def test_compiles_real_owner_generated_bodies_and_fixed_six_calls(compiler): + req=request(compiler);plan=compiler.compile(req) + calls=[compiler.before_call(plan,i) for i in range(6)] + assert [x.method for x in calls]==['GET','PUT','GET','GET','PUT','GET'] + assert len({x.path for x in calls})==2 + policy=json.loads(calls[1].payload)['policy'];assert 'capabilities = ["read"]' in policy + role=json.loads(calls[4].payload);assert role['policies']=='workload-kv-read-issue-core-runtime' + assert role['bound_service_account_namespaces']==['external-secrets'] + assert len(plan.content_digest)==64 and not plan.content_digest.startswith('sha256:') + assert all(x.payload_sha256.startswith('sha256:') and len(x.call_digest)==64 for x in calls) + +@pytest.mark.parametrize('field,value',[('action','apply'),('kind','service-jwt-login'),('catalog_id','another'),('ccr_sha256','sha256:'+'b'*64),('catalog_sha256','sha256:'+'b'*64),('request_digest','a'*64)]) +def test_altered_bindings_refuse(compiler,field,value): + req=request(compiler);req[field]=value + with pytest.raises(m.Refused):compiler.compile(req) + +@pytest.mark.parametrize('ordinal',[False,0.0,'0',-1,6]) +def test_fixed_ordinals_refuse(compiler,ordinal): + plan=compiler.compile(request(compiler)) + with pytest.raises(m.Refused):compiler.before_call(plan,ordinal) + +@pytest.mark.parametrize('field,value',[('method','DELETE'),('path','platform/data/workloads/keycape/x'),('payload_sha256','sha256:'+'c'*64)]) +def test_caller_operation_cannot_change_target_body_or_method(compiler,field,value): + req=request(compiler);req['operations'][1][field]=value + with pytest.raises(m.Refused):compiler.compile(req) + +@pytest.mark.parametrize('which',['ccr','catalog','policy','validator']) +def test_owner_source_drift_between_calls_refuses(compiler,which): + plan=compiler.compile(request(compiler));compiler.before_call(plan,0) + path={'ccr':compiler.ccr,'catalog':compiler.catalog,'policy':next(Path(p) for p in compiler.pins if p.endswith('.hcl')),'validator':compiler.validator}[which] + path.write_bytes(path.read_bytes()+b'\n# drift\n') + with pytest.raises(m.Refused):compiler.before_call(plan,1) + +@pytest.mark.parametrize('change',['extra','budget','request']) +def test_direct_plan_constructor_cannot_bypass_recompilation(compiler,change): + plan=compiler.compile(request(compiler));data=plan.metadata() + if change=='extra':data['caller_hcl']='path "*" {}' + elif change=='budget':data['task_token_max_uses']=999 + else:data['request']['operations'][3]['payload_sha256']='sha256:'+'d'*64 + with pytest.raises(m.Refused):compiler.before_call(m.Plan(m.encoded(data)),3) + + +def repin(c,path): + pins=dict(c.config.source_pins);pins[str(path)]=m.tagged(path.read_bytes()) + return m.Compiler(m.OwnerConfig(c.config.platform_repo,c.config.secrets_repo,c.config.ccr_filename,c.config.catalog_filename,c.config.catalog_directory,tuple(pins.items()))) + +@pytest.mark.parametrize('status',['in_flight','proposed','applied','verified','active','cancelled']) +def test_even_re_pinned_nonapproved_owner_record_refuses(compiler,status): + ccr=yaml.safe_load(compiler.ccr.read_text());ccr['status']=status;compiler.ccr.write_text(yaml.safe_dump(ccr)) + with pytest.raises(m.Refused):repin(compiler,compiler.ccr).proposal() + + +def test_source_role_policy_escalation_refuses_even_when_re_pinned(compiler): + ccr=yaml.safe_load(compiler.ccr.read_text());ccr['openbao']['auth']['policies']=['root'];compiler.ccr.write_text(yaml.safe_dump(ccr)) + with pytest.raises(m.Refused):repin(compiler,compiler.ccr).proposal() + + +def test_catalog_hash_agrees_with_actual_consumer_unicode_canonicalization(compiler): + catalog=yaml.safe_load(compiler.catalog.read_text());catalog['consumers'][0]['purpose']='café';compiler.catalog.write_text(yaml.safe_dump(catalog)) + compiler=repin(compiler,compiler.catalog) + from secrets_engine.execution_contract import catalog_target_digest + proposed=compiler.proposal();assert proposed['catalog_sha256']==catalog_target_digest(proposed['catalog_target']) + + +def test_caller_file_path_and_policy_fields_are_refused(compiler): + req=request(compiler);req['file']='/etc/passwd';req['policy']='path "*" {}' + with pytest.raises(m.Refused):compiler.compile(req) + + +def test_actual_inflight_ccr42_is_not_an_admitted_target(): + actual=yaml.safe_load((PLATFORM/'credential-change-requests/CCR-2026-0042-forgejo-netkingdom-oidc-consumer-reader.yaml').read_text()) + assert actual['status']=='in_flight' and 'auth' not in actual['openbao'] + +@pytest.mark.parametrize('kind',['symlink','hardlink','writable']) +def test_owner_policy_file_indirection_or_permissions_refuse(compiler,tmp_path,kind): + req=request(compiler) + path=next(Path(p) for p in compiler.pins if p.endswith('.hcl')) + if kind=='symlink': + copy=tmp_path/'outside-policy';copy.write_bytes(path.read_bytes());path.unlink();path.symlink_to(copy) + elif kind=='hardlink': + import os + os.link(path,tmp_path/'policy-link') + else:path.chmod(0o666) + with pytest.raises(m.Refused):compiler.compile(req) + + +def test_re_pinned_policy_body_escalation_refuses(compiler): + path=next(Path(p) for p in compiler.pins if p.endswith('.hcl')) + path.write_text('path "*" { capabilities = ["sudo", "read", "update"] }\n') + with pytest.raises(m.Refused):repin(compiler,path).proposal() + + +def test_duplicate_owner_yaml_key_refuses_even_when_re_pinned(compiler): + compiler.ccr.write_bytes(compiler.ccr.read_bytes()+b'\nstatus: approved\n') + with pytest.raises(m.Refused):repin(compiler,compiler.ccr).proposal() + + +def test_unconfirmed_claim_binding_refuses_even_when_re_pinned(compiler): + ccr=yaml.safe_load(compiler.ccr.read_text());ccr['openbao']['auth']['bound_claims_confirmed']=False + compiler.ccr.write_text(yaml.safe_dump(ccr)) + with pytest.raises(m.Refused):repin(compiler,compiler.ccr).proposal() + + +def test_catalogue_engine_managed_approle_is_not_silently_generic_apply(compiler): + data=yaml.safe_load(compiler.catalog.read_text());data['delivery_auth']['management']='engine' + compiler.catalog.write_text(yaml.safe_dump(data)) + with pytest.raises(m.Refused):repin(compiler,compiler.catalog).proposal() + + +def test_oidc_role_body_reuses_owner_auth_payload_and_existing_mount(compiler): + ccr=yaml.safe_load(compiler.ccr.read_text()) + source=yaml.safe_load((PLATFORM/'credential-change-requests/CCR-2026-0034.yaml').read_text()) + ccr['openbao']['auth']=source['openbao']['auth'];ccr['openbao']['auth']['role']='synthetic-operator-workload-kv-read' + ccr['openbao']['auth']['policies']=[ccr['openbao']['policy_name']] + compiler.ccr.write_text(yaml.safe_dump(ccr));compiler=repin(compiler,compiler.ccr) + plan=compiler.compile(request(compiler));call=compiler.before_call(plan,4) + role=json.loads(call.payload) + assert call.path=='auth/netkingdom/role/synthetic-operator-workload-kv-read' + assert role['role_type']=='oidc' and role['policies']=='workload-kv-read-issue-core-runtime' + assert role['bound_claims']==source['openbao']['auth']['bound_claims'] diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index a1791cc..7d111c5 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -932,7 +932,7 @@ obligations are retained in T12 and existing T02 rather than a finished residual ```task id: RPF-WP-0035-T12 -status: todo +status: progress priority: high depends_on: [RPF-WP-0035-T11] state_hub_task_id: "914bf383-c13f-5247-9af0-91d7e3a4784a" @@ -1172,3 +1172,23 @@ coding-agent producer/client/source, native role/plugin/leeway/route/issuer readiness/Argo/maintenanceexception, refusal/revoke/restoration/readback and unknown owner recovery remain prerequisites. Warden quietlane cannot carry ready controlstdout; no transport/custody/native grant. No owner checkout/liveSSH/native. + + +2026-10-11 RPF35-T12 source implementation progress: durable journal source v4 +was independently accepted/committed; owner-derived typed plan compiler component +v2 is prepared at immutable manifest +`41cda74ab32384c789ea8a760475671aadad4e5121335c8f60567ff53d66665b` +with40 offline tests. Completed own bounded source-preparation ADHOCT02 anchor +was persisted at7adbcb4 before the qualified Platform contract/readiness receipt. +T12 progresses for actual source implementation underway, not native acceptance +or full descriptor/authority readiness. The compiler derives local CCR/HCL/role +bodies, matches actual independently pinned Secrets catalogue and revalidates +source/body before fixed calls; no body/target chosen by service is dispatched. +Independent technical integration review and exact full ExecutionContract/owner +grant/baseline/estate/deadline matching, initiating human/User/Tenant PIP/transport +representation/service identity, served PDP/claim/consume correspondence, journal +call dispatch/restricted adapter, native token TTL/uses/custody/revoke/cleanup and +owner-private unknown recovery remain unimplemented integration/native gates. +Actual CCR42 has no eligible approved pilot/catalogue; no source metadata is +invented. T02 and original negative/capability/native evidence remain unchanged; +T11/T13 history retained, no defaultflip/nativego or SecretsT06 closure follows.