From e19bcd1f3d2f02a64c82932bfa29ac61a2bf135a Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 1 Nov 2026 22:42:33 +0100 Subject: [PATCH] docs: preserve reviewed NetKingdom session artifacts --- .../b2-b3/actual-python-source-smoke.py | 90 + .../original-approval-claim.json | 39 + .../b2-b3/actual-python-source/receipt.json | 18 + .../b2-b3/actual-python-source/request.json | 142 + .../b2-b3/actual-python-source/response.json | 229 ++ .../signature-verification.json | 1 + .../actual-python-source/source-record.json | 229 ++ .../synthetic-public-trust.json | 5 + .../b2-b3/existing-producer-proof-verifier.go | 54 + .../owner-source-review-addendum-r1.json | 22 + .../b2-b3/owner-source-review-r1.json | 30 + .../b2-b3/r2/actual-python-source-smoke.py | 90 + .../original-approval-claim.json | 39 + .../r2/actual-python-source/receipt.json | 22 + .../r2/actual-python-source/request.json | 142 + .../r2/actual-python-source/response.json | 229 ++ .../signature-verification.json | 1 + .../actual-python-source/source-record.json | 229 ++ .../synthetic-public-trust.json | 5 + .../b2-b3/r2/owner-temporal-review.json | 39 + .../b2-b3/r2/r1-to-r2-temporal.patch | 283 ++ .../r2/trusted-source-r2-over-b1-r2.patch | 2458 ++++++++++++++++ .../r3/actual-retained-python-source-smoke.py | 101 + .../allow-signature-verification.json | 1 + .../claimfree-request.json | 103 + .../original-approval-claim.json | 39 + .../preapproval-receipt.json | 196 ++ .../preapproval-signature-verification.json | 1 + .../preapproval-source-record.json | 196 ++ .../receipt.json | 29 + .../request.json | 142 + .../response.json | 251 ++ .../source-record.json | 251 ++ .../r3/initial-proof-assertion-failure.json | 1 + .../b2-b3/r3/owner-preapproval-review.json | 36 + .../b2-b3/r3/r2-to-r3-preapproval.patch | 269 ++ .../r3/trusted-source-r3-over-b1-r2.patch | 2592 +++++++++++++++++ .../b2-b3/trusted-source-over-b1-r2.patch | 2332 +++++++++++++++ .../b2-preparation/manifest.json | 14 + .../raw-representation-probe.go | 11 + .../raw-representation-results-r1.json | 136 + .../run-representation-probe.py | 20 + .../existing-ci-public-metadata-r1.json | 28 + 43 files changed, 11145 insertions(+) create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source-smoke.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/original-approval-claim.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/request.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/response.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/signature-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/source-record.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/synthetic-public-trust.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/existing-producer-proof-verifier.go create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-addendum-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source-smoke.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/original-approval-claim.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/request.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/response.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/signature-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/source-record.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/synthetic-public-trust.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/owner-temporal-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/r1-to-r2-temporal.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/trusted-source-r2-over-b1-r2.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source-smoke.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/allow-signature-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/claimfree-request.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/original-approval-claim.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-signature-verification.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-source-record.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/receipt.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/request.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/response.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/source-record.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/initial-proof-assertion-failure.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/owner-preapproval-review.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/r2-to-r3-preapproval.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/trusted-source-r3-over-b1-r2.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/trusted-source-over-b1-r2.patch create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/manifest.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-probe.go create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-results-r1.json create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/run-representation-probe.py create mode 100644 docs/changes/netkingdom-target-2026-10-16/access-native-source/existing-ci-public-metadata-r1.json diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source-smoke.py b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source-smoke.py new file mode 100644 index 00000000..0c47d566 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source-smoke.py @@ -0,0 +1,90 @@ +"""Disposable synthetic proof: actual Approval App HTTPS -> actual Access serve. +No native config, credential environment, approval or backend effect is used. +""" +import base64, hashlib, http.server, io, json, os, pathlib, socket, ssl, subprocess, sys, tempfile, threading, time +from datetime import datetime, timedelta, timezone +from wsgiref.simple_server import make_server, WSGIRequestHandler +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +sys.path.insert(0, '/home/worsch/approval-engine') +sys.path.insert(0, '/home/worsch/secrets-engine/src') +from approval_engine.api import App +from approval_engine.auth import Identity, StaticTokenAuthenticator +from approval_engine.store import Engine +from secrets_engine.authorization import request_digest, approval_binding_digest +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +OUT=pathlib.Path(sys.argv[1]);OUT.mkdir(exist_ok=True) +def digest(raw):return 'sha256:'+hashlib.sha256(raw).hexdigest() +def write(path,raw,mode=0o600):path.write_bytes(raw);path.chmod(mode) +def identity(subject,kind,scopes):return Identity(subject=subject,issuer='synthetic-test-only',audiences=('approval-engine',),principal_type=kind,tenant='tenant:platform',roles=frozenset(),scopes=frozenset(scopes),assurance={'level':'synthetic'},evidence_ref='synthetic-test-only',tenant_source='directory') +class Quiet(WSGIRequestHandler): + def log_message(self,*args):pass +class Review(http.server.BaseHTTPRequestHandler): + def do_POST(self): + raw=self.rfile.read(int(self.headers['Content-Length']));body=json.loads(raw) + assert self.path=='/apis/authentication.k8s.io/v1/tokenreviews' + assert self.headers['Authorization']=='Bearer synthetic-reviewer' + assert body['spec']['audiences']==['flex-auth','access-engine'] + self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps({'status':{'authenticated':True,'audiences':['flex-auth'],'user':{'username':'system:serviceaccount:secrets-engine:secrets-engine'}}}).encode()) + def log_message(self,*args):pass +with tempfile.TemporaryDirectory(prefix='access-actual-python-source-') as folder: + root=pathlib.Path(folder);now=datetime.now(timezone.utc) + key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'localhost')]);cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('localhost')]),critical=False).sign(key,hashes.SHA256())) + ca=root/'ca.pem';private=root/'tls-key.pem';write(ca,cert.public_bytes(serialization.Encoding.PEM));write(private,key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())) + tls=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);tls.load_cert_chain(ca,private) + ctx=ssl.create_default_context(cafile=str(ca)) + def start(server):server.socket=tls.wrap_socket(server.socket,server_side=True);threading.Thread(target=server.serve_forever,daemon=True).start();return 'https://localhost:'+str(server.server_port) + review=http.server.HTTPServer(('127.0.0.1',0),Review);review_origin=start(review) + engine=Engine(root/'approval.sqlite3',freshness_ttl=60) + auth=StaticTokenAuthenticator({'synthetic-creator':identity('secrets-engine','service',{'approval:create'}),'synthetic-human':identity('synthetic-human','human',{'approval:approve'}),'synthetic-reader':identity('synthetic-access-reader','service',{'approval:read'})}) + app=App(engine,auth) + source=make_server('127.0.0.1',0,app,handler_class=Quiet);origin=start(source) + def source_call(method,path,body,token): + request=Request(origin+path,data=json.dumps(body).encode() if body is not None else None,method=method,headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'}) + with urlopen(request,context=ctx,timeout=3) as response:return response.status,json.load(response) + # Reuse existing owner interoperability contract generator; it has synthetic metadata only. + generated=subprocess.check_output([sys.executable,'/tmp/access-trusted-b2-b3/tools/service_task_execution_fixture.py'],env={'PATH':os.defpath}) + request=json.loads(generated)['request'];request['subject']['tenant']='tenant:platform';request['resource']['tenant']='tenant:platform';request['context']['purpose']='actual Python App source-only synthetic proof';del request['context']['approval'] + native={'action':request['action'],'actor':'secrets-engine','principal':'secrets-engine','purpose':request['context']['purpose'],'target':request['resource']} + aid='aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + status,created=source_call('POST','/v1/approvals',{'id':aid,'binding':native,'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()},'required_count':1,'pdp_digest':approval_binding_digest(request),'pdp_path':True,'human_control':True},'synthetic-creator');assert status==201 + status,_=source_call('POST',f'/v1/approvals/{aid}/entries',{},'synthetic-human');assert status==200 + status,claim=source_call('GET',f'/v1/approvals/{aid}/claim',None,'synthetic-reader');assert status==200 and claim['valid_now'] is True and claim['binding']['human_control'] is True + request['context']['approval']=claim + seed=root/'seed';write(seed,base64.urlsafe_b64encode(bytes([19])*32).rstrip(b'='),0o440) + reviewer_token=root/'review-token';write(reviewer_token,b'synthetic-reviewer',0o440) + reader_token=root/'reader-token';write(reader_token,b'synthetic-reader',0o440) + registry=root/'registry.json';write(registry,b'{}') + config=root/'source.json';write(config,json.dumps({'source_id':'synthetic-actual-approval-app','source_revision':'actual-owner-python-source-fixture','origin':origin,'store_tenant':'tenant:platform','ca_file':str(ca),'token_file':str(reader_token)}).encode()) + listener=socket.socket();listener.bind(('127.0.0.1',0));port=listener.getsockname()[1];listener.close() + record=root/'record.jsonl';url=f'http://127.0.0.1:{port}' + command=['/tmp/netkingdom-access-trusted-r1','serve','--addr',f'127.0.0.1:{port}','--registry',str(registry),'--policy','/tmp/access-trusted-b2-b3/examples/secrets-engine-service-task-execution-v2/policy_package.md','--log',str(record),'--caller-auth-mode','enforce','--caller-audience','flex-auth','--caller-kubernetes-url',review_origin,'--caller-reviewer-token-file',str(reviewer_token),'--caller-ca-file',str(ca),'--caller-binding','secrets-engine=system:serviceaccount:secrets-engine:secrets-engine','--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-v2','--approval-source-file',str(config)] + child=subprocess.Popen(command,env={},stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) + try: + for _ in range(100): + try: + with urlopen(url+'/healthz',timeout=.2):break + except Exception:time.sleep(.02) + jwt='synthetic.'+base64.urlsafe_b64encode(json.dumps({'exp':int((now+timedelta(minutes=2)).timestamp())}).encode()).decode().rstrip('=')+'.synthetic' + def check(value): + req=Request(url+'/v1/check',data=json.dumps(value).encode(),headers={'Authorization':'Bearer '+jwt,'Content-Type':'application/json'}) + try: + with urlopen(req,timeout=5) as response:return response.status,response.read() + except HTTPError as error:return error.code,error.read() + status,raw=check(request);assert status==200,(status,raw) + response=json.loads(raw);assert response['effect']=='allow' and response['signature']['mode']=='signed' + assert response['binding']['context']==request['context'];assert response['binding']['submitted_request_digest']==request_digest(request);assert response['binding']['approval_binding_digest']==approval_binding_digest(request) + assert response['provenance']['approval_observation']['source_id']=='synthetic-actual-approval-app' + saved=record.read_bytes();assert json.loads(saved)==response + # Actual producer state transition is fetched anew and cannot be rescued by carried claim. + engine.revoke(aid) + refused,_=check(request);assert refused!=200 and record.read_bytes()==saved + for name,value in [('request.json',request),('response.json',response),('source-record.json',json.loads(saved)),('original-approval-claim.json',claim)]:write(OUT/name,(json.dumps(value,indent=2)+'\n').encode(),0o644) + receipt={'scope':'disposable synthetic only','actual_source':'Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine','actual_access':'production serve binary from frozen B2/B3 R1','allow':True,'original_context_unchanged':True,'published_digest_match':True,'returned_source_record_match':True,'actual_source_revoke_refused':True,'no_record_after_refusal':True,'native_admission':False,'files_sha256':{p.name:digest(p.read_bytes()) for p in OUT.iterdir() if p.suffix=='.json'}} + write(OUT/'receipt.json',(json.dumps(receipt,indent=2)+'\n').encode(),0o644);print(json.dumps(receipt,indent=2)) + finally: + child.terminate();child.wait(timeout=5);source.shutdown();review.shutdown();engine.close() diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/original-approval-claim.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/original-approval-claim.json new file mode 100644 index 00000000..d18c1da2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/original-approval-claim.json @@ -0,0 +1,39 @@ +{ + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T09:07:33+00:00", + "observed_at": "2026-10-16T09:06:33+00:00", + "ttl_seconds": 60 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T09:09:33.329626+00:00", + "not_before": "2026-10-16T09:05:33.329626+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/receipt.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/receipt.json new file mode 100644 index 00000000..015100ec --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/receipt.json @@ -0,0 +1,18 @@ +{ + "scope": "disposable synthetic only", + "actual_source": "Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine", + "actual_access": "production serve binary from frozen B2/B3 R1", + "allow": true, + "original_context_unchanged": true, + "published_digest_match": true, + "returned_source_record_match": true, + "actual_source_revoke_refused": true, + "no_record_after_refusal": true, + "native_admission": false, + "files_sha256": { + "source-record.json": "sha256:592b375208d6c2d5add685df0e789b3e0adc0794989712871a518b1a6800e1dc", + "response.json": "sha256:592b375208d6c2d5add685df0e789b3e0adc0794989712871a518b1a6800e1dc", + "original-approval-claim.json": "sha256:002221b16301dbc61767a926e950dad594bb3e6b6f6c1d9e038deeb56ca9dd83", + "request.json": "sha256:47793f3bad156f0b50e8d9bb663c585c5fce779b87a59aac0887b4f2dfe8799f" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/request.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/request.json new file mode 100644 index 00000000..c08d7ae1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/request.json @@ -0,0 +1,142 @@ +{ + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod" + }, + "tenant": "tenant:platform" + }, + "context": { + "execution_contract": { + "backend_budget": { + "ttl_seconds": 120, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T09:08:13.606884+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "human_control": true, + "service_actor_binding": { + "version": "proposed-service-task-caller/v1", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform" + }, + "purpose": "actual Python App source-only synthetic proof", + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T09:07:33+00:00", + "observed_at": "2026-10-16T09:06:33+00:00", + "ttl_seconds": 60 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T09:09:33.329626+00:00", + "not_before": "2026-10-16T09:05:33.329626+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/response.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/response.json new file mode 100644 index 00000000..d98a2431 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/response.json @@ -0,0 +1,229 @@ +{ + "id": "decision:f85ef006900157fe", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T09:07:33+00:00", + "observed_at": "2026-10-16T09:06:33+00:00", + "ttl_seconds": 60 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T09:09:33.329626+00:00", + "not_before": "2026-10-16T09:05:33.329626+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 120, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T09:08:13.606884+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:18db34b2bad28d655d96ce62e3a0a62bf54f47aea6041f41068b404497fec001", + "approval_binding_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "submitted_request_digest": "sha256:18db34b2bad28d655d96ce62e3a0a62bf54f47aea6041f41068b404497fec001" + }, + "lifetime": { + "kind": "ttl", + "ttl": "15s", + "not_before": "2026-10-16T09:06:33Z", + "expires_at": "2026-10-16T09:06:48Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": false + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:fc5a8769886b72006ba298bc0799741d0f1ed327dde9e36a6c35d26be4ea0bdc", + "registry_snapshot_digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "input_claim_digests": { + "context": "sha256:908f4edd173f06b6728bde1351316f0dcb644f8c209ef6459aa3b53c1ee0bfcb" + }, + "decision_time": "2026-10-16T09:06:33Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:87e9190aed4ba05323551621cd3e0567357e2f41de9af356f2f8b77d57d63b2d", + "claim_digest": "sha256:c4eefea24458257b1d10a7186729a271e8a219f1b9763294eab01eb832aaaaa2", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T09:06:33Z", + "not_after": "2026-10-16T09:07:33Z", + "validated_at": "2026-10-16T09:06:33.709710196Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T09:08:33Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "4gO4Cqr0fCiIP3Az-Tz8XP0qmgmxfU4S8Oz7y1vKgE7gsV7swNXcJ3hzs1wx1-H3mdoGEQz13URiq94cZO5KBA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/signature-verification.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/signature-verification.json new file mode 100644 index 00000000..056d6192 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/signature-verification.json @@ -0,0 +1 @@ +{"existing_producer_signature_verified":true,"independent_synthetic_public_fixture":true,"returned_record_canonical_bytes_match":true,"new_signature_implementation":false} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/source-record.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/source-record.json new file mode 100644 index 00000000..d98a2431 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/source-record.json @@ -0,0 +1,229 @@ +{ + "id": "decision:f85ef006900157fe", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T09:07:33+00:00", + "observed_at": "2026-10-16T09:06:33+00:00", + "ttl_seconds": 60 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T09:09:33.329626+00:00", + "not_before": "2026-10-16T09:05:33.329626+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 120, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T09:08:13.606884+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:18db34b2bad28d655d96ce62e3a0a62bf54f47aea6041f41068b404497fec001", + "approval_binding_digest": "sha256:945b3b5c014370f1a64dbca47c44a5c85124b294b6e6a11e4974c958530aa86b", + "submitted_request_digest": "sha256:18db34b2bad28d655d96ce62e3a0a62bf54f47aea6041f41068b404497fec001" + }, + "lifetime": { + "kind": "ttl", + "ttl": "15s", + "not_before": "2026-10-16T09:06:33Z", + "expires_at": "2026-10-16T09:06:48Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": false + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:fc5a8769886b72006ba298bc0799741d0f1ed327dde9e36a6c35d26be4ea0bdc", + "registry_snapshot_digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "input_claim_digests": { + "context": "sha256:908f4edd173f06b6728bde1351316f0dcb644f8c209ef6459aa3b53c1ee0bfcb" + }, + "decision_time": "2026-10-16T09:06:33Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:87e9190aed4ba05323551621cd3e0567357e2f41de9af356f2f8b77d57d63b2d", + "claim_digest": "sha256:c4eefea24458257b1d10a7186729a271e8a219f1b9763294eab01eb832aaaaa2", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T09:06:33Z", + "not_after": "2026-10-16T09:07:33Z", + "validated_at": "2026-10-16T09:06:33.709710196Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T09:08:33Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "4gO4Cqr0fCiIP3Az-Tz8XP0qmgmxfU4S8Oz7y1vKgE7gsV7swNXcJ3hzs1wx1-H3mdoGEQz13URiq94cZO5KBA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/synthetic-public-trust.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/synthetic-public-trust.json new file mode 100644 index 00000000..234e3d7f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/actual-python-source/synthetic-public-trust.json @@ -0,0 +1,5 @@ +{ + "scope": "synthetic disposable fixture only; no native key admission", + "kid": "synthetic-v2", + "public_key": "Zs1gi5KLiOUODv6qM_rxxDzv4HKUsLh-n-CrpqPPdjM" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/existing-producer-proof-verifier.go b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/existing-producer-proof-verifier.go new file mode 100644 index 00000000..650f1513 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/existing-producer-proof-verifier.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/netkingdom/access-engine/internal/sign" + "os" +) + +func main() { + raw, err := os.ReadFile(os.Args[1]) + if err != nil { + panic(err) + } + trust, err := os.ReadFile(os.Args[2]) + if err != nil { + panic(err) + } + var public struct { + KID string `json:"kid"` + Public string `json:"public_key"` + } + if json.Unmarshal(trust, &public) != nil { + panic("trust decode") + } + key, err := base64.RawURLEncoding.DecodeString(public.Public) + if err != nil { + panic(err) + } + envelope, err := sign.DecodeEnvelope(raw) + if err != nil { + panic(err) + } + if err := sign.Verify(envelope, map[string]ed25519.PublicKey{public.KID: ed25519.PublicKey(key)}); err != nil { + panic(err) + } + record, err := os.ReadFile(os.Args[3]) + if err != nil { + panic(err) + } + source, err := sign.DecodeEnvelope(record) + if err != nil { + panic(err) + } + a, _ := sign.CanonicalBytes(envelope) + b, _ := sign.CanonicalBytes(source) + if !bytes.Equal(a, b) { + panic("canonical record mismatch") + } + fmt.Println(`{"existing_producer_signature_verified":true,"independent_synthetic_public_fixture":true,"returned_record_canonical_bytes_match":true,"new_signature_implementation":false}`) +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-addendum-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-addendum-r1.json new file mode 100644 index 00000000..36071393 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-addendum-r1.json @@ -0,0 +1,22 @@ +{ + "supersedes_review_caveat": "owner-source-review-r1.json remaining_proof actual Python App/Engine source fixture is now complete", + "actual_producer_proof": "actual-python-source/receipt.json", + "signature_proof": "actual-python-source/signature-verification.json", + "remaining_proof": [ + "independent source review", + "strict Secrets refreshed producer source/bundle acceptance", + "Platform exact successor whole-stack positive and negatives", + "owner-admitted native source/custody/key/image provenance and rollout separately" + ], + "native_acceptance": false, + "artifact_sha256": { + "actual-python-source/original-approval-claim.json": "002221b16301dbc61767a926e950dad594bb3e6b6f6c1d9e038deeb56ca9dd83", + "actual-python-source/receipt.json": "11d114aa8f8e7956fc2231de9ea91baf887c90bf7251d8f703b0881b49e23284", + "actual-python-source/request.json": "47793f3bad156f0b50e8d9bb663c585c5fce779b87a59aac0887b4f2dfe8799f", + "actual-python-source/response.json": "592b375208d6c2d5add685df0e789b3e0adc0794989712871a518b1a6800e1dc", + "actual-python-source/signature-verification.json": "cedaa2294985c0d793f1ee9bf31ca34f6dd7ef304226166feec3b96820422066", + "actual-python-source/source-dependencies.json": "0c9a5ec99aaefdf775a4510cabff3ccd75f0d35efb1b0819dd15367f8a6c3c0f", + "actual-python-source/source-record.json": "592b375208d6c2d5add685df0e789b3e0adc0794989712871a518b1a6800e1dc", + "actual-python-source/synthetic-public-trust.json": "08bee627543864ba29aa422cdf7c73ccffd864f9a40cd5022b0cecb948207072" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-r1.json new file mode 100644 index 00000000..c11d824c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/owner-source-review-r1.json @@ -0,0 +1,30 @@ +{ + "owner": "Access Engine", + "status": "source proposal ready for independent coordinator and cross-owner review", + "implementation": "fixed source-authenticated Approval PIP reader + unchanged owner canonical/fact helper + typed existing PDP evaluator; no new PDP", + "synthetic_positive": "Actual HTTP handler and production serve subprocess, real TLS TokenReview and fixed TLS Approval-source fixture; signed ALLOW, exact original context and numeric kind, published digest compatibility and response/JSONL parity", + "negatives": [ + "redirects", + "source duplicates", + "source consumed", + "source expiry", + "stale original despite fresh provider", + "native integer/float authority mutation", + "invalid source media/encoding/UTF8", + "bounded response and context deadline", + "unsafe mounted token permissions", + "request-selected origin/path/tenant", + "generic/missing typed path" + ], + "numeric_replay": "Original numeric representation retained in request and authoritative registry, unchanged Approval helper canonicalizes native act, digest-only existing PDP projection, unchanged signature Marshal; JSONL/outbox replay retain numeric signed bytes", + "remaining_proof": [ + "independent source review", + "actual Python App/Engine source fixture beyond current HTTP schema/helper fixture", + "strict Secrets refreshed producer source/bundle acceptance", + "Platform exact successor whole-stack positive and negatives", + "owner-admitted native source/custody/key/image provenance and rollout separately" + ], + "native_acceptance": false, + "runtime_changes": false, + "policy_v1_unchanged": true +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source-smoke.py b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source-smoke.py new file mode 100644 index 00000000..d4cbbd42 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source-smoke.py @@ -0,0 +1,90 @@ +"""Disposable synthetic proof: actual Approval App HTTPS -> actual Access serve. +No native config, credential environment, approval or backend effect is used. +""" +import base64, hashlib, http.server, io, json, os, pathlib, socket, ssl, subprocess, sys, tempfile, threading, time +from datetime import datetime, timedelta, timezone +from wsgiref.simple_server import make_server, WSGIRequestHandler +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +sys.path.insert(0, '/home/worsch/approval-engine') +sys.path.insert(0, '/home/worsch/secrets-engine/src') +from approval_engine.api import App +from approval_engine.auth import Identity, StaticTokenAuthenticator +from approval_engine.store import Engine +from secrets_engine.authorization import request_digest, approval_binding_digest +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +OUT=pathlib.Path(sys.argv[1]);OUT.mkdir(exist_ok=True) +def digest(raw):return 'sha256:'+hashlib.sha256(raw).hexdigest() +def write(path,raw,mode=0o600):path.write_bytes(raw);path.chmod(mode) +def identity(subject,kind,scopes):return Identity(subject=subject,issuer='synthetic-test-only',audiences=('approval-engine',),principal_type=kind,tenant='tenant:platform',roles=frozenset(),scopes=frozenset(scopes),assurance={'level':'synthetic'},evidence_ref='synthetic-test-only',tenant_source='directory') +class Quiet(WSGIRequestHandler): + def log_message(self,*args):pass +class Review(http.server.BaseHTTPRequestHandler): + def do_POST(self): + raw=self.rfile.read(int(self.headers['Content-Length']));body=json.loads(raw) + assert self.path=='/apis/authentication.k8s.io/v1/tokenreviews' + assert self.headers['Authorization']=='Bearer synthetic-reviewer' + assert body['spec']['audiences']==['flex-auth','access-engine'] + self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps({'status':{'authenticated':True,'audiences':['flex-auth'],'user':{'username':'system:serviceaccount:secrets-engine:secrets-engine'}}}).encode()) + def log_message(self,*args):pass +with tempfile.TemporaryDirectory(prefix='access-actual-python-source-') as folder: + root=pathlib.Path(folder);now=datetime.now(timezone.utc) + key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'localhost')]);cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('localhost')]),critical=False).sign(key,hashes.SHA256())) + ca=root/'ca.pem';private=root/'tls-key.pem';write(ca,cert.public_bytes(serialization.Encoding.PEM));write(private,key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())) + tls=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);tls.load_cert_chain(ca,private) + ctx=ssl.create_default_context(cafile=str(ca)) + def start(server):server.socket=tls.wrap_socket(server.socket,server_side=True);threading.Thread(target=server.serve_forever,daemon=True).start();return 'https://localhost:'+str(server.server_port) + review=http.server.HTTPServer(('127.0.0.1',0),Review);review_origin=start(review) + engine=Engine(root/'approval.sqlite3',freshness_ttl=120) + auth=StaticTokenAuthenticator({'synthetic-creator':identity('secrets-engine','service',{'approval:create'}),'synthetic-human':identity('synthetic-human','human',{'approval:approve'}),'synthetic-reader':identity('synthetic-access-reader','service',{'approval:read'})}) + app=App(engine,auth) + source=make_server('127.0.0.1',0,app,handler_class=Quiet);origin=start(source) + def source_call(method,path,body,token): + request=Request(origin+path,data=json.dumps(body).encode() if body is not None else None,method=method,headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'}) + with urlopen(request,context=ctx,timeout=3) as response:return response.status,json.load(response) + # Reuse existing owner interoperability contract generator; it has synthetic metadata only. + generated=subprocess.check_output([sys.executable,'/tmp/access-trusted-b2-b3-r2/tools/service_task_execution_fixture.py'],env={'PATH':os.defpath}) + request=json.loads(generated)['request'];request['subject']['tenant']='tenant:platform';request['resource']['tenant']='tenant:platform';request['context']['purpose']='actual Python App source-only synthetic proof';del request['context']['approval'];request['context']['execution_contract']['backend_budget']['ttl_seconds']=30;request['context']['execution_contract']['operation_deadline']=(now+timedelta(seconds=60)).replace(microsecond=0).isoformat() + native={'action':request['action'],'actor':'secrets-engine','principal':'secrets-engine','purpose':request['context']['purpose'],'target':request['resource']} + aid='aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + status,created=source_call('POST','/v1/approvals',{'id':aid,'binding':native,'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()},'required_count':1,'pdp_digest':approval_binding_digest(request),'pdp_path':True,'human_control':True},'synthetic-creator');assert status==201 + status,_=source_call('POST',f'/v1/approvals/{aid}/entries',{},'synthetic-human');assert status==200 + status,claim=source_call('GET',f'/v1/approvals/{aid}/claim',None,'synthetic-reader');assert status==200 and claim['valid_now'] is True and claim['binding']['human_control'] is True + request['context']['approval']=claim + seed=root/'seed';write(seed,base64.urlsafe_b64encode(bytes([19])*32).rstrip(b'='),0o440) + reviewer_token=root/'review-token';write(reviewer_token,b'synthetic-reviewer',0o440) + reader_token=root/'reader-token';write(reader_token,b'synthetic-reader',0o440) + registry=root/'registry.json';write(registry,b'{}') + config=root/'source.json';write(config,json.dumps({'source_id':'synthetic-actual-approval-app','source_revision':'actual-owner-python-source-fixture','origin':origin,'store_tenant':'tenant:platform','ca_file':str(ca),'token_file':str(reader_token)}).encode()) + listener=socket.socket();listener.bind(('127.0.0.1',0));port=listener.getsockname()[1];listener.close() + record=root/'record.jsonl';url=f'http://127.0.0.1:{port}' + command=['/tmp/netkingdom-access-trusted-r2','serve','--addr',f'127.0.0.1:{port}','--registry',str(registry),'--policy','/tmp/access-trusted-b2-b3-r2/examples/secrets-engine-service-task-execution-v2/policy_package.md','--log',str(record),'--caller-auth-mode','enforce','--caller-audience','flex-auth','--caller-kubernetes-url',review_origin,'--caller-reviewer-token-file',str(reviewer_token),'--caller-ca-file',str(ca),'--caller-binding','secrets-engine=system:serviceaccount:secrets-engine:secrets-engine','--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-v2','--approval-source-file',str(config)] + child=subprocess.Popen(command,env={},stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) + try: + for _ in range(100): + try: + with urlopen(url+'/healthz',timeout=.2):break + except Exception:time.sleep(.02) + jwt='synthetic.'+base64.urlsafe_b64encode(json.dumps({'exp':int((now+timedelta(minutes=2)).timestamp())}).encode()).decode().rstrip('=')+'.synthetic' + def check(value): + req=Request(url+'/v1/check',data=json.dumps(value).encode(),headers={'Authorization':'Bearer '+jwt,'Content-Type':'application/json'}) + try: + with urlopen(req,timeout=5) as response:return response.status,response.read() + except HTTPError as error:return error.code,error.read() + status,raw=check(request);assert status==200,(status,raw) + response=json.loads(raw);assert response['effect']=='allow' and response['signature']['mode']=='signed' + assert response['lifetime']['expires_at']==request['context']['execution_contract']['operation_deadline'].replace('+00:00','Z');assert response['binding']['context']==request['context'];assert response['binding']['submitted_request_digest']==request_digest(request);assert response['binding']['approval_binding_digest']==approval_binding_digest(request) + assert response['provenance']['approval_observation']['source_id']=='synthetic-actual-approval-app' + saved=record.read_bytes();assert json.loads(saved)==response + # Actual producer state transition is fetched anew and cannot be rescued by carried claim. + engine.revoke(aid) + refused,_=check(request);assert refused!=200 and record.read_bytes()==saved + for name,value in [('request.json',request),('response.json',response),('source-record.json',json.loads(saved)),('original-approval-claim.json',claim)]:write(OUT/name,(json.dumps(value,indent=2)+'\n').encode(),0o644) + receipt={'scope':'disposable synthetic only','actual_source':'Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine','actual_access':'production serve binary from B2/B3 R2 temporal successor','allow':True,'backend_ttl_seconds':30,'operation_authority_seconds':60,'explicit_approval_freshness_seconds':120,'decision_expiry_equals_exact_operation_deadline':True,'original_context_unchanged':True,'published_digest_match':True,'returned_source_record_match':True,'actual_source_revoke_refused':True,'no_record_after_refusal':True,'native_admission':False,'files_sha256':{p.name:digest(p.read_bytes()) for p in OUT.iterdir() if p.suffix=='.json'}} + write(OUT/'receipt.json',(json.dumps(receipt,indent=2)+'\n').encode(),0o644);print(json.dumps(receipt,indent=2)) + finally: + child.terminate();child.wait(timeout=5);source.shutdown();review.shutdown();engine.close() diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/original-approval-claim.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/original-approval-claim.json new file mode 100644 index 00000000..b6c46055 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/original-approval-claim.json @@ -0,0 +1,39 @@ +{ + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T14:39:10+00:00", + "observed_at": "2026-10-16T14:37:10+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T14:40:10.168539+00:00", + "not_before": "2026-10-16T14:36:10.168539+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/receipt.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/receipt.json new file mode 100644 index 00000000..1b973ca3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/receipt.json @@ -0,0 +1,22 @@ +{ + "scope": "disposable synthetic only", + "actual_source": "Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine", + "actual_access": "production serve binary from B2/B3 R2 temporal successor", + "allow": true, + "backend_ttl_seconds": 30, + "operation_authority_seconds": 60, + "explicit_approval_freshness_seconds": 120, + "decision_expiry_equals_exact_operation_deadline": true, + "original_context_unchanged": true, + "published_digest_match": true, + "returned_source_record_match": true, + "actual_source_revoke_refused": true, + "no_record_after_refusal": true, + "native_admission": false, + "files_sha256": { + "source-record.json": "sha256:5fab76b026ac330233517b4de85cd9aeda096de6a2b118300cffd177652ad38e", + "response.json": "sha256:5fab76b026ac330233517b4de85cd9aeda096de6a2b118300cffd177652ad38e", + "original-approval-claim.json": "sha256:d03abc6f2278cb87e0c7f36a4e5342902bd3f17c6c4e19862c2e7784564bea14", + "request.json": "sha256:fc04f36cf1e0ef03995336665c411de62044194e73ee953ceebc7f615cc1de5d" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/request.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/request.json new file mode 100644 index 00000000..67fa7bac --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/request.json @@ -0,0 +1,142 @@ +{ + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod" + }, + "tenant": "tenant:platform" + }, + "context": { + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:38:10+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "human_control": true, + "service_actor_binding": { + "version": "proposed-service-task-caller/v1", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform" + }, + "purpose": "actual Python App source-only synthetic proof", + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T14:39:10+00:00", + "observed_at": "2026-10-16T14:37:10+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T14:40:10.168539+00:00", + "not_before": "2026-10-16T14:36:10.168539+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/response.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/response.json new file mode 100644 index 00000000..78129a6a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/response.json @@ -0,0 +1,229 @@ +{ + "id": "decision:345172e88988026a", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T14:39:10+00:00", + "observed_at": "2026-10-16T14:37:10+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T14:40:10.168539+00:00", + "not_before": "2026-10-16T14:36:10.168539+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:38:10+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:5de47c87ba31a96c51cffd75e3a1236a830745c9bf2087d5a44b640b40c11540", + "approval_binding_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "submitted_request_digest": "sha256:5de47c87ba31a96c51cffd75e3a1236a830745c9bf2087d5a44b640b40c11540" + }, + "lifetime": { + "kind": "ttl", + "ttl": "59.254796424s", + "not_before": "2026-10-16T14:37:10.745203576Z", + "expires_at": "2026-10-16T14:38:10Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": false + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "input_claim_digests": { + "context": "sha256:1000c59c0b5765bc2de159a42c0cfa861ce840547e87edb5cadd8de20f177a31" + }, + "decision_time": "2026-10-16T14:37:10Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:b646af61ef4d13a89c82a267d93b38eab6bcfc3ba0314550577dd883bdd221a2", + "claim_digest": "sha256:dfeec32f189bc8ed5500bc1f7a53758183cd889ee9472dc8ee5d506dd5c67393", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T14:37:10Z", + "not_after": "2026-10-16T14:39:10Z", + "validated_at": "2026-10-16T14:37:10.733320979Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T14:39:10Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "XP7oAYq5QDFThW8NkQunirFEcCmOwqkxeJiaQWbJGdi--x2XEVOtJH_mZmAQGeTXmcZGWS6WNtNVltgD2NxVDA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/signature-verification.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/signature-verification.json new file mode 100644 index 00000000..056d6192 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/signature-verification.json @@ -0,0 +1 @@ +{"existing_producer_signature_verified":true,"independent_synthetic_public_fixture":true,"returned_record_canonical_bytes_match":true,"new_signature_implementation":false} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/source-record.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/source-record.json new file mode 100644 index 00000000..78129a6a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/source-record.json @@ -0,0 +1,229 @@ +{ + "id": "decision:345172e88988026a", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T14:39:10+00:00", + "observed_at": "2026-10-16T14:37:10+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T14:40:10.168539+00:00", + "not_before": "2026-10-16T14:36:10.168539+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:38:10+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:5de47c87ba31a96c51cffd75e3a1236a830745c9bf2087d5a44b640b40c11540", + "approval_binding_digest": "sha256:1314ecedf5f8607a41ac4b5f951a4f47460ff4a6fecff70b057698688a5e71e4", + "submitted_request_digest": "sha256:5de47c87ba31a96c51cffd75e3a1236a830745c9bf2087d5a44b640b40c11540" + }, + "lifetime": { + "kind": "ttl", + "ttl": "59.254796424s", + "not_before": "2026-10-16T14:37:10.745203576Z", + "expires_at": "2026-10-16T14:38:10Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": false + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "input_claim_digests": { + "context": "sha256:1000c59c0b5765bc2de159a42c0cfa861ce840547e87edb5cadd8de20f177a31" + }, + "decision_time": "2026-10-16T14:37:10Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:b646af61ef4d13a89c82a267d93b38eab6bcfc3ba0314550577dd883bdd221a2", + "claim_digest": "sha256:dfeec32f189bc8ed5500bc1f7a53758183cd889ee9472dc8ee5d506dd5c67393", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T14:37:10Z", + "not_after": "2026-10-16T14:39:10Z", + "validated_at": "2026-10-16T14:37:10.733320979Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T14:39:10Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "XP7oAYq5QDFThW8NkQunirFEcCmOwqkxeJiaQWbJGdi--x2XEVOtJH_mZmAQGeTXmcZGWS6WNtNVltgD2NxVDA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/synthetic-public-trust.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/synthetic-public-trust.json new file mode 100644 index 00000000..234e3d7f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/synthetic-public-trust.json @@ -0,0 +1,5 @@ +{ + "scope": "synthetic disposable fixture only; no native key admission", + "kid": "synthetic-v2", + "public_key": "Zs1gi5KLiOUODv6qM_rxxDzv4HKUsLh-n-CrpqPPdjM" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/owner-temporal-review.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/owner-temporal-review.json new file mode 100644 index 00000000..d47884c1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/owner-temporal-review.json @@ -0,0 +1,39 @@ +{ + "owner": "Access Engine", + "status": "source proposal for independent coordinator review", + "supersedes": "R1 temporal behavior only; all R1 artifacts preserved", + "contradiction": "15s decision expiry and default Approval freshness30 cannot bound backendmin30+cleanupreserve5 under whole-operation authority", + "authoritative_facts": { + "Secrets task-scoped-openbao-execution-contract.md": "task TTL bounded by decision/approval/operation; min30; reserve and max120", + "Secrets execution_contract.require_execution_deadline": "operationdeadline<=served decision/approval bound", + "Platform task_execution_native_fixture.materialize_grant": "remaining>=declaredTTL+5 before effects", + "Platform owner recommendation": "conservative source maximum120, backend30/deadline60, explicit Approval freshness120" + }, + "source_correction": [ + "source policy120 is hard ceiling, not blanket ALLOW expiry", + "exact immutable operationdeadline is actual signed expiry, retaining fractional timestamp", + "entire operation fits original/fresh freshness+validity and caller expiry", + "remaining>=declaredbackendTTL+5 after I/O and final evaluation", + "max120 and structurallyinteger30..120 enforced; structurallyallowed120 refuses ifcannotfitreserve", + "strict consumer/native checks unchanged" + ], + "actual_proof": "actual Approval App TLS -> production Access serve ALLOW with backend30/deadline60/explicitfresh120, unchanged context/digests/record; revoke refuses", + "negative_tests": [ + "old15s conflict", + "below35s minimum by1ns", + "elapsed setup consumes lifetime", + "expired operation", + "above120hardcap by1ns", + "attemptedpolicycap121", + "shorter original/fresh authority", + "backend120 cannotfit", + "noninteger structural TTL", + "HTTPsource live but shorterfreshness thanoperation" + ], + "remaining": [ + "independent source and cross-owner successor integration review", + "Platform absolute per-call elapsed deadline/read/close and cleanup reserve proof; inactivity timeout alone is insufficient", + "separately owner-admitted runtime freshness/trust/custody/image rollout" + ], + "native_admission": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/r1-to-r2-temporal.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/r1-to-r2-temporal.patch new file mode 100644 index 00000000..ebbd6b2a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/r1-to-r2-temporal.patch @@ -0,0 +1,283 @@ +diff --git a/cmd/access-engine/trusted_service_http_test.go b/cmd/access-engine/trusted_service_http_test.go +--- a/cmd/access-engine/trusted_service_http_test.go ++++ b/cmd/access-engine/trusted_service_http_test.go +@@ -33,6 +33,7 @@ + func trustedRequest(t *testing.T, now time.Time) api.CheckRequest { + t.Helper() + r := pilotRequest(now) ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(60 * time.Second).UTC().Truncate(time.Second).Format(time.RFC3339) + r.Subject.Tenant = r.Tenant + r.Resource.Tenant = r.Tenant + r.Resource.Attributes["numeric_fixture"] = json.Number("1.0") +@@ -44,7 +45,7 @@ + for _, key := range []string{"ccr_immutable_sha256", "grant_immutable_sha256", "catalog_sha256", "expected_before", "expected_after"} { + c[key] = hash + } +- c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("60"), "uses": json.Number("12")} ++ c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("30"), "uses": json.Number("12")} + var ops []any + for i, method := range []string{"GET", "PUT", "GET", "GET", "PUT", "GET"} { + path := "sys/policies/acl/synthetic" +@@ -74,7 +75,7 @@ + binding["pdp_digest"] = api.ApprovalBindingDigest(legacy) + binding["pdp_path"] = true + binding["human_control"] = true +- r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("60"), "not_after": now.Add(59 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} ++ r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("120"), "not_after": now.Add(119 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} + return r + } + func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { +@@ -114,6 +115,12 @@ + v, _ := approvalfact.DecodeJSON(claimRaw) + v.(map[string]any)["consumed"] = true + json.NewEncoder(w).Encode(v) ++ case "short-fresh": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ fresh := v.(map[string]any)["freshness"].(map[string]any) ++ fresh["ttl_seconds"] = json.Number("20") ++ fresh["not_after"] = now.Add(19 * time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) + case "expired": + v, _ := approvalfact.DecodeJSON(claimRaw) + v.(map[string]any)["freshness"].(map[string]any)["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) +@@ -186,6 +193,9 @@ + if !reflect.DeepEqual(envelope.Binding.Context, original.Context) { + t.Fatal("original context changed") + } ++ if envelope.Lifetime.ExpiresAt != original.Context["execution_contract"].(map[string]any)["operation_deadline"] { ++ t.Fatal("decision does not bound entire exact operation") ++ } + if envelope.Binding.Resource.Attributes["numeric_fixture"] != json.Number("1.0") { + t.Fatal("native numeric kind lost") + } +@@ -200,7 +210,7 @@ + if !reflect.DeepEqual(recordEnvelope, envelope) { + t.Fatal("returned/recorded mismatch") + } +- for _, failure := range []string{"redirect", "duplicate", "changed", "expired"} { ++ for _, failure := range []string{"redirect", "duplicate", "changed", "expired", "short-fresh"} { + t.Run(failure, func(t *testing.T) { + mode = failure + status, _ := post(original) +@@ -217,7 +227,7 @@ + t.Fatal(err) + } + fresh := r.Context["approval"].(map[string]any)["freshness"].(map[string]any) +- fresh["observed_at"] = now.Add(-61 * time.Second).Format(time.RFC3339Nano) ++ fresh["observed_at"] = now.Add(-121 * time.Second).Format(time.RFC3339Nano) + fresh["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) + status, _ := post(r) + if status == 200 { +diff --git a/docs/trusted-service-task-source.md b/docs/trusted-service-task-source.md +--- a/docs/trusted-service-task-source.md ++++ b/docs/trusted-service-task-source.md +@@ -44,9 +44,21 @@ + batch and list entry points refuse task execution across all policy names. No + request-selected verified flag, provider origin or policy enable switch bypasses + the typed path. Missing approval can receive signed DENY for preapproval binding; +-ALLOW requires authenticated matching original/fresh claims. Its expiry is capped +-by policy TTL, caller expiry, operation deadline and both claims' freshness and +-validity, rechecked after I/O/evaluation. Existing source-owned signing covers the ++ALLOW requires authenticated matching original/fresh claims. Its expiry is exactly the immutable operation deadline, after validating that ++it fits the source-selected 120s hard maximum and every caller/original/fresh ++freshness/approval bound. At least the declared backend TTL plus the existing ++5s cleanup reserve must still fit after source I/O and final evaluation. The ++source maximum conservatively matches the existing 120s task-token cap; the ++structural backend budget maximum is not a promise every budget can fit. In ++particular backend TTL120 cannot fit this authority ceiling plus reserve. A ++fractional operation deadline is retained exactly rather than rounded earlier. ++Approval's default 30s observation freshness cannot cover the minimum30+5 ++operation budget. A separate owner-admitted source configuration must explicitly ++supply adequate original/fresh observation lifetime; fetching anew cannot renew ++an original claim. The disposable reviewed proof uses backendTTL30, immutable ++operationdeadline+60s and explicitly configured Approval freshness120. This is ++an entire-operation authority interval, not a decision-to-start window. Platform ++must retain native-entry and per-call deadline/cleanup gates; delays simply refuse. Existing source-owned signing covers the + original binding and new `provenance.approval_observation` receipt. The receipt + contains source/configuration identity, exact fetched-byte digest, native digest + and observation/validation/deadline timestamps. JSONL readback and outbox replay +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_package.md b/examples/secrets-engine-service-task-execution-v2/policy_package.md +--- a/examples/secrets-engine-service-task-execution-v2/policy_package.md ++++ b/examples/secrets-engine-service-task-execution-v2/policy_package.md +@@ -5,7 +5,7 @@ + version: v2 + status: proposed + package: flexauth.secrets_engine.service_task_execution +-allow_ttl: 15s ++allow_ttl: 120s + actions: [execute-ccr] + owner: team:platform-security + fixtures: [policy_fixtures.yaml] +@@ -22,6 +22,11 @@ + path requires exact TokenReview caller mapping, immutable numeric source + representations, a fixed authenticated Approval reader, separate original and + fresh claim validation and native/PDP correspondence before policy evaluation. ++Its 120s declared TTL is a hard source ceiling, not a blanket grant. The typed ++path derives actual expiry exactly from the immutable operation deadline and ++requires the entire declared backend TTL plus the existing 5s cleanup reserve ++within original/fresh claim, approval and caller bounds. Budgets that cannot fit ++refuse; the 120s structural backend maximum is not guaranteed admissible. + Generic check, batch and list routes refuse task execution. Selecting this source + package alone cannot bypass those joins. No deployment or trust admission is + established by this package. The v1 package remains unchanged and DENY-only. +@@ -84,6 +89,8 @@ + not input.context.delegation + input.resource.attributes.stage == "prod" + time.parse_rfc3339_ns(c.operation_deadline) > time.now_ns() ++ time.parse_rfc3339_ns(c.operation_deadline) <= time.now_ns() + 120000000000 ++ time.parse_rfc3339_ns(c.operation_deadline) >= time.now_ns() + (c.backend_budget.ttl_seconds + 5) * 1000000000 + input.context.approval.schema_version == "0.1" + input.context.approval.kind == "approval-claim" + input.context.approval.issuer == "approval-engine" +@@ -94,6 +101,7 @@ + hash_ok(input.context.approval.binding.pdp_digest) + time.parse_rfc3339_ns(input.context.approval.freshness.not_after) > time.now_ns() + time.parse_rfc3339_ns(input.context.approval.validity.expires_at) >= time.parse_rfc3339_ns(c.operation_deadline) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) >= time.parse_rfc3339_ns(c.operation_deadline) + input.context.approval.binding.human_control == true + } + +diff --git a/internal/decision/trusted_service.go b/internal/decision/trusted_service.go +--- a/internal/decision/trusted_service.go ++++ b/internal/decision/trusted_service.go +@@ -143,22 +143,14 @@ + } + envelope.Provenance.ApprovalObservation = approvalEvidence + if envelope.Effect == api.DecisionEffectAllow { +- if approvalEvidence == nil || envelope.Lifetime == nil || !deadline.After(now) { ++ if approvalEvidence == nil || envelope.Lifetime == nil { + return api.DecisionEnvelope{}, ErrServicePilotUnavailable + } +- end, err := time.Parse(time.RFC3339, envelope.Lifetime.ExpiresAt) ++ lifetime, err := trustedTaskLifetime(contract, operationEnd, deadline, now, e.policy.Metadata.AllowTTL) + if err != nil { + return api.DecisionEnvelope{}, err + } +- if deadline.Before(end) { +- end = deadline +- } +- // Floor expiry to whole seconds, never extend an authoritative deadline. +- end = end.Truncate(time.Second) +- if !end.After(now) { +- return api.DecisionEnvelope{}, ErrServicePilotUnavailable +- } +- envelope.Lifetime.ExpiresAt = end.UTC().Format(time.RFC3339) ++ envelope.Lifetime = lifetime + } + if err := e.signTaskEnvelope(&envelope); err != nil { + return api.DecisionEnvelope{}, err +@@ -168,3 +160,34 @@ + } + return envelope, nil + } ++ ++// The source ceiling is deliberately no larger than the existing task-token ++// ceiling. Structural backend budgets do not guarantee an executable window. ++const trustedTaskMaxAuthority = 120 * time.Second ++const trustedTaskCleanupReserve = 5 * time.Second ++ ++// trustedTaskLifetime authorizes the entire exact operation, not merely its ++// start. It cannot extend any caller, original/fresh fact, or approval bound. ++func trustedTaskLifetime(contract map[string]any, operationEnd, authorityEnd, now time.Time, declared string) (*api.DecisionLifetime, error) { ++ maximum, err := api.ParseAllowTTL(declared) ++ if err != nil || maximum <= 0 || maximum > trustedTaskMaxAuthority || now.IsZero() || operationEnd.After(authorityEnd) || !operationEnd.After(now) || operationEnd.Sub(now) > maximum { ++ return nil, ErrServicePilotUnavailable ++ } ++ budget, ok := contract["backend_budget"].(map[string]any) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ number, ok := budget["ttl_seconds"].(json.Number) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ seconds, err := number.Int64() ++ if err != nil || seconds < 30 || seconds > 120 { ++ return nil, ErrServicePilotUnavailable ++ } ++ remaining := operationEnd.Sub(now) ++ if remaining < time.Duration(seconds)*time.Second+trustedTaskCleanupReserve { ++ return nil, ErrServicePilotUnavailable ++ } ++ return &api.DecisionLifetime{Kind: api.DecisionLifetimeTTL, TTL: remaining.String(), NotBefore: now.UTC().Format(time.RFC3339Nano), ExpiresAt: operationEnd.UTC().Format(time.RFC3339Nano)}, nil ++} +diff --git a/internal/decision/trusted_service_time_test.go b/internal/decision/trusted_service_time_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service_time_test.go +@@ -0,0 +1,56 @@ ++package decision ++ ++import ( ++ "encoding/json" ++ "testing" ++ "time" ++) ++ ++func TestTrustedTaskWholeOperationLifetime(t *testing.T) { ++ now := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) ++ contract := func(ttl string) map[string]any { ++ return map[string]any{"backend_budget": map[string]any{"ttl_seconds": json.Number(ttl), "uses": json.Number("12")}} ++ } ++ end := now.Add(60 * time.Second) ++ lifetime, err := trustedTaskLifetime(contract("30"), end, now.Add(120*time.Second), now, "120s") ++ if err != nil { ++ t.Fatal(err) ++ } ++ if lifetime.ExpiresAt != end.Format(time.RFC3339) || lifetime.TTL != "1m0s" || lifetime.NotBefore != now.Format(time.RFC3339) { ++ t.Fatal("lifetime not derived from exact operation") ++ } ++ fractional := end.Add(123456789 * time.Nanosecond) ++ lifetime, err = trustedTaskLifetime(contract("30"), fractional, fractional, now, "120s") ++ if err != nil || lifetime.ExpiresAt != fractional.Format(time.RFC3339Nano) { ++ t.Fatal("exact fractional deadline shortened or extended") ++ } ++ cases := []struct { ++ name string ++ ttl string ++ end, authority, time time.Time ++ maximum string ++ }{ ++ {"old-15s-conflict", "30", end, end, now, "15s"}, ++ {"insufficient-cleanup-reserve", "30", now.Add(35*time.Second - time.Nanosecond), end, now, "120s"}, ++ {"elapsed-consume-setup", "30", end, end, now.Add(26 * time.Second), "120s"}, ++ {"expired-operation", "30", now, now, now, "120s"}, ++ {"source-hard-maximum", "30", now.Add(120*time.Second + time.Nanosecond), now.Add(130 * time.Second), now, "120s"}, ++ {"attempted-policy-cap-bypass", "30", end, end, now, "121s"}, ++ {"shorter-original-or-fresh-fact", "30", end, end.Add(-time.Nanosecond), now, "120s"}, ++ {"structural-120-cannot-fit", "120", now.Add(120 * time.Second), now.Add(120 * time.Second), now, "120s"}, ++ {"noninteger-structural-ttl", "30.0", end, end, now, "120s"}, ++ } ++ for _, c := range cases { ++ t.Run(c.name, func(t *testing.T) { ++ if _, err := trustedTaskLifetime(contract(c.ttl), c.end, c.authority, c.time, c.maximum); err == nil { ++ t.Fatal("invalid temporal authority accepted") ++ } ++ }) ++ } ++ if _, err := trustedTaskLifetime(contract("30"), now.Add(35*time.Second), end, now, "120s"); err != nil { ++ t.Fatal("exact required lifetime refused") ++ } ++ if _, err := trustedTaskLifetime(contract("115"), now.Add(120*time.Second), now.Add(120*time.Second), now, "120s"); err != nil { ++ t.Fatal("max fitting declared budget refused") ++ } ++} +diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -248,3 +248,15 @@ + source proposal requires independent cross-owner review and full consumer/Platform + successor integration, then separately admitted runtime custody, source trust, + image provenance and rollout. No source-only proof establishes those runtime gates. ++ ++ ++2026-10-16 B2/B3 R2 temporal correction (R1 retained): whole-stack review exposed ++that R1's 15s ALLOW and default Approval freshness30 cannot fit the declared ++minimum backendTTL30 plus existing cleanupreserve5. The v2 successor now derives ++exact ALLOW expiry from the immutable operationdeadline, requiring all original/ ++fresh approval/freshness/caller bounds, a conservative source maximum120 and ++remaining declaredTTL+5 after source I/O/evaluation. No consumer/native deadline ++or cleanup guard is relaxed. Max structural budgets that cannot fit refuse. ++The fixture declares backendTTL30/deadline60 and explicit source freshness120; ++owner runtime source freshness configuration and full native integration remain ++separate reviewed gates. See docs/trusted-service-task-source.md. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/trusted-source-r2-over-b1-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/trusted-source-r2-over-b1-r2.patch new file mode 100644 index 00000000..bd8f112c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/trusted-source-r2-over-b1-r2.patch @@ -0,0 +1,2458 @@ +diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml +--- a/.forgejo/workflows/image.yaml ++++ b/.forgejo/workflows/image.yaml +@@ -19,6 +19,7 @@ + - "cmd/**" + - "internal/**" + - "pkg/**" ++ - "third_party/**" + - "examples/**" + - "go.mod" + - "go.sum" +diff --git a/Containerfile b/Containerfile +--- a/Containerfile ++++ b/Containerfile +@@ -1,6 +1,8 @@ + FROM golang:1.26.7-alpine@sha256:28d89ee9cc0ff9fec75c82ca201e6bf7fdf9a679d4b7b24dfa04f2bb766bb468 AS build + WORKDIR /src + COPY go.mod go.sum ./ ++# Portable, unchanged Approval-owner pure data helper; available before download. ++COPY third_party/approvalfact ./third_party/approvalfact + RUN go mod download + COPY . . + RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/access-engine ./cmd/access-engine && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/flex-auth ./cmd/flex-auth +diff --git a/cmd/access-engine/main.go b/cmd/access-engine/main.go +--- a/cmd/access-engine/main.go ++++ b/cmd/access-engine/main.go +@@ -333,6 +333,7 @@ + logPath := fs.String("log", "", "optional JSONL decision log path") + signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file") + signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id") ++ approvalSourceFile := fs.String("approval-source-file", "", "owner fixed Approval HTTPS reader configuration file") + outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log") + emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine") + auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats") +@@ -402,7 +403,19 @@ + if err != nil { + return fail(stderr, err) + } +- mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) ++ var approvalReader *taskexecution.ApprovalReader ++ if engine.IsTrustedServicePilot() { ++ if *signingKeyFile == "" || *approvalSourceFile == "" { ++ return fail(stderr, fmt.Errorf("trusted task source requires signing and fixed Approval reader configuration")) ++ } ++ approvalReader, err = taskexecution.LoadApprovalReader(*approvalSourceFile) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ } else if *approvalSourceFile != "" { ++ return fail(stderr, fmt.Errorf("Approval reader requires exact trusted task source package")) ++ } ++ mux := newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, approvalReader) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -457,6 +470,9 @@ + } + + func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { ++ return newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, nil) ++} ++func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs, approvalReader *taskexecution.ApprovalReader) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -467,12 +483,16 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + if serviceInputs == nil { + http.Error(w, "service input unavailable", http.StatusServiceUnavailable) + return + } +- request, err := taskexecution.DecodeServiceRequest(r.Body) ++ decode := taskexecution.DecodeServiceRequest ++ if engine.IsTrustedServicePilot() { ++ decode = taskexecution.DecodeTrustedServiceRequest ++ } ++ request, err := decode(r.Body) + if err != nil || request.Purpose != "" { + http.Error(w, "invalid service task request", http.StatusBadRequest) + return +@@ -482,7 +502,12 @@ + http.Error(w, "service task input refused", http.StatusForbidden) + return + } +- decision, err := engine.CheckServicePilot(r.Context(), input) ++ var decision api.DecisionEnvelope ++ if engine.IsTrustedServicePilot() { ++ decision, err = engine.CheckTrustedService(r.Context(), input, approvalReader) ++ } else { ++ decision, err = engine.CheckServicePilot(r.Context(), input) ++ } + writeHTTP(w, decision, err) + return + } +@@ -506,7 +531,7 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + http.Error(w, "service task batch unavailable", http.StatusForbidden) + return + } +@@ -537,7 +562,7 @@ + // Only the existing exact service pilot package selects this source path. + // This is not an eligibility toggle; successful preparation still only DENYs. + func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) { +- if !engine.IsServicePilot() { ++ if !engine.IsTaskPin() { + return nil, nil + } + if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { +@@ -546,6 +571,9 @@ + reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) + if err != nil { + return nil, err ++ } ++ if engine.IsTrustedServicePilot() { ++ return taskexecution.NewTrustedServiceInputs(reviewer) + } + return taskexecution.NewServicePilotInputs(reviewer) + } +@@ -575,11 +603,15 @@ + } + + func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) { +- store, err := registry.LoadFile(registryPath) ++ pkg, err := policy.LoadAndValidateFile(ctx, policyPath) + if err != nil { + return nil, err + } +- pkg, err := policy.LoadAndValidateFile(ctx, policyPath) ++ load := registry.LoadFile ++ if pkg.Metadata.ID == taskexecution.ServicePilotPackageID && pkg.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion { ++ load = registry.LoadFilePreservingNumbers ++ } ++ store, err := load(registryPath) + if err != nil { + return nil, err + } +diff --git a/cmd/access-engine/trusted_service_http_test.go b/cmd/access-engine/trusted_service_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/trusted_service_http_test.go +@@ -0,0 +1,340 @@ ++package main ++ ++import ( ++ "bytes" ++ "context" ++ "crypto/ed25519" ++ "encoding/base64" ++ "encoding/json" ++ "encoding/pem" ++ "io" ++ "net" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "os/exec" ++ "path/filepath" ++ "reflect" ++ "strings" ++ "testing" ++ "time" ++ ++ "github.com/netkingdom/access-engine/internal/audit" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/decision" ++ "github.com/netkingdom/access-engine/internal/policy" ++ "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++func trustedRequest(t *testing.T, now time.Time) api.CheckRequest { ++ t.Helper() ++ r := pilotRequest(now) ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(60 * time.Second).UTC().Truncate(time.Second).Format(time.RFC3339) ++ r.Subject.Tenant = r.Tenant ++ r.Resource.Tenant = r.Tenant ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1.0") ++ c := r.Context["execution_contract"].(map[string]any) ++ c["operation_id"] = "11111111-2222-4333-8444-555555555555" ++ c["ccr_id"] = "synthetic-change" ++ c["execution_grant_ccr_id"] = "synthetic-grant" ++ hash := "sha256:" + strings.Repeat("1", 64) ++ for _, key := range []string{"ccr_immutable_sha256", "grant_immutable_sha256", "catalog_sha256", "expected_before", "expected_after"} { ++ c[key] = hash ++ } ++ c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("30"), "uses": json.Number("12")} ++ var ops []any ++ for i, method := range []string{"GET", "PUT", "GET", "GET", "PUT", "GET"} { ++ path := "sys/policies/acl/synthetic" ++ if i >= 3 { ++ path = "auth/synthetic/role/synthetic" ++ } ++ ops = append(ops, map[string]any{"method": method, "path": path, "payload_sha256": hash}) ++ } ++ c["operations"] = ops ++ delete(r.Context, "approval") ++ raw, _ := json.Marshal(map[string]any{"action": r.Action, "actor": r.Subject.ID, "principal": "secrets-engine", "purpose": "synthetic source-only proof", "target": r.Resource}) ++ r.Context["purpose"] = "synthetic source-only proof" ++ _, digest, err := approvalfact.CanonicalBinding(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ bindingValue, err := approvalfact.DecodeJSON(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ binding := bindingValue.(map[string]any) ++ // PDP profile intentionally retains the published legacy numeric projection. ++ encoded, _ := json.Marshal(r) ++ var legacy api.CheckRequest ++ json.Unmarshal(encoded, &legacy) ++ binding["digest"] = digest ++ binding["pdp_digest"] = api.ApprovalBindingDigest(legacy) ++ binding["pdp_path"] = true ++ binding["human_control"] = true ++ r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("120"), "not_after": now.Add(119 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} ++ return r ++} ++func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { ++ now := time.Now().UTC() ++ folder := t.TempDir() ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ w.WriteHeader(403) ++ return ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": true, "audiences": []string{"flex-auth"}, "user": map[string]any{"username": taskexecution.ServicePilotPrincipal}}}) ++ })) ++ defer reviewer.Close() ++ ca := filepath.Join(folder, "review-ca") ++ token := filepath.Join(folder, "review-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(token, []byte("synthetic-reviewer"), 0600) ++ original := trustedRequest(t, now) ++ claimRaw, _ := json.Marshal(original.Context["approval"]) ++ mode := "ok" ++ reads := 0 ++ provider := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reads++ ++ if r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Header.Get("Authorization") != "Bearer synthetic-approval-reader" { ++ w.WriteHeader(403) ++ return ++ } ++ if mode == "redirect" { ++ http.Redirect(w, r, reviewer.URL, 302) ++ return ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode { ++ case "duplicate": ++ io.WriteString(w, `{"state":"valid","state":"invalid"}`) ++ case "changed": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["consumed"] = true ++ json.NewEncoder(w).Encode(v) ++ case "short-fresh": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ fresh := v.(map[string]any)["freshness"].(map[string]any) ++ fresh["ttl_seconds"] = json.Number("20") ++ fresh["not_after"] = now.Add(19 * time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) ++ case "expired": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["freshness"].(map[string]any)["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) ++ default: ++ w.Write(claimRaw) ++ } ++ })) ++ defer provider.Close() ++ pca := filepath.Join(folder, "approval-ca") ++ ptoken := filepath.Join(folder, "approval-token") ++ os.WriteFile(pca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: provider.Certificate().Raw}), 0600) ++ os.WriteFile(ptoken, []byte("synthetic-approval-reader"), 0440) ++ source, err := taskexecution.NewApprovalReader(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{19}, 32)) ++ engine.SetEnvelopeSigningKey(key, "synthetic-v2") ++ record := filepath.Join(folder, "record.jsonl") ++ engine.SetDecisionLog(audit.NewJSONLDecisionLog(record)) ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, token, ca, map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithTaskFacts(engine, callerauth.Disabled(), inputs, source)) ++ defer server.Close() ++ post := func(request api.CheckRequest) (int, []byte) { ++ t.Helper() ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ body, _ := io.ReadAll(response.Body) ++ return response.StatusCode, body ++ } ++ status, body := post(original) ++ if status != 200 { ++ t.Fatalf("positive status %d: %s", status, body) ++ } ++ envelope, err := sign.DecodeEnvelope(body) ++ if err != nil { ++ t.Fatal(err) ++ } ++ t.Logf("actual HTTP source policy pin: %s %s %s", pkg.Metadata.ID, pkg.Metadata.Version, pkg.Digest()) ++ if envelope.Effect != api.DecisionEffectAllow || sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("not signed ALLOW: %s", body) ++ } ++ if envelope.Provenance.ApprovalObservation == nil || envelope.Provenance.ApprovalObservation.SourceID != "synthetic-approval" || reads != 1 { ++ t.Fatal("missing authenticated observation provenance") ++ } ++ legacyRaw, _ := json.Marshal(original) ++ var legacyRequest api.CheckRequest ++ json.Unmarshal(legacyRaw, &legacyRequest) ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) { ++ t.Fatal("published original/full/approval-free digest profile changed") ++ } ++ if !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original context changed") ++ } ++ if envelope.Lifetime.ExpiresAt != original.Context["execution_contract"].(map[string]any)["operation_deadline"] { ++ t.Fatal("decision does not bound entire exact operation") ++ } ++ if envelope.Binding.Resource.Attributes["numeric_fixture"] != json.Number("1.0") { ++ t.Fatal("native numeric kind lost") ++ } ++ recorded, err := os.ReadFile(record) ++ if err != nil { ++ t.Fatal(err) ++ } ++ recordEnvelope, err := sign.DecodeEnvelope(bytes.TrimSpace(recorded)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if !reflect.DeepEqual(recordEnvelope, envelope) { ++ t.Fatal("returned/recorded mismatch") ++ } ++ for _, failure := range []string{"redirect", "duplicate", "changed", "expired", "short-fresh"} { ++ t.Run(failure, func(t *testing.T) { ++ mode = failure ++ status, _ := post(original) ++ if status == 200 { ++ t.Fatal("bad source admitted") ++ } ++ }) ++ } ++ mode = "ok" ++ t.Run("original-stale", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, err := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ fresh := r.Context["approval"].(map[string]any)["freshness"].(map[string]any) ++ fresh["observed_at"] = now.Add(-121 * time.Second).Format(time.RFC3339Nano) ++ fresh["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("fresh provider rescued stale original") ++ } ++ }) ++ t.Run("native-number-kind", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, _ := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1") ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("changed native integer/float authority accepted") ++ } ++ }) ++ after, _ := os.ReadFile(record) ++ if !bytes.Equal(after, recorded) { ++ t.Fatal("refused calls recorded executable envelopes") ++ } ++ // Exercise actual production CLI configuration with no ambient credentials. ++ seedPath := filepath.Join(folder, "cli-seed") ++ os.WriteFile(seedPath, []byte(base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{19}, 32))), 0440) ++ sourcePath := filepath.Join(folder, "approval-source.json") ++ sourceJSON, _ := json.Marshal(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ os.WriteFile(sourcePath, sourceJSON, 0600) ++ registryPath := filepath.Join(folder, "registry.json") ++ os.WriteFile(registryPath, []byte(`{}`), 0600) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-record.jsonl") ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ args := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", token, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedPath, "--envelope-signing-kid", "synthetic-v2", "--approval-source-file", sourcePath} ++ argsJSON, _ := json.Marshal(args) ++ child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$") ++ child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)} ++ child.Stdout = io.Discard ++ child.Stderr = io.Discard ++ if err := child.Start(); err != nil { ++ t.Fatal(err) ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ response, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ response.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("trusted CLI not ready") ++ } ++ raw, _ := json.Marshal(original) ++ req, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ wire, _ := io.ReadAll(response.Body) ++ cliEnvelope, err := sign.DecodeEnvelope(wire) ++ if response.StatusCode != 200 || err != nil || cliEnvelope.Effect != api.DecisionEffectAllow || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("CLI did not return signed trusted ALLOW: %s", wire) ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("CLI response/readback record mismatch") ++ } ++ if !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("CLI changed original numeric context") ++ } ++ ++} ++ ++func TestTrustedServiceMissingAndGenericAdmissionRefuse(t *testing.T) { ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ request := trustedRequest(t, time.Now().UTC()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic list bypass") ++ } ++ if _, err := engine.CheckTrustedService(context.Background(), taskexecution.ServiceInput{}, nil); err == nil { ++ t.Fatal("missing typed input and fixed source accepted") ++ } ++ recorder := httptest.NewRecorder() ++ raw, _ := json.Marshal(request) ++ newServeMux(engine).ServeHTTP(recorder, httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw))) ++ if recorder.Code != 503 { ++ t.Fatal("unwired typed path accepted") ++ } ++} +diff --git a/docs/trusted-service-task-source.md b/docs/trusted-service-task-source.md +new file mode 100644 +--- /dev/null ++++ b/docs/trusted-service-task-source.md +@@ -0,0 +1,74 @@ ++# Trusted service task source proposal (v2) ++ ++The separate `secrets-engine.task-execution.service-pilot` v2 source package can ++return ALLOW only through the existing `/v1/check` typed task path. The original ++v1 package remains unchanged and DENY-only. No chart, runtime, source identity, ++credential custody or native execution admission is enabled by these sources. ++ ++The server requires enforced TokenReview, audience `flex-auth`, the exact retained ++Secrets service-account mapping, mounted envelope signing configuration and an ++owner-installed `--approval-source-file`. This public configuration contains ++`source_id`, `source_revision`, fixed HTTPS `origin`, `store_tenant` exactly ++`tenant:platform`, absolute `ca_file` and absolute mounted `token_file`. The ++credential must independently carry existing Approval `approval:read` authority ++for that store. Source labels describe the owner-admitted configuration; they do ++not perform its admission. No key or token value belongs in the configuration. ++ ++The reader sends one authenticated GET to the fixed ++`/v1/approvals/{UUID}/claim` endpoint, accepts only bounded strict JSON, uses only ++the configured CA, refuses redirects and environmental proxies, and has a total ++three-second HTTP timeout. It neither creates nor approves nor consumes. Mounted ++bearer files must be regular, bounded and have no group write or other access; ++0440 is supported. Public configuration/CA files also refuse group/other writes. ++Errors do not include response bodies, credentials or transport diagnostics. ++ ++The unchanged Approval-owner pure data helper is copied with exact source pins ++under `third_party/approvalfact`. The fixed reader establishes provider origin; ++the helper establishes schema and semantic correspondence. Both original and ++fresh claims must independently be currently valid, unconsumed, human-controlled ++PDP-path claims for the exact UUID, native five-field act and Access approval-free ++request digest. Nonfreshness authority must match exactly. The freshly fetched ++claim never replaces the submitted claim/context or renews original freshness. ++ ++The native act derives from the normalized subject/resource, fixed service ++principal, action and nonempty `context.purpose`. The HTTP decoder and task registry ++loader preserve raw JSON numeric representations. Registry facts loaded through ++the old lossy profile cannot enter the trusted path. Only the Approval-owner ++helper canonicalizes native act data. Existing PDP full/submitted/approval-free ++digests retain their published numeric profile through a digest-only legacy ++projection; the signed structured binding retains the original numeric kinds. ++Inputs unsupported by that existing PDP profile refuse; this proposal does not ++weaken any consumer numeric admission or invent numeric restrictions. ++ ++The same existing policy evaluator decides after those joins. Generic check, ++batch and list entry points refuse task execution across all policy names. No ++request-selected verified flag, provider origin or policy enable switch bypasses ++the typed path. Missing approval can receive signed DENY for preapproval binding; ++ALLOW requires authenticated matching original/fresh claims. Its expiry is exactly the immutable operation deadline, after validating that ++it fits the source-selected 120s hard maximum and every caller/original/fresh ++freshness/approval bound. At least the declared backend TTL plus the existing ++5s cleanup reserve must still fit after source I/O and final evaluation. The ++source maximum conservatively matches the existing 120s task-token cap; the ++structural backend budget maximum is not a promise every budget can fit. In ++particular backend TTL120 cannot fit this authority ceiling plus reserve. A ++fractional operation deadline is retained exactly rather than rounded earlier. ++Approval's default 30s observation freshness cannot cover the minimum30+5 ++operation budget. A separate owner-admitted source configuration must explicitly ++supply adequate original/fresh observation lifetime; fetching anew cannot renew ++an original claim. The disposable reviewed proof uses backendTTL30, immutable ++operationdeadline+60s and explicitly configured Approval freshness120. This is ++an entire-operation authority interval, not a decision-to-start window. Platform ++must retain native-entry and per-call deadline/cleanup gates; delays simply refuse. Existing source-owned signing covers the ++original binding and new `provenance.approval_observation` receipt. The receipt ++contains source/configuration identity, exact fetched-byte digest, native digest ++and observation/validation/deadline timestamps. JSONL readback and outbox replay ++preserve producer numeric signatures; no second signing algorithm is introduced. ++ ++Packaging must copy the local owner helper before `go mod download`. Existing ++ordinary image CI triggers include `third_party/**` so a helper update cannot ++silently leave the old image. No build publication is invoked by this preparation. ++Rollback selects the unchanged v1 package or removes the v2 admission; generic ++fallback remains refused. Rotating/withdrawing source or signing trust is a ++separate owner-admitted runtime operation. The earlier Platform positive proof ++remains evidence only at its old ec21518e Access pin; its generic synthetic policy ++is deliberately incompatible with the successor guarded source. +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +@@ -0,0 +1,8 @@ ++- id: unsigned-service-caller-not-authority ++ request: ++ tenant: tenant:platform ++ subject: {id: secrets-engine, type: service} ++ action: execute-ccr ++ resource: {id: catalog:test, type: secret-catalog-lane, system: secrets-engine} ++ context: {human_control: true, runtime_admission: true, verified: true} ++ expect: {effect: deny, reason: service_execution_contract_unmatched} +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_package.md b/examples/secrets-engine-service-task-execution-v2/policy_package.md +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_package.md +@@ -0,0 +1,120 @@ ++--- ++id: secrets-engine.task-execution.service-pilot ++name: Secrets admitted service task execution ++namespace: secrets-engine:secret-catalog-lane ++version: v2 ++status: proposed ++package: flexauth.secrets_engine.service_task_execution ++allow_ttl: 120s ++actions: [execute-ccr] ++owner: team:platform-security ++fixtures: [policy_fixtures.yaml] ++caring: ++ profile: caring-0.4.0-rc2 ++ enforce: false ++activation: ++ mode: local ++--- ++ ++# Trusted service task source successor ++ ++This source package is evaluated exclusively by the typed v2 HTTP path. That ++path requires exact TokenReview caller mapping, immutable numeric source ++representations, a fixed authenticated Approval reader, separate original and ++fresh claim validation and native/PDP correspondence before policy evaluation. ++Its 120s declared TTL is a hard source ceiling, not a blanket grant. The typed ++path derives actual expiry exactly from the immutable operation deadline and ++requires the entire declared backend TTL plus the existing 5s cleanup reserve ++within original/fresh claim, approval and caller bounds. Budgets that cannot fit ++refuse; the 120s structural backend maximum is not guaranteed admissible. ++Generic check, batch and list routes refuse task execution. Selecting this source ++package alone cannot bypass those joins. No deployment or trust admission is ++established by this package. The v1 package remains unchanged and DENY-only. ++ ++```rego ++default decision := {"effect": "deny", "reason": "service_execution_contract_unmatched"} ++ ++decision := {"effect": "allow", "reason": "exact_task_execution_matched"} { ++ candidate_matches ++} ++ ++hash_ok(x) { is_string(x); regex.match("^sha256:[0-9a-f]{64}$", x) } ++ ++candidate_matches { ++ input.action == "execute-ccr" ++ input.tenant == "tenant:platform" ++ input.subject.id == "secrets-engine" ++ input.subject.type == "service" ++ input.resource.system == "secrets-engine" ++ input.resource.type == "secret-catalog-lane" ++ input.context.human_control == true ++ c := input.context.execution_contract ++ c.contract_version == "SECRETS-EXEC-20261011-v1" ++ c.operation == "metadata-apply-ccr" ++ c.execution_actor == "service:secrets-engine" ++ regex.match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", c.operation_id) ++ c.ccr_id != c.execution_grant_ccr_id ++ hash_ok(c.ccr_immutable_sha256) ++ hash_ok(c.grant_immutable_sha256) ++ hash_ok(c.catalog_sha256) ++ hash_ok(c.expected_before) ++ hash_ok(c.expected_after) ++ c.backend_budget.ttl_seconds <= 120 ++ c.backend_budget.ttl_seconds >= 30 ++ c.backend_budget.uses == 12 ++ count(c.operations) == 6 ++ c.operations[0].method == "GET" ++ c.operations[1].method == "PUT" ++ c.operations[2].method == "GET" ++ c.operations[3].method == "GET" ++ c.operations[4].method == "PUT" ++ c.operations[5].method == "GET" ++ regex.match("^sys/policies/acl/[A-Za-z0-9_-]+$", c.operations[0].path) ++ regex.match("^auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+$", c.operations[3].path) ++ c.operations[0].path == c.operations[1].path ++ c.operations[1].path == c.operations[2].path ++ c.operations[3].path == c.operations[4].path ++ c.operations[4].path == c.operations[5].path ++ every_operation_hash_valid ++ b := input.context.service_actor_binding ++ count(b) == 5 ++ b.version == "proposed-service-task-caller/v1" ++ b.transport_principal == "system:serviceaccount:secrets-engine:secrets-engine" ++ b.subject_id == input.subject.id ++ b.subject_type == "service" ++ b.tenant == input.tenant ++ not input.context.task_execution_projection ++ not input.context.initiator ++ not input.context.represented_principal ++ not input.context.delegation ++ input.resource.attributes.stage == "prod" ++ time.parse_rfc3339_ns(c.operation_deadline) > time.now_ns() ++ time.parse_rfc3339_ns(c.operation_deadline) <= time.now_ns() + 120000000000 ++ time.parse_rfc3339_ns(c.operation_deadline) >= time.now_ns() + (c.backend_budget.ttl_seconds + 5) * 1000000000 ++ input.context.approval.schema_version == "0.1" ++ input.context.approval.kind == "approval-claim" ++ input.context.approval.issuer == "approval-engine" ++ input.context.approval.valid_now == true ++ input.context.approval.consumed == false ++ input.context.approval.reason_code == "ok" ++ input.context.approval.binding.pdp_path == true ++ hash_ok(input.context.approval.binding.pdp_digest) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) > time.now_ns() ++ time.parse_rfc3339_ns(input.context.approval.validity.expires_at) >= time.parse_rfc3339_ns(c.operation_deadline) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) >= time.parse_rfc3339_ns(c.operation_deadline) ++ input.context.approval.binding.human_control == true ++} ++ ++every_operation_hash_valid { ++ count([x | x := input.context.execution_contract.operations[_]; hash_ok(x.payload_sha256)]) == 6 ++} ++``` ++ ++```rego test ++import data.flexauth.secrets_engine.service_task_execution ++ ++test_unsigned_flags_cannot_enable { ++ service_task_execution.decision.effect == "deny" ++ with input as {"context": {"runtime_admission": true, "verified": true}} ++} ++``` +diff --git a/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +@@ -0,0 +1,6 @@ ++# Synthetic source-validation fixture only; no estate registry admission. ++resources: ++ - id: catalog:synthetic ++ system: secrets-engine ++ type: secret-catalog-lane ++ attributes: {stage: prod} +diff --git a/go.mod b/go.mod +--- a/go.mod ++++ b/go.mod +@@ -3,6 +3,7 @@ + go 1.26.7 + + require ( ++ github.com/netkingdom/approval-engine/adapters/go/approvalfact v0.0.0 + github.com/open-policy-agent/opa v1.4.0 + gopkg.in/yaml.v3 v3.0.1 + ) +@@ -39,3 +40,5 @@ + google.golang.org/protobuf v1.36.6 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect + ) ++ ++replace github.com/netkingdom/approval-engine/adapters/go/approvalfact => ./third_party/approvalfact +diff --git a/internal/audit/log.go b/internal/audit/log.go +--- a/internal/audit/log.go ++++ b/internal/audit/log.go +@@ -8,6 +8,7 @@ + "path/filepath" + "sync" + ++ "github.com/netkingdom/access-engine/internal/sign" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -67,8 +68,8 @@ + if scanner.Text() == "" { + continue + } +- var decision api.DecisionEnvelope +- if err := json.Unmarshal(scanner.Bytes(), &decision); err != nil { ++ decision, err := sign.DecodeEnvelope(scanner.Bytes()) ++ if err != nil { + return nil, fmt.Errorf("unmarshal decision log line %d: %w", len(decisions)+1, err) + } + decisions = append(decisions, decision) +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -158,6 +158,10 @@ + func (e *Engine) IsServicePilot() bool { + return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion + } ++func (e *Engine) IsTrustedServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion ++} ++func (e *Engine) IsTaskPin() bool { return e.IsServicePilot() || e.IsTrustedServicePilot() } + + // ErrServicePilotUnavailable withholds any result that is not this B1 slice's + // signed DENY. Provider joins and executable eligibility remain unimplemented. +@@ -189,7 +193,7 @@ + // Check evaluates one subject/action/resource request. The decision is + // released only after its required record commits (GH-DEC-2026-024). + func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return api.DecisionEnvelope{}, ErrServicePilotUnavailable + } + decision, err := e.decide(ctx, request) +@@ -205,7 +209,7 @@ + // BatchCheck evaluates one subject/action/context tuple against resources in + // request order, and commits the whole batch in one durable write. + func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) +@@ -232,7 +236,7 @@ + + // ListAllowed evaluates candidate resources and returns only allow decisions. + func (e *Engine) ListAllowed(ctx context.Context, request ListAllowedRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) +diff --git a/internal/decision/trusted_service.go b/internal/decision/trusted_service.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service.go +@@ -0,0 +1,193 @@ ++package decision ++ ++import ( ++ "context" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "time" ++) ++ ++// legacyDigestProjection preserves the published PDP digest profile while the ++// signed structured binding retains original numeric kinds. It is used only ++// for digest material, never to reconstruct an Approval-native act. ++func legacyDigestProjection(request api.CheckRequest) (api.CheckRequest, error) { ++ raw, err := json.Marshal(request) ++ if err != nil { ++ return api.CheckRequest{}, err ++ } ++ var projected api.CheckRequest ++ err = json.Unmarshal(raw, &projected) ++ return projected, err ++} ++func nativeServiceAct(request api.CheckRequest) ([]byte, error) { ++ purpose, ok := request.Context["purpose"].(string) ++ if !ok || purpose == "" || request.Subject.ID != "secrets-engine" || request.Subject.Type != "service" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ if principal, exists := request.Subject.Attributes["principal"]; exists && principal != "secrets-engine" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ // All map numbers originate from the number-preserving request/registry ++ // profile. This raw draft is canonicalized only by the Approval owner helper. ++ return json.Marshal(map[string]any{"action": request.Action, "actor": request.Subject.ID, "principal": "secrets-engine", "purpose": purpose, "target": request.Resource}) ++} ++func (e *Engine) signTaskEnvelope(envelope *api.DecisionEnvelope) error { ++ e.mu.RLock() ++ key, kid := e.signingKey, e.signingKID ++ e.mu.RUnlock() ++ if len(key) == 0 { ++ return ErrServicePilotUnavailable ++ } ++ return sign.Sign(envelope, key, kid) ++} ++ ++// CheckTrustedService is the only v2 task evaluation path. Its source reader ++// and immutable service input are server-created, not HTTP validation flags. ++// It never consumes an approval or causes a protected backend effect. ++func (e *Engine) CheckTrustedService(ctx context.Context, input taskexecution.ServiceInput, source *taskexecution.ApprovalReader) (api.DecisionEnvelope, error) { ++ if !e.IsTrustedServicePilot() || !input.NumberPreserved() || !source.Configured() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now := e.now() ++ submitted, err := input.RequestAt(now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ normalized, facts := e.normalizeRequest(submitted) ++ if (facts.resourceFound || facts.subjectFound) && !e.store.NumbersPreserved() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ original, err := legacyDigestProjection(submitted) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ evaluated, err := legacyDigestProjection(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ ctx = callerauth.WithRecord(ctx, input.Caller()) ++ var approvalEvidence *api.ApprovalObservationProvenance ++ deadline := input.Caller().NotAfter ++ contract := submitted.Context["execution_contract"].(map[string]any) ++ operationEnd, err := time.Parse(time.RFC3339Nano, contract["operation_deadline"].(string)) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrServiceInput ++ } ++ if operationEnd.Before(deadline) { ++ deadline = operationEnd ++ } ++ carried, hasClaim := submitted.Context[api.ApprovalContextKey] ++ if hasClaim { ++ claim, ok := carried.(map[string]any) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ id, ok := claim["approval_id"].(string) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ native, err := nativeServiceAct(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ originalRaw, err := json.Marshal(carried) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ observed, err := source.Observe(ctx, id) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ // One validation instant AFTER network I/O; a fresh read cannot rescue the ++ // original carried claim's freshness, source expiry or operation deadline. ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ verified, err := observed.Validate(originalRaw, approvalfact.Expected{ApprovalID: id, NativeBindingJSON: native, PDPDigest: api.ApprovalBindingDigest(evaluated)}, now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ factEnd, err := verified.Deadline() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if factEnd.Before(deadline) { ++ deadline = factEnd ++ } ++ approvalEvidence, err = verified.Provenance() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ } ++ expectation, err := e.policy.EvaluatePreservingNumbers(ctx, normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if !hasClaim && expectation.Effect == api.DecisionEffectAllow { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope := e.envelope(ctx, normalized, submitted, expectation, facts) ++ envelope.Binding.RequestDigest = api.RequestDigest(evaluated) ++ envelope.Binding.SubmittedRequestDigest = api.RequestDigest(original) ++ if hasClaim { ++ envelope.Binding.ApprovalBindingDigest = api.ApprovalBindingDigest(evaluated) ++ } ++ envelope.Provenance.ApprovalObservation = approvalEvidence ++ if envelope.Effect == api.DecisionEffectAllow { ++ if approvalEvidence == nil || envelope.Lifetime == nil { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ lifetime, err := trustedTaskLifetime(contract, operationEnd, deadline, now, e.policy.Metadata.AllowTTL) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope.Lifetime = lifetime ++ } ++ if err := e.signTaskEnvelope(&envelope); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ ++// The source ceiling is deliberately no larger than the existing task-token ++// ceiling. Structural backend budgets do not guarantee an executable window. ++const trustedTaskMaxAuthority = 120 * time.Second ++const trustedTaskCleanupReserve = 5 * time.Second ++ ++// trustedTaskLifetime authorizes the entire exact operation, not merely its ++// start. It cannot extend any caller, original/fresh fact, or approval bound. ++func trustedTaskLifetime(contract map[string]any, operationEnd, authorityEnd, now time.Time, declared string) (*api.DecisionLifetime, error) { ++ maximum, err := api.ParseAllowTTL(declared) ++ if err != nil || maximum <= 0 || maximum > trustedTaskMaxAuthority || now.IsZero() || operationEnd.After(authorityEnd) || !operationEnd.After(now) || operationEnd.Sub(now) > maximum { ++ return nil, ErrServicePilotUnavailable ++ } ++ budget, ok := contract["backend_budget"].(map[string]any) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ number, ok := budget["ttl_seconds"].(json.Number) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ seconds, err := number.Int64() ++ if err != nil || seconds < 30 || seconds > 120 { ++ return nil, ErrServicePilotUnavailable ++ } ++ remaining := operationEnd.Sub(now) ++ if remaining < time.Duration(seconds)*time.Second+trustedTaskCleanupReserve { ++ return nil, ErrServicePilotUnavailable ++ } ++ return &api.DecisionLifetime{Kind: api.DecisionLifetimeTTL, TTL: remaining.String(), NotBefore: now.UTC().Format(time.RFC3339Nano), ExpiresAt: operationEnd.UTC().Format(time.RFC3339Nano)}, nil ++} +diff --git a/internal/decision/trusted_service_time_test.go b/internal/decision/trusted_service_time_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service_time_test.go +@@ -0,0 +1,56 @@ ++package decision ++ ++import ( ++ "encoding/json" ++ "testing" ++ "time" ++) ++ ++func TestTrustedTaskWholeOperationLifetime(t *testing.T) { ++ now := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) ++ contract := func(ttl string) map[string]any { ++ return map[string]any{"backend_budget": map[string]any{"ttl_seconds": json.Number(ttl), "uses": json.Number("12")}} ++ } ++ end := now.Add(60 * time.Second) ++ lifetime, err := trustedTaskLifetime(contract("30"), end, now.Add(120*time.Second), now, "120s") ++ if err != nil { ++ t.Fatal(err) ++ } ++ if lifetime.ExpiresAt != end.Format(time.RFC3339) || lifetime.TTL != "1m0s" || lifetime.NotBefore != now.Format(time.RFC3339) { ++ t.Fatal("lifetime not derived from exact operation") ++ } ++ fractional := end.Add(123456789 * time.Nanosecond) ++ lifetime, err = trustedTaskLifetime(contract("30"), fractional, fractional, now, "120s") ++ if err != nil || lifetime.ExpiresAt != fractional.Format(time.RFC3339Nano) { ++ t.Fatal("exact fractional deadline shortened or extended") ++ } ++ cases := []struct { ++ name string ++ ttl string ++ end, authority, time time.Time ++ maximum string ++ }{ ++ {"old-15s-conflict", "30", end, end, now, "15s"}, ++ {"insufficient-cleanup-reserve", "30", now.Add(35*time.Second - time.Nanosecond), end, now, "120s"}, ++ {"elapsed-consume-setup", "30", end, end, now.Add(26 * time.Second), "120s"}, ++ {"expired-operation", "30", now, now, now, "120s"}, ++ {"source-hard-maximum", "30", now.Add(120*time.Second + time.Nanosecond), now.Add(130 * time.Second), now, "120s"}, ++ {"attempted-policy-cap-bypass", "30", end, end, now, "121s"}, ++ {"shorter-original-or-fresh-fact", "30", end, end.Add(-time.Nanosecond), now, "120s"}, ++ {"structural-120-cannot-fit", "120", now.Add(120 * time.Second), now.Add(120 * time.Second), now, "120s"}, ++ {"noninteger-structural-ttl", "30.0", end, end, now, "120s"}, ++ } ++ for _, c := range cases { ++ t.Run(c.name, func(t *testing.T) { ++ if _, err := trustedTaskLifetime(contract(c.ttl), c.end, c.authority, c.time, c.maximum); err == nil { ++ t.Fatal("invalid temporal authority accepted") ++ } ++ }) ++ } ++ if _, err := trustedTaskLifetime(contract("30"), now.Add(35*time.Second), end, now, "120s"); err != nil { ++ t.Fatal("exact required lifetime refused") ++ } ++ if _, err := trustedTaskLifetime(contract("115"), now.Add(120*time.Second), now.Add(120*time.Second), now, "120s"); err != nil { ++ t.Fatal("max fitting declared budget refused") ++ } ++} +diff --git a/internal/emission/delivery.go b/internal/emission/delivery.go +--- a/internal/emission/delivery.go ++++ b/internal/emission/delivery.go +@@ -112,7 +112,9 @@ + return entry{}, 0, err + } + var e entry +- if err := json.Unmarshal(line, &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(line)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return entry{}, 0, err + } + if e.Seq != o.cursor.Seq+1 || e.Event.Source != o.source { +diff --git a/internal/emission/numeric_replay_test.go b/internal/emission/numeric_replay_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/emission/numeric_replay_test.go +@@ -0,0 +1,53 @@ ++package emission ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestOutboxReplayPreservesProducerNumericSignature(t *testing.T) { ++ folder := t.TempDir() ++ outbox, err := Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{21}, 32)) ++ envelope := api.DecisionEnvelope{ID: "decision:synthetic-numeric", Effect: api.DecisionEffectDeny, Resource: api.ResourceRef{ID: "synthetic", Type: "tenant", System: "tenant-engine"}} ++ envelope.Binding = &api.DecisionBinding{Context: map[string]any{"float": json.Number("1.0"), "negative_zero": json.Number("-0.0"), "integer": json.Number("9007199254740993"), "exponent": json.Number("1e-07")}} ++ if err := sign.Sign(&envelope, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ if err := outbox.Append(envelope); err != nil { ++ t.Fatal(err) ++ } ++ outbox.Close() ++ outbox, err = Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer outbox.Close() ++ entry, _, err := outbox.nextEntry() ++ if err != nil { ++ t.Fatal(err) ++ } ++ wire, err := json.Marshal(entry.Event.Data) ++ if err != nil { ++ t.Fatal(err) ++ } ++ replayed, err := sign.DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := sign.Verify(replayed, map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)}); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := sign.CanonicalBytes(envelope) ++ after, _ := sign.CanonicalBytes(replayed) ++ if !bytes.Equal(before, after) { ++ t.Fatal("replayed signed bytes changed") ++ } ++} +diff --git a/internal/emission/outbox.go b/internal/emission/outbox.go +--- a/internal/emission/outbox.go ++++ b/internal/emission/outbox.go +@@ -136,7 +136,9 @@ + return os.Truncate(o.path, o.size) + } + var e entry +- if err := json.Unmarshal(data[good:good+end], &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(data[good : good+end])) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return fmt.Errorf("outbox line %d is corrupt: %w", line, err) + } + if e.Seq != o.seq+1 || e.Event.Source != o.source { +diff --git a/internal/policy/package.go b/internal/policy/package.go +--- a/internal/policy/package.go ++++ b/internal/policy/package.go +@@ -1,6 +1,7 @@ + package policy + + import ( ++ "bytes" + "context" + "encoding/json" + "fmt" +@@ -375,7 +376,25 @@ + } + + func (p *Package) evaluateDecision(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, false) ++} ++ ++// EvaluatePreservingNumbers feeds the same evaluator the task profile's exact ++// numeric values. Ordinary evaluation behavior and canonical contracts stay put. ++func (p *Package) EvaluatePreservingNumbers(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, true) ++} ++func (p *Package) evaluateDecisionProfile(ctx context.Context, request api.CheckRequest, preserve bool) (api.DecisionExpectation, error) { + input, err := toRegoInput(request) ++ if preserve { ++ raw, e := json.Marshal(request) ++ if e != nil { ++ return api.DecisionExpectation{}, e ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ err = decoder.Decode(&input) ++ } + if err != nil { + return api.DecisionExpectation{}, err + } +diff --git a/internal/registry/numeric_source_test.go b/internal/registry/numeric_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/registry/numeric_source_test.go +@@ -0,0 +1,36 @@ ++package registry ++ ++import ( ++ "encoding/json" ++ "os" ++ "path/filepath" ++ "testing" ++) ++ ++func TestTaskRegistryNumericSourceAndStrictDecode(t *testing.T) { ++ path := filepath.Join(t.TempDir(), "snapshot.json") ++ raw := []byte(`{"resource_manifests":[{"id":"synthetic","system":"secrets-engine","resources":[{"id":"catalog:synthetic","type":"secret-catalog-lane","attributes":{"float":1.0,"integer":9007199254740993,"negative_zero":-0.0}}]}]}`) ++ os.WriteFile(path, raw, 0600) ++ store, err := LoadFilePreservingNumbers(path) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if !store.NumbersPreserved() { ++ t.Fatal("missing source profile") ++ } ++ resource, ok := store.Resource("secrets-engine", "catalog:synthetic") ++ if !ok { ++ t.Fatal("missing resource") ++ } ++ for k, v := range map[string]string{"float": "1.0", "integer": "9007199254740993", "negative_zero": "-0.0"} { ++ if resource.Attributes[k] != json.Number(v) { ++ t.Fatalf("lost native representation: %s", k) ++ } ++ } ++ for _, raw := range []string{`{} {}`, `{"subjects":[],"subjects":[]}`, `{"unknown":true}`} { ++ os.WriteFile(path, []byte(raw), 0600) ++ if _, err := LoadFilePreservingNumbers(path); err == nil { ++ t.Fatal("invalid authoritative registry accepted") ++ } ++ } ++} +diff --git a/internal/registry/store.go b/internal/registry/store.go +--- a/internal/registry/store.go ++++ b/internal/registry/store.go +@@ -1,6 +1,7 @@ + package registry + + import ( ++ "bytes" + "encoding/json" + "fmt" + "os" +@@ -8,10 +9,12 @@ + "sort" + + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + ) + + // Store is a deterministic local registry for standalone development and tests. + type Store struct { ++ numbersPreserved bool + systems map[string]api.ProtectedSystemManifest + resourceManifests map[string]api.ResourceManifest + resources map[string]api.Resource +@@ -66,6 +69,32 @@ + return store, nil + } + ++// LoadFilePreservingNumbers is the task-source profile: owner registry numeric ++// representations stay intact for native act correspondence. Ordinary loaders ++// and their existing snapshot/digest behavior remain unchanged. ++func LoadFilePreservingNumbers(path string) (*Store, error) { ++ raw, err := os.ReadFile(path) ++ if err != nil { ++ return nil, err ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, err ++ } ++ var snapshot Snapshot ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&snapshot); err != nil { ++ return nil, err ++ } ++ store := NewStore() ++ if err := store.ApplySnapshot(snapshot); err != nil { ++ return nil, err ++ } ++ store.numbersPreserved = true ++ return store, nil ++} ++ + // SaveFile writes the store as deterministic pretty-printed JSON. + func (s *Store) SaveFile(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { +@@ -380,3 +409,6 @@ + } + return out + } ++ ++// NumbersPreserved reports the source profile of file-backed registry facts. ++func (s *Store) NumbersPreserved() bool { return s != nil && s.numbersPreserved } +diff --git a/internal/sign/numeric_decode_test.go b/internal/sign/numeric_decode_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/sign/numeric_decode_test.go +@@ -0,0 +1,54 @@ ++package sign ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestProducerNumericEnvelopeDecodeCompatibility(t *testing.T) { ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{11}, 32)) ++ keys := map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)} ++ for _, raw := range []string{`1`, `1.0`, `-0.0`, `1e-07`, `1e+20`, `9007199254740993`, `1.7976931348623157e308`, `5e-324`, `true`, `"<>&\u2028\u2029 cafĂ©"`, `[1,1.0,-0.0]`, `{"nested":1.0}`} { ++ t.Run(raw, func(t *testing.T) { ++ var value any ++ decoder := json.NewDecoder(bytes.NewBufferString(raw)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&value); err != nil { ++ t.Fatal(err) ++ } ++ original := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"numeric": value}}} ++ if err := Sign(&original, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ wire, _ := json.Marshal(original) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := Verify(decoded, keys); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := CanonicalBytes(original) ++ after, _ := CanonicalBytes(decoded) ++ if !bytes.Equal(before, after) { ++ t.Fatal("producer canonical bytes changed") ++ } ++ }) ++ } ++ // Existing float64 producer profile still verifies through the new decoder. ++ legacy := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"number": float64(1), "exponent": float64(1e-7)}}} ++ Sign(&legacy, key, "synthetic-numeric") ++ wire, _ := json.Marshal(legacy) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil || Verify(decoded, keys) != nil { ++ t.Fatal("legacy producer compatibility failed") ++ } ++ for _, raw := range []string{`{} {}`, `{"new_unknown_authority":true}`} { ++ if _, err := DecodeEnvelope([]byte(raw)); err == nil { ++ t.Fatal("invalid envelope accepted") ++ } ++ } ++} +diff --git a/internal/sign/sign.go b/internal/sign/sign.go +--- a/internal/sign/sign.go ++++ b/internal/sign/sign.go +@@ -4,10 +4,12 @@ + package sign + + import ( ++ "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" ++ "io" + + "github.com/netkingdom/access-engine/pkg/api" + ) +@@ -74,3 +76,20 @@ + } + return nil + } ++ ++// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs. ++// Legacy producer envelopes remain byte-compatible. It does not invent another ++// signature canonicalization: CanonicalBytes still calls typed json.Marshal. ++func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) { ++ var envelope api.DecisionEnvelope ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&envelope); err != nil { ++ return envelope, err ++ } ++ if _, err := decoder.Token(); err != io.EOF { ++ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data") ++ } ++ return envelope, nil ++} +diff --git a/internal/taskexecution/approval_source.go b/internal/taskexecution/approval_source.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source.go +@@ -0,0 +1,215 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "crypto/sha256" ++ "crypto/tls" ++ "crypto/x509" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "mime" ++ "net/http" ++ "net/url" ++ "os" ++ "path/filepath" ++ "regexp" ++ "strings" ++ "time" ++ ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++var ErrApprovalSource = errors.New("trusted Approval source refused") ++var approvalUUID = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`) ++ ++type ApprovalSourceConfig struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ Origin string `json:"origin"` ++ StoreTenant string `json:"store_tenant"` ++ CAFile string `json:"ca_file"` ++ TokenFile string `json:"token_file"` ++} ++ ++// ApprovalReader is installed from owner configuration, never an HTTP request. ++// It reads one fixed PIP endpoint; no policy, retry, consume or effect lives here. ++type ApprovalReader struct { ++ config ApprovalSourceConfig ++ configurationDigest string ++ client *http.Client ++} ++ ++func NewApprovalReader(config ApprovalSourceConfig) (*ApprovalReader, error) { ++ u, err := url.Parse(config.Origin) ++ if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || strings.ContainsAny(config.Origin, "\r\n\t ") || config.SourceID == "" || config.SourceRevision == "" || config.StoreTenant != "tenant:platform" || !filepath.IsAbs(config.CAFile) || !filepath.IsAbs(config.TokenFile) { ++ return nil, ErrApprovalSource ++ } ++ ca, err := readPublicApprovalFile(config.CAFile, 1048576) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ pool := x509.NewCertPool() ++ if !pool.AppendCertsFromPEM(ca) { ++ return nil, ErrApprovalSource ++ } ++ config.Origin = strings.TrimRight(config.Origin, "/") ++ digest := api.CanonicalDigest(struct { ++ Config ApprovalSourceConfig `json:"configuration"` ++ CA string `json:"ca_sha256"` ++ }{config, byteDigest(ca)}) ++ return &ApprovalReader{config: config, configurationDigest: digest, client: &http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{Proxy: nil, DisableCompression: true, TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}}}}, nil ++} ++func readPublicApprovalFile(path string, limit int64) ([]byte, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 { ++ return nil, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, limit+1)) ++ if err != nil || int64(len(raw)) > limit { ++ return nil, ErrApprovalSource ++ } ++ return raw, nil ++} ++func LoadApprovalReader(path string) (*ApprovalReader, error) { ++ if !filepath.IsAbs(path) { ++ return nil, ErrApprovalSource ++ } ++ raw, err := readPublicApprovalFile(path, 65536) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, ErrApprovalSource ++ } ++ var config ApprovalSourceConfig ++ decoder := json.NewDecoder(strings.NewReader(string(raw))) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&config) != nil { ++ return nil, ErrApprovalSource ++ } ++ return NewApprovalReader(config) ++} ++func (r *ApprovalReader) Configured() bool { ++ return r != nil && r.client != nil && r.configurationDigest != "" ++} ++func byteDigest(raw []byte) string { ++ h := sha256.Sum256(raw) ++ return "sha256:" + hex.EncodeToString(h[:]) ++} ++func readApprovalToken(path string) (string, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return "", ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 { ++ return "", ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, 8193)) ++ if err != nil || len(raw) > 8192 { ++ return "", ErrApprovalSource ++ } ++ token := strings.TrimSpace(string(raw)) ++ if token == "" || strings.ContainsAny(token, " \t\r\n") { ++ return "", ErrApprovalSource ++ } ++ return token, nil ++} ++ ++// ApprovalObservation cannot be created by caller JSON or a public verified flag. ++// It is raw fact data plus the source/configuration used by this fixed reader. ++type ApprovalObservation struct { ++ raw []byte ++ sourceID, revision, configurationDigest string ++} ++ ++func (r *ApprovalReader) Observe(ctx context.Context, id string) (ApprovalObservation, error) { ++ if !r.Configured() || !approvalUUID.MatchString(id) { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ token, err := readApprovalToken(r.config.TokenFile) ++ if err != nil { ++ return ApprovalObservation{}, err ++ } ++ req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.config.Origin+"/v1/approvals/"+id+"/claim", nil) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ req.Header.Set("Authorization", "Bearer "+token) ++ req.Header.Set("Accept", "application/json") ++ response, err := r.client.Do(req) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ defer response.Body.Close() ++ types := response.Header.Values("Content-Type") ++ if len(types) != 1 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ media, parameters, err := mime.ParseMediaType(types[0]) ++ if err != nil || media != "application/json" || (parameters["charset"] != "" && !strings.EqualFold(parameters["charset"], "utf-8")) || (len(parameters) != 0 && (len(parameters) != 1 || parameters["charset"] == "")) || response.StatusCode != 200 || response.Header.Get("Content-Encoding") != "" { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(response.Body, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ return ApprovalObservation{raw: append([]byte(nil), raw...), sourceID: r.config.SourceID, revision: r.config.SourceRevision, configurationDigest: r.configurationDigest}, nil ++} ++ ++// VerifiedApprovalFacts establishes typed fact correspondence, not a decision. ++// Only an authenticated fixed-reader observation can construct this handle. ++type VerifiedApprovalFacts struct { ++ original, fetched approvalfact.Fact ++ evidence api.ApprovalObservationProvenance ++ valid bool ++} ++ ++func (o ApprovalObservation) Validate(original []byte, expected approvalfact.Expected, now time.Time) (VerifiedApprovalFacts, error) { ++ if len(o.raw) == 0 || o.sourceID == "" || o.revision == "" || o.configurationDigest == "" { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ originalFact, err := approvalfact.ValidateClaim(original, expected, now) ++ if err != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ fetched, err := approvalfact.ValidateClaim(o.raw, expected, now) ++ if err != nil || approvalfact.MatchObserved(originalFact, fetched) != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ limit := originalFact.Deadline() ++ if fetched.Deadline().Before(limit) { ++ limit = fetched.Deadline() ++ } ++ evidence := api.ApprovalObservationProvenance{SourceID: o.sourceID, SourceRevision: o.revision, ConfigurationDigest: o.configurationDigest, ClaimDigest: byteDigest(o.raw), NativeBindingDigest: fetched.NativeDigest(), ObservedAt: fetched.ObservedAt().UTC().Format(time.RFC3339Nano), NotAfter: limit.UTC().Format(time.RFC3339Nano), ValidatedAt: now.UTC().Format(time.RFC3339Nano)} ++ return VerifiedApprovalFacts{original: originalFact, fetched: fetched, evidence: evidence, valid: true}, nil ++} ++func (f VerifiedApprovalFacts) Deadline() (time.Time, error) { ++ if !f.valid { ++ return time.Time{}, ErrApprovalSource ++ } ++ a := f.original.Deadline() ++ if f.fetched.Deadline().Before(a) { ++ a = f.fetched.Deadline() ++ } ++ return a, nil ++} ++func (f VerifiedApprovalFacts) Provenance() (*api.ApprovalObservationProvenance, error) { ++ if !f.valid { ++ return nil, ErrApprovalSource ++ } ++ copy := f.evidence ++ return ©, nil ++} +diff --git a/internal/taskexecution/approval_source_test.go b/internal/taskexecution/approval_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source_test.go +@@ -0,0 +1,118 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "encoding/pem" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "path/filepath" ++ "strings" ++ "sync/atomic" ++ "testing" ++ "time" ++) ++ ++func TestFixedApprovalReaderRefusalBoundary(t *testing.T) { ++ folder := t.TempDir() ++ token := filepath.Join(folder, "token") ++ ca := filepath.Join(folder, "ca") ++ if err := os.WriteFile(token, []byte("synthetic-source-token"), 0440); err != nil { ++ t.Fatal(err) ++ } ++ var mode atomic.Value ++ mode.Store("ok") ++ var foreignCalls atomic.Int64 ++ foreign := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ foreignCalls.Add(1) ++ t.Error("credential escaped fixed origin") ++ })) ++ defer foreign.Close() ++ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.Header.Get("Authorization") != "Bearer synthetic-source-token" || r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Method != "GET" { ++ t.Error("wrong auth/path/method") ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode.Load().(string) { ++ case "redirect": ++ w.Header().Set("Location", foreign.URL) ++ w.WriteHeader(302) ++ return ++ case "status": ++ w.WriteHeader(403) ++ case "large": ++ w.Write([]byte(strings.Repeat(" ", 524289))) ++ case "duplicate": ++ w.Write([]byte(`{"a":true,"a":false}`)) ++ case "invalid-utf8": ++ w.Write([]byte{'"', 255, '"'}) ++ case "media": ++ w.Header().Set("Content-Type", "application/json; arbitrary=authority") ++ w.Write([]byte(`{}`)) ++ case "duplicate-media": ++ w.Header().Add("Content-Type", "application/json") ++ w.Write([]byte(`{}`)) ++ case "compressed": ++ w.Header().Set("Content-Encoding", "gzip") ++ w.Write([]byte(`{}`)) ++ case "slow": ++ select { ++ case <-r.Context().Done(): ++ case <-time.After(time.Second): ++ } ++ default: ++ w.Write([]byte(`{}`)) ++ } ++ })) ++ defer server.Close() ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0600) ++ config := ApprovalSourceConfig{SourceID: "synthetic-source", SourceRevision: "synthetic-revision", Origin: server.URL, StoreTenant: "tenant:platform", CAFile: ca, TokenFile: token} ++ reader, err := NewApprovalReader(config) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err != nil { ++ t.Fatal(err) ++ } ++ for _, name := range []string{"redirect", "status", "large", "duplicate", "invalid-utf8", "media", "duplicate-media", "compressed", "slow"} { ++ t.Run(name, func(t *testing.T) { ++ mode.Store(name) ++ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) ++ defer cancel() ++ if _, err := reader.Observe(ctx, "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("invalid provider response admitted") ++ } ++ }) ++ } ++ if foreignCalls.Load() != 0 { ++ t.Fatal("redirect followed") ++ } ++ mode.Store("ok") ++ for _, bad := range []string{"../consume", "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/consume", ""} { ++ if _, err := reader.Observe(context.Background(), bad); err == nil { ++ t.Fatal("request selected route") ++ } ++ } ++ for _, mode := range []os.FileMode{0460, 0644, 0620} { ++ os.Chmod(token, mode) ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("unsafe token permissions") ++ } ++ } ++ for _, origin := range []string{"http://127.0.0.1", "https://user:password@localhost", "https://localhost/path", "https://localhost?query=1", "https://localhost#fragment"} { ++ bad := config ++ bad.Origin = origin ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("unsafe origin") ++ } ++ } ++ bad := config ++ bad.StoreTenant = "tenant:foreign" ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("foreign tenant configured") ++ } ++ if _, err := (ApprovalObservation{}).Validate([]byte(`{}`), approvalfact.Expected{}, time.Now()); err == nil { ++ t.Fatal("zero observation admitted") ++ } ++} +diff --git a/internal/taskexecution/service_input.go b/internal/taskexecution/service_input.go +--- a/internal/taskexecution/service_input.go ++++ b/internal/taskexecution/service_input.go +@@ -7,6 +7,7 @@ + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + "io" + "time" + ) +@@ -15,6 +16,7 @@ + + const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" + const ServicePilotPackageVersion = "v1" ++const TrustedServicePilotPackageVersion = "v2" + + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" +@@ -24,7 +26,10 @@ + // It installs the explicit + // proposed fixed subject-to-transport mapping, never a request-selected mapping. + // No constructor parameter can select warn/disabled mode or another subject. +-type ServicePilotInputs struct{ auth *callerauth.Authenticator } ++type ServicePilotInputs struct { ++ auth *callerauth.Authenticator ++ preserveNumbers bool ++} + + func NewServicePilotInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { + a, e := callerauth.New(callerauth.ModeEnforce, reviewer, "flex-auth", map[string]string{"secrets-engine": ServicePilotPrincipal}, nil) +@@ -34,12 +39,24 @@ + return &ServicePilotInputs{auth: a}, nil + } + ++// NewTrustedServiceInputs preserves numeric representations only for the exact ++// v2 task path; it grants no Approval-source trust or executable eligibility. ++func NewTrustedServiceInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { ++ input, err := NewServicePilotInputs(reviewer) ++ if err != nil { ++ return nil, err ++ } ++ input.preserveNumbers = true ++ return input, nil ++} ++ + // ServiceInput cannot be fabricated through exported fields. It proves only + // source input preparation through TokenReview, not approval provenance/consume + // or execution authority. Default zero value is not an authenticated input. + type ServiceInput struct { +- serialized []byte +- caller callerauth.Record ++ serialized []byte ++ caller callerauth.Record ++ preserveNumbers bool + } + + func (s ServiceInput) Request() (api.CheckRequest, error) { +@@ -47,7 +64,11 @@ + return api.CheckRequest{}, ErrServiceInput + } + var r api.CheckRequest +- if json.Unmarshal(s.serialized, &r) != nil { ++ decoder := json.NewDecoder(bytes.NewReader(s.serialized)) ++ if s.preserveNumbers { ++ decoder.UseNumber() ++ } ++ if decoder.Decode(&r) != nil { + return r, ErrServiceInput + } + return r, nil +@@ -142,5 +163,27 @@ + if err != nil { + return refuse() + } +- return ServiceInput{serialized: raw, caller: record}, nil ++ return ServiceInput{serialized: raw, caller: record, preserveNumbers: s.preserveNumbers}, nil + } ++ ++func (s ServiceInput) NumberPreserved() bool { return s.preserveNumbers } ++ ++// DecodeTrustedServiceRequest preserves numbers in the exact new task profile. ++// Owner helper validates strict Unicode/JSON; this is not a new canonicalizer. ++func DecodeTrustedServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ var request api.CheckRequest ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} +diff --git a/pkg/api/canonical.go b/pkg/api/canonical.go +--- a/pkg/api/canonical.go ++++ b/pkg/api/canonical.go +@@ -635,16 +635,30 @@ + + // DecisionProvenance captures evaluator and policy provenance. + type DecisionProvenance struct { +- Evaluator string `json:"evaluator" yaml:"evaluator"` +- Mode string `json:"mode" yaml:"mode"` +- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` +- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` +- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` +- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` +- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` +- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` +- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` +- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++ Evaluator string `json:"evaluator" yaml:"evaluator"` ++ Mode string `json:"mode" yaml:"mode"` ++ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` ++ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` ++ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` ++ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` ++ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` ++ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` ++ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` ++ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"` ++ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++} ++ ++// ApprovalObservationProvenance binds fresh server-owned fact evidence without ++// rewriting the original carried claim/context or either PDP request digest. ++type ApprovalObservationProvenance struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ ConfigurationDigest string `json:"configuration_digest"` ++ ClaimDigest string `json:"claim_digest"` ++ NativeBindingDigest string `json:"native_binding_digest"` ++ ObservedAt string `json:"observed_at"` ++ NotAfter string `json:"not_after"` ++ ValidatedAt string `json:"validated_at"` + } + + // CallerProvenance records how the request was authenticated to flex-auth. +diff --git a/third_party/approvalfact/README.md b/third_party/approvalfact/README.md +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/README.md +@@ -0,0 +1,55 @@ ++# Approval fact data adapter ++ ++Provider-owned pure Go module for Access's Approval B2 source reader. No network, ++credentials, subprocess, policy evaluator, server or consume mutation exists here. ++Python `approval_engine.binding` remains the authoritative native format. ++ ++API: ++ ++- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes ++ and sha256 digest; extra binding metadata is excluded. ++- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path ++ claim against independently owner-derived `Expected.NativeBindingJSON`, exact ++ approval ID and actual Access approval-free PDP digest. ++- `MatchObserved(submitted, observed)` requires two validated facts at the same ++ captured instant and identical nonfreshness authority facts. No request is ++ rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines ++ preserve each input's own freshness and approval expiry. ++ ++`Fact` means validated data, never authenticated provider origin. Access must ++fetch provider bytes through its independently admitted fixed origin/identity, ++then wrap this data in its own unforgeable source handle. A caller-supplied claim ++cannot become source truth merely by passing this helper. Access remains the ++single evaluator and must bound decision/operation/caller lifetimes separately. ++PEP consume CAS and live one-shot journal remain required before any effect. ++ ++Expected native act bytes must preserve the actual owner/compiler/normalization ++representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and ++large integers. Raw submitted resource is sufficient only when Access proves ++the active registry cannot override that target and applies its exact existing ++normalization. Otherwise authoritative raw enrichment/compiler evidence is ++needed. This helper supplies no fallback or invented restricted numeric domain. ++ ++The strict parser retains integer/floating lexical types, arbitrary integer ++magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values. ++Canonical float/string formatting follows Python source, including negative ++floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate ++object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. ++Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at ++most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++These explicit parser/resource limits fail closed, never normalize authority. ++ ++`go test -race ./...` checks the portable producer vectors, typed fact pairing, ++strict JSON/Unicode and exact deadline semantics. From the owner repo root, ++`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case ++development-only differential corpus against the actual Python producer, ++including random finite IEEE bit patterns, exponent boundaries/subnormals and ++Unicode. It uses a test-only local oracle file; production imports no oracle or ++Python runtime. Sample conformance is evidence, not a formal proof over all JSON. ++ ++The module pins Go 1.26.7 to match the existing Access consumer toolchain. ++`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner ++fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`. ++Transport cases requiring HTTP remain the Access adapter's tests; this module ++checks their duplicate/nonfinite bodies only. Import/deployment/native identity ++admission and positive policy gates require separate owner evidence. +diff --git a/third_party/approvalfact/SOURCE.json b/third_party/approvalfact/SOURCE.json +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/SOURCE.json +@@ -0,0 +1,13 @@ ++{ ++ "owner": "approval-engine", ++ "module": "github.com/netkingdom/approval-engine/adapters/go/approvalfact", ++ "source_artifact": "approval-go-fact/manifest.json", ++ "review_status": "owner-frozen; independent coordinator review required", ++ "role": "pure data canonicalization and fact correspondence only; not provider authentication or PDP", ++ "files_sha256": { ++ "canonical.go": "d48588fb26e9037c8e83421586b8a818f4c0a52afb874f3fadf7229fc971c282", ++ "claim.go": "007a2494f79c47c104635402407e9e68f17d8599d23e0f2c8ebf6cb1b47e3621", ++ "go.mod": "bb09c53ffea926fbfba6e580ed3bd71ca358575401717389b3c7c49c0c674b43", ++ "README.md": "5d936b7f3de87bbf4e946f0dc44823d2f69b62b4122b55b9064791b7a19fd4f5" ++ } ++} +diff --git a/third_party/approvalfact/canonical.go b/third_party/approvalfact/canonical.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/canonical.go +@@ -0,0 +1,295 @@ ++// Package approvalfact provides data-only Approval claim correspondence. ++// It neither authenticates a provider nor decides permission or consumes objects. ++package approvalfact ++ ++import ( ++ "bytes" ++ "crypto/sha256" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "math" ++ "math/big" ++ "sort" ++ "strconv" ++ "strings" ++ "unicode/utf8" ++) ++ ++const MaxJSONBytes = 1 << 20 ++ ++var errJSON = errors.New("invalid Approval JSON") ++ ++// DecodeJSON retains numeric lexical types, refuses duplicates and invalid ++// Unicode, and bounds work. A Go map previously decoded into float64 cannot ++// recover the producer's integer-versus-floating identity; pass source bytes. ++func DecodeJSON(raw []byte) (any, error) { ++ if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) { ++ return nil, errJSON ++ } ++ d := json.NewDecoder(bytes.NewReader(raw)) ++ d.UseNumber() ++ var read func(int) (any, error) ++ read = func(depth int) (any, error) { ++ if depth > 128 { ++ return nil, errJSON ++ } ++ t, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ switch x := t.(type) { ++ case json.Delim: ++ if x == '{' { ++ m := map[string]any{} ++ for d.More() { ++ k, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ s, ok := k.(string) ++ if !ok { ++ return nil, errJSON ++ } ++ if _, ok = m[s]; ok { ++ return nil, errJSON ++ } ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ m[s] = v ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim('}') { ++ return nil, errJSON ++ } ++ return m, nil ++ } ++ if x == '[' { ++ a := []any{} ++ for d.More() { ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ a = append(a, v) ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim(']') { ++ return nil, errJSON ++ } ++ return a, nil ++ } ++ return nil, errJSON ++ case json.Number: ++ if _, e := pythonNumber(x); e != nil { ++ return nil, e ++ } ++ return x, nil ++ case string, bool, nil: ++ return x, nil ++ default: ++ return nil, errJSON ++ } ++ } ++ v, e := read(0) ++ if e != nil { ++ return nil, e ++ } ++ if _, e = d.Token(); e != io.EOF { ++ return nil, errJSON ++ } ++ return v, nil ++} ++ ++// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer ++// cannot encode them as UTF-8. Reject before the decoder can erase that fact. ++func validSurrogates(raw []byte) bool { ++ in := false ++ for i := 0; i < len(raw); i++ { ++ if raw[i] == '"' { ++ in = !in ++ continue ++ } ++ if !in || raw[i] != '\\' { ++ continue ++ } ++ i++ ++ if i >= len(raw) { ++ return false ++ } ++ if raw[i] != 'u' { ++ continue ++ } ++ if i+4 >= len(raw) { ++ return false ++ } ++ n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16) ++ if e != nil { ++ return false ++ } ++ i += 4 ++ if n >= 0xdc00 && n <= 0xdfff { ++ return false ++ } ++ if n < 0xd800 || n > 0xdbff { ++ continue ++ } ++ if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' { ++ return false ++ } ++ low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16) ++ if e != nil || low < 0xdc00 || low > 0xdfff { ++ return false ++ } ++ i += 6 ++ } ++ return !in ++} ++ ++func pythonNumber(n json.Number) (string, error) { ++ s := string(n) ++ if !strings.ContainsAny(s, ".eE") { ++ i, ok := new(big.Int).SetString(s, 10) ++ if !ok { ++ return "", errJSON ++ } ++ return i.String(), nil ++ } ++ f, e := strconv.ParseFloat(s, 64) ++ if e != nil || math.IsInf(f, 0) || math.IsNaN(f) { ++ return "", errJSON ++ } ++ // Python repr uses shortest round-trip digits, fixed form for exponents ++ // -4..15, a mandatory .0 for integral floats, and two exponent digits. ++ sci := strconv.FormatFloat(f, 'e', -1, 64) ++ at := strings.IndexByte(sci, 'e') ++ exp, _ := strconv.Atoi(sci[at+1:]) ++ if exp >= -4 && exp < 16 { ++ s = strconv.FormatFloat(f, 'f', -1, 64) ++ if !strings.Contains(s, ".") { ++ s += ".0" ++ } ++ return s, nil ++ } ++ sign := "+" ++ if exp < 0 { ++ sign = "-" ++ exp = -exp ++ } ++ digits := strconv.Itoa(exp) ++ if len(digits) < 2 { ++ digits = "0" + digits ++ } ++ return sci[:at] + "e" + sign + digits, nil ++} ++ ++func quote(s string) string { ++ var b strings.Builder ++ b.WriteByte('"') ++ for _, r := range s { ++ switch r { ++ case '"', '\\': ++ b.WriteByte('\\') ++ b.WriteRune(r) ++ case '\b': ++ b.WriteString(`\b`) ++ case '\f': ++ b.WriteString(`\f`) ++ case '\n': ++ b.WriteString(`\n`) ++ case '\r': ++ b.WriteString(`\r`) ++ case '\t': ++ b.WriteString(`\t`) ++ default: ++ if r < 32 { ++ b.WriteString(`\u00`) ++ const h = "0123456789abcdef" ++ b.WriteByte(h[byte(r)>>4]) ++ b.WriteByte(h[byte(r)&15]) ++ } else { ++ b.WriteRune(r) ++ } ++ } ++ } ++ b.WriteByte('"') ++ return b.String() ++} ++ ++func canonical(v any) (string, error) { ++ switch x := v.(type) { ++ case nil: ++ return "null", nil ++ case bool: ++ if x { ++ return "true", nil ++ } ++ return "false", nil ++ case string: ++ return quote(x), nil ++ case json.Number: ++ return pythonNumber(x) ++ case []any: ++ a := make([]string, len(x)) ++ for i, v := range x { ++ s, e := canonical(v) ++ if e != nil { ++ return "", e ++ } ++ a[i] = s ++ } ++ return "[" + strings.Join(a, ",") + "]", nil ++ case map[string]any: ++ keys := make([]string, 0, len(x)) ++ for k := range x { ++ keys = append(keys, k) ++ } ++ sort.Strings(keys) ++ a := make([]string, 0, len(keys)) ++ for _, k := range keys { ++ s, e := canonical(x[k]) ++ if e != nil { ++ return "", e ++ } ++ a = append(a, quote(k)+":"+s) ++ } ++ return "{" + strings.Join(a, ",") + "}", nil ++ default: ++ return "", errJSON ++ } ++} ++ ++// CanonicalBinding reproduces approval_engine.binding.binding_digest over the ++// five native act fields. Extra binding metadata is excluded by that contract. ++func CanonicalBinding(raw []byte) ([]byte, string, error) { ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return nil, "", e ++ } ++ m, ok := v.(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act := map[string]any{} ++ for _, k := range []string{"action", "actor", "principal", "purpose"} { ++ s, ok := m[k].(string) ++ if !ok || s == "" { ++ return nil, "", errJSON ++ } ++ act[k] = s ++ } ++ target, ok := m["target"].(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act["target"] = target ++ s, e := canonical(act) ++ if e != nil { ++ return nil, "", e ++ } ++ sum := sha256.Sum256([]byte(s)) ++ return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil ++} +diff --git a/third_party/approvalfact/claim.go b/third_party/approvalfact/claim.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/claim.go +@@ -0,0 +1,170 @@ ++package approvalfact ++ ++import ( ++ "encoding/json" ++ "errors" ++ "regexp" ++ "strconv" ++ "time" ++) ++ ++var errClaim = errors.New("Approval fact does not match required claim") ++var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) ++var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) ++var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`) ++ ++// Expected contains independently owner-derived native act bytes and the actual ++// Access approval-free digest. Marshaling an already float64-decoded Go request ++// is not an authoritative reconstruction of the native act. ++type Expected struct { ++ ApprovalID string ++ NativeBindingJSON []byte ++ PDPDigest string ++} ++ ++// Fact is validated data shape/correspondence, NOT authenticated provider origin ++// or execution authority. Access must wrap it only after its own source admission. ++type Fact struct { ++ raw []byte ++ stable string ++ digest string ++ observed, timeLimit, validatedAt time.Time ++} ++ ++func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) } ++func (f Fact) ObservedAt() time.Time { return f.observed } ++func (f Fact) Deadline() time.Time { return f.timeLimit } ++func (f Fact) NativeDigest() string { return f.digest } ++ ++func members(m map[string]any, required, optional []string) bool { ++ allowed := map[string]bool{} ++ for _, k := range required { ++ allowed[k] = true ++ if _, ok := m[k]; !ok { ++ return false ++ } ++ } ++ for _, k := range optional { ++ allowed[k] = true ++ } ++ for k := range m { ++ if !allowed[k] { ++ return false ++ } ++ } ++ return true ++} ++func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok } ++func stamp(v any) (time.Time, error) { ++ s, ok := v.(string) ++ if !ok || !timePattern.MatchString(s) { ++ return time.Time{}, errClaim ++ } ++ t, e := time.Parse(time.RFC3339Nano, s) ++ if e != nil { ++ return time.Time{}, errClaim ++ } ++ return t, nil ++} ++ ++// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim ++// at a caller-captured instant, using the existing Approval producer semantics. ++// It has no I/O, policy evaluator, credentials, cache, or consume mutation. ++func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) { ++ if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) { ++ return Fact{}, errClaim ++ } ++ _, want, e := CanonicalBinding(expected.NativeBindingJSON) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ m, ok := object(v) ++ if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) { ++ return Fact{}, errClaim ++ } ++ if y, ok := m["yields_to"]; ok { ++ if _, ok = y.(string); !ok { ++ return Fact{}, errClaim ++ } ++ } ++ if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" { ++ return Fact{}, errClaim ++ } ++ b, ok := object(m["binding"]) ++ if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) { ++ return Fact{}, errClaim ++ } ++ if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true { ++ return Fact{}, errClaim ++ } ++ text, e := canonical(b) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ _, got, e := CanonicalBinding([]byte(text)) ++ if e != nil || got != want { ++ return Fact{}, errClaim ++ } ++ f, ok := object(m["freshness"]) ++ if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) { ++ return Fact{}, errClaim ++ } ++ observed, e := stamp(f["observed_at"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ notAfter, e := stamp(f["not_after"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ ttl, ok := f["ttl_seconds"].(json.Number) ++ if !ok { ++ return Fact{}, errClaim ++ } ++ seconds, e := strconv.ParseInt(string(ttl), 10, 64) ++ if e != nil || seconds < 1 { ++ return Fact{}, errClaim ++ } ++ // Bound arithmetic to time.Duration's representable seconds, not a new ++ // semantic freshness grant or configured TTL. Unsupported extremes refuse. ++ if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) { ++ return Fact{}, errClaim ++ } ++ validity, ok := object(m["validity"]) ++ if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) { ++ return Fact{}, errClaim ++ } ++ before, e := stamp(validity["not_before"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ expires, e := stamp(validity["expires_at"]) ++ if e != nil || now.Before(before) || !now.Before(expires) { ++ return Fact{}, errClaim ++ } ++ limit := notAfter ++ if expires.Before(limit) { ++ limit = expires ++ } ++ delete(m, "freshness") ++ delete(m, "yields_to") ++ stable, e := canonical(m) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil ++} ++ ++// MatchObserved requires independently validated original/fetched facts at the ++// same captured instant and exact nonfreshness authority correspondence. It ++// never rewrites the original request or extends its original deadline. ++func MatchObserved(submitted, observed Fact) error { ++ if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable { ++ return errClaim ++ } ++ return nil ++} +diff --git a/third_party/approvalfact/go.mod b/third_party/approvalfact/go.mod +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/go.mod +@@ -0,0 +1,3 @@ ++module github.com/netkingdom/approval-engine/adapters/go/approvalfact ++ ++go 1.26.7 +diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -237,3 +237,26 @@ + and canonical vectors; B3 additionally needs exact source/normalization numeric + representation and full trusted provider joins. No current policy false gate, + production trust, native deployment or protected effect is enabled by B1. ++ ++2026-10-16 B2/B3 source successor preparation (separate from frozen B1): the ++isolated v2 source path now joins the fixed authenticated Approval reader, ++unchanged Approval-owner canonical/fact helper, original/fresh correspondence, ++raw numeric request/registry source retention, same policy evaluator and signed ++source provenance. Disposable actual HTTP and production serve subprocess prove ++synthetic signed ALLOW and fail-closed negative joins with exact response/record ++parity. See `docs/trusted-service-task-source.md`. T06 remains `progress`: this ++source proposal requires independent cross-owner review and full consumer/Platform ++successor integration, then separately admitted runtime custody, source trust, ++image provenance and rollout. No source-only proof establishes those runtime gates. ++ ++ ++2026-10-16 B2/B3 R2 temporal correction (R1 retained): whole-stack review exposed ++that R1's 15s ALLOW and default Approval freshness30 cannot fit the declared ++minimum backendTTL30 plus existing cleanupreserve5. The v2 successor now derives ++exact ALLOW expiry from the immutable operationdeadline, requiring all original/ ++fresh approval/freshness/caller bounds, a conservative source maximum120 and ++remaining declaredTTL+5 after source I/O/evaluation. No consumer/native deadline ++or cleanup guard is relaxed. Max structural budgets that cannot fit refuse. ++The fixture declares backendTTL30/deadline60 and explicit source freshness120; ++owner runtime source freshness configuration and full native integration remain ++separate reviewed gates. See docs/trusted-service-task-source.md. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source-smoke.py b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source-smoke.py new file mode 100644 index 00000000..69b02183 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source-smoke.py @@ -0,0 +1,101 @@ +"""Disposable synthetic proof: actual Approval App HTTPS -> actual Access serve. +No native config, credential environment, approval or backend effect is used. +""" +import base64, hashlib, http.server, io, json, os, pathlib, socket, ssl, subprocess, sys, tempfile, threading, time +from datetime import datetime, timedelta, timezone +from wsgiref.simple_server import make_server, WSGIRequestHandler +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +sys.path.insert(0, '/home/worsch/approval-engine') +sys.path.insert(0, '/home/worsch/secrets-engine/src') +from approval_engine.api import App +from approval_engine.auth import Identity, StaticTokenAuthenticator +from approval_engine.store import Engine +from secrets_engine.authorization import request_digest, approval_binding_digest +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +OUT=pathlib.Path(sys.argv[1]);OUT.mkdir(exist_ok=True) +if any(OUT.iterdir()):raise RuntimeError('use a fresh empty proof output directory') +def digest(raw):return 'sha256:'+hashlib.sha256(raw).hexdigest() +def write(path,raw,mode=0o600):path.write_bytes(raw);path.chmod(mode) +def identity(subject,kind,scopes):return Identity(subject=subject,issuer='synthetic-test-only',audiences=('approval-engine',),principal_type=kind,tenant='tenant:platform',roles=frozenset(),scopes=frozenset(scopes),assurance={'level':'synthetic'},evidence_ref='synthetic-test-only',tenant_source='directory') +class Quiet(WSGIRequestHandler): + def log_message(self,*args):pass +class Review(http.server.BaseHTTPRequestHandler): + def do_POST(self): + raw=self.rfile.read(int(self.headers['Content-Length']));body=json.loads(raw) + assert self.path=='/apis/authentication.k8s.io/v1/tokenreviews' + assert self.headers['Authorization']=='Bearer synthetic-reviewer' + assert body['spec']['audiences']==['flex-auth','access-engine'] + self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps({'status':{'authenticated':True,'audiences':['flex-auth'],'user':{'username':'system:serviceaccount:secrets-engine:secrets-engine'}}}).encode()) + def log_message(self,*args):pass +with tempfile.TemporaryDirectory(prefix='access-actual-python-source-') as folder: + root=pathlib.Path(folder);now=datetime.now(timezone.utc) + key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'localhost')]);cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('localhost')]),critical=False).sign(key,hashes.SHA256())) + ca=root/'ca.pem';private=root/'tls-key.pem';write(ca,cert.public_bytes(serialization.Encoding.PEM));write(private,key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())) + tls=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER);tls.load_cert_chain(ca,private) + ctx=ssl.create_default_context(cafile=str(ca)) + def start(server):server.socket=tls.wrap_socket(server.socket,server_side=True);threading.Thread(target=server.serve_forever,daemon=True).start();return 'https://localhost:'+str(server.server_port) + review=http.server.HTTPServer(('127.0.0.1',0),Review);review_origin=start(review) + engine=Engine(root/'approval.sqlite3',freshness_ttl=120) + auth=StaticTokenAuthenticator({'synthetic-creator':identity('secrets-engine','service',{'approval:create'}),'synthetic-human':identity('synthetic-human','human',{'approval:approve'}),'synthetic-reader':identity('synthetic-access-reader','service',{'approval:read'})}) + app=App(engine,auth) + source=make_server('127.0.0.1',0,app,handler_class=Quiet);origin=start(source) + def source_call(method,path,body,token): + request=Request(origin+path,data=json.dumps(body).encode() if body is not None else None,method=method,headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'}) + with urlopen(request,context=ctx,timeout=3) as response:return response.status,json.load(response) + # Reuse existing owner interoperability contract generator; it has synthetic metadata only. + generated=subprocess.check_output([sys.executable,'/tmp/access-trusted-b2-b3-r3/tools/service_task_execution_fixture.py'],env={'PATH':os.defpath}) + request=json.loads(generated)['request'];request['subject']['tenant']='tenant:platform';request['resource']['tenant']='tenant:platform';request['context']['purpose']='actual Python App source-only synthetic proof';del request['context']['approval'];request['context']['execution_contract']['backend_budget']['ttl_seconds']=30;request['context']['execution_contract']['operation_deadline']=(now+timedelta(seconds=60)).replace(microsecond=0).isoformat() + seed=root/'seed';write(seed,base64.urlsafe_b64encode(bytes([19])*32).rstrip(b'='),0o440) + reviewer_token=root/'review-token';write(reviewer_token,b'synthetic-reviewer',0o440) + reader_token=root/'reader-token';write(reader_token,b'synthetic-reader',0o440) + registry=root/'registry.json';write(registry,pathlib.Path('/tmp/access-trusted-b2-b3-r3/examples/secrets-engine/registry_snapshot.json').read_bytes()) + config=root/'source.json';write(config,json.dumps({'source_id':'synthetic-actual-approval-app','source_revision':'actual-owner-python-source-fixture','origin':origin,'store_tenant':'tenant:platform','ca_file':str(ca),'token_file':str(reader_token)}).encode()) + listener=socket.socket();listener.bind(('127.0.0.1',0));port=listener.getsockname()[1];listener.close() + record=root/'record.jsonl';url=f'http://127.0.0.1:{port}' + command=['/tmp/netkingdom-access-trusted-r3','serve','--addr',f'127.0.0.1:{port}','--registry',str(registry),'--policy','/tmp/access-trusted-b2-b3-r3/examples/secrets-engine-service-task-execution-v2/policy_package.md','--log',str(record),'--caller-auth-mode','enforce','--caller-audience','flex-auth','--caller-kubernetes-url',review_origin,'--caller-reviewer-token-file',str(reviewer_token),'--caller-ca-file',str(ca),'--caller-binding','secrets-engine=system:serviceaccount:secrets-engine:secrets-engine','--envelope-signing-key-file',str(seed),'--envelope-signing-kid','synthetic-v2','--approval-source-file',str(config)] + child=subprocess.Popen(command,env={},stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) + try: + for _ in range(100): + try: + with urlopen(url+'/healthz',timeout=.2):break + except Exception:time.sleep(.02) + jwt='synthetic.'+base64.urlsafe_b64encode(json.dumps({'exp':int((now+timedelta(minutes=2)).timestamp())}).encode()).decode().rstrip('=')+'.synthetic' + def check(value): + req=Request(url+'/v1/check',data=json.dumps(value).encode(),headers={'Authorization':'Bearer '+jwt,'Content-Type':'application/json'}) + try: + with urlopen(req,timeout=5) as response:return response.status,response.read() + except HTTPError as error:return error.code,error.read() + claimfree=json.loads(json.dumps(request)) + pre_status,pre_raw=check(claimfree);assert pre_status==200,(pre_status,pre_raw) + pre_file=root/'preapproval.json';write(pre_file,pre_raw) + subprocess.run(['/tmp/netkingdom-access-owner-proof-verifier',str(pre_file),'/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r2/actual-python-source/synthetic-public-trust.json',str(pre_file)],env={},check=True,stdout=subprocess.DEVNULL) + pre=json.loads(pre_raw);assert pre['effect']=='deny' and 'lifetime' not in pre and pre['signature']['mode']=='signed' + assert pre['binding']['submitted_request_digest']==request_digest(claimfree) + assert pre['binding']['request_digest']==pre['binding']['approval_binding_digest']!=request_digest(claimfree) + assert pre['binding']['context']==claimfree['context'] and pre['diagnostics']['registry_subject'] is True + assert pre['provenance']['policy_package']=='secrets-engine.task-execution.service-pilot' and pre['provenance']['policy_version']=='v2' + assert pre['provenance']['policy_package_digest']=='sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d' + native={'action':pre['binding']['action'],'actor':pre['binding']['subject']['id'],'principal':pre['binding']['subject'].get('attributes',{}).get('principal') or pre['binding']['subject']['id'],'purpose':pre['binding']['context']['purpose'],'target':pre['binding']['resource']} + aid='aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + status,created=source_call('POST','/v1/approvals',{'id':aid,'binding':native,'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()},'required_count':1,'pdp_digest':pre['binding']['approval_binding_digest'],'pdp_path':True,'human_control':True},'synthetic-creator');assert status==201 + status,_=source_call('POST',f'/v1/approvals/{aid}/entries',{},'synthetic-human');assert status==200 + status,claim=source_call('GET',f'/v1/approvals/{aid}/claim',None,'synthetic-reader');assert status==200 and claim['valid_now'] is True and claim['binding']['human_control'] is True + request['context']['approval']=claim + status,raw=check(request);assert status==200,(status,raw) + response=json.loads(raw);assert response['effect']=='allow' and response['signature']['mode']=='signed' + assert response['lifetime']['expires_at']==request['context']['execution_contract']['operation_deadline'].replace('+00:00','Z');assert response['binding']['context']==request['context'];assert response['binding']['submitted_request_digest']==request_digest(request);assert response['binding']['approval_binding_digest']==pre['binding']['approval_binding_digest'] + assert response['provenance']['approval_observation']['source_id']=='synthetic-actual-approval-app' + saved=record.read_bytes();records=[json.loads(line) for line in saved.splitlines()];assert len(records)==2 and records[0]==pre and records[1]==response + # Actual producer state transition is fetched anew and cannot be rescued by carried claim. + engine.revoke(aid) + refused,_=check(request);assert refused!=200 and record.read_bytes()==saved + for name,value in [('request.json',request),('response.json',response),('source-record.json',records[1]),('preapproval-receipt.json',pre),('preapproval-source-record.json',records[0]),('claimfree-request.json',claimfree),('original-approval-claim.json',claim)]:write(OUT/name,(json.dumps(value,indent=2)+'\n').encode(),0o644) + receipt={'scope':'disposable synthetic only','actual_source':'Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine','actual_access':'production serve binary from B2/B3 R3 retained-registry preapproval successor','allow':True,'backend_ttl_seconds':30,'operation_authority_seconds':60,'explicit_approval_freshness_seconds':120,'decision_expiry_equals_exact_operation_deadline':True,'original_context_unchanged':True,'published_digest_match':True,'returned_source_record_match':True,'actual_source_revoke_refused':True,'no_record_after_refusal':True,'native_admission':False,'retained_registry_sha256':digest(registry.read_bytes()),'signed_deny_supplies_facts_not_execution':True,'approval_issued_from_authenticated_normalized_binding':True,'submitted_proposal_unchanged_except_original_issued_claim':True,'files_sha256':{p.name:digest(p.read_bytes()) for p in OUT.iterdir() if p.suffix=='.json' and p.name!='receipt.json'}} + write(OUT/'receipt.json',(json.dumps(receipt,indent=2)+'\n').encode(),0o644);print(json.dumps(receipt,indent=2)) + finally: + child.terminate();child.wait(timeout=5);source.shutdown();review.shutdown();engine.close() diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/allow-signature-verification.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/allow-signature-verification.json new file mode 100644 index 00000000..056d6192 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/allow-signature-verification.json @@ -0,0 +1 @@ +{"existing_producer_signature_verified":true,"independent_synthetic_public_fixture":true,"returned_record_canonical_bytes_match":true,"new_signature_implementation":false} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/claimfree-request.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/claimfree-request.json new file mode 100644 index 00000000..d11b07a6 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/claimfree-request.json @@ -0,0 +1,103 @@ +{ + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod" + }, + "tenant": "tenant:platform" + }, + "context": { + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "human_control": true, + "service_actor_binding": { + "version": "proposed-service-task-caller/v1", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform" + }, + "purpose": "actual Python App source-only synthetic proof" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/original-approval-claim.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/original-approval-claim.json new file mode 100644 index 00000000..4d82fd57 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/original-approval-claim.json @@ -0,0 +1,39 @@ +{ + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T15:00:50+00:00", + "observed_at": "2026-10-16T14:58:50+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T15:01:50.071540+00:00", + "not_before": "2026-10-16T14:57:50.071540+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-receipt.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-receipt.json new file mode 100644 index 00000000..4dda8551 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-receipt.json @@ -0,0 +1,196 @@ +{ + "id": "decision:035194cd9fdcc3c1", + "contract_version": "flex-auth.decision-record.v1", + "effect": "deny", + "reason": "service_execution_contract_unmatched", + "matched_policy_version": "v2", + "matched_rule": "service_execution_contract_unmatched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "approval_binding_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "submitted_request_digest": "sha256:1775bdd19296cb013659bb24a61e76ef252524bd23db674bb58b9429588c2962" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": true + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:b19fd358cf9b0aced2ad60ac83a3284e1672d158479a4a0f78663bd213b5c810" + }, + "decision_time": "2026-10-16T14:58:50Z", + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T15:00:50Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "jm09aiLHWNgWj7blcFDEtGFfGtWiwtdkWhUQzcFSJ_WgDq2U_QEnaifBaIYWjRuXmw3Ac8aXuaLXvmRiUmzbCQ" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-signature-verification.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-signature-verification.json new file mode 100644 index 00000000..056d6192 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-signature-verification.json @@ -0,0 +1 @@ +{"existing_producer_signature_verified":true,"independent_synthetic_public_fixture":true,"returned_record_canonical_bytes_match":true,"new_signature_implementation":false} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-source-record.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-source-record.json new file mode 100644 index 00000000..4dda8551 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/preapproval-source-record.json @@ -0,0 +1,196 @@ +{ + "id": "decision:035194cd9fdcc3c1", + "contract_version": "flex-auth.decision-record.v1", + "effect": "deny", + "reason": "service_execution_contract_unmatched", + "matched_policy_version": "v2", + "matched_rule": "service_execution_contract_unmatched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "approval_binding_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "submitted_request_digest": "sha256:1775bdd19296cb013659bb24a61e76ef252524bd23db674bb58b9429588c2962" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": true + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:b19fd358cf9b0aced2ad60ac83a3284e1672d158479a4a0f78663bd213b5c810" + }, + "decision_time": "2026-10-16T14:58:50Z", + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T15:00:50Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "jm09aiLHWNgWj7blcFDEtGFfGtWiwtdkWhUQzcFSJ_WgDq2U_QEnaifBaIYWjRuXmw3Ac8aXuaLXvmRiUmzbCQ" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/receipt.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/receipt.json new file mode 100644 index 00000000..57f017c0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/receipt.json @@ -0,0 +1,29 @@ +{ + "scope": "disposable synthetic only", + "actual_source": "Approval App over TLS with authenticated create, configured synthetic human entry, read; actual SQLite engine", + "actual_access": "production serve binary from B2/B3 R3 retained-registry preapproval successor", + "allow": true, + "backend_ttl_seconds": 30, + "operation_authority_seconds": 60, + "explicit_approval_freshness_seconds": 120, + "decision_expiry_equals_exact_operation_deadline": true, + "original_context_unchanged": true, + "published_digest_match": true, + "returned_source_record_match": true, + "actual_source_revoke_refused": true, + "no_record_after_refusal": true, + "native_admission": false, + "retained_registry_sha256": "sha256:37fba44ec0e0fd9b1e17a05ad1b7e4f9bb6ec9e0fc0bb3133e8af2ac6f832d7c", + "signed_deny_supplies_facts_not_execution": true, + "approval_issued_from_authenticated_normalized_binding": true, + "submitted_proposal_unchanged_except_original_issued_claim": true, + "files_sha256": { + "preapproval-receipt.json": "sha256:dca4347b07516b2070fb3a80c48649f52fb3b49410eae76f5d8f5b15a79862a5", + "preapproval-source-record.json": "sha256:dca4347b07516b2070fb3a80c48649f52fb3b49410eae76f5d8f5b15a79862a5", + "source-record.json": "sha256:45da993dae29a881452616be6b13276549116c8e3fb5364016009b422b4a5bcb", + "claimfree-request.json": "sha256:398437740393b3a9cc9e6063e8c022b84f587942e71e9f35153c23f128e3f300", + "response.json": "sha256:45da993dae29a881452616be6b13276549116c8e3fb5364016009b422b4a5bcb", + "original-approval-claim.json": "sha256:92b646831ab55ce9433f844ea6f7c22be94289b5f118799d17a7044532dccfa4", + "request.json": "sha256:323e6309b3c4a5ab45ff54e5850e5a4e3ce001c1dfeb8e6fff69e55a093b275e" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/request.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/request.json new file mode 100644 index 00000000..e8260ea4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/request.json @@ -0,0 +1,142 @@ +{ + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform" + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "attributes": { + "stage": "prod" + }, + "tenant": "tenant:platform" + }, + "context": { + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "human_control": true, + "service_actor_binding": { + "version": "proposed-service-task-caller/v1", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform" + }, + "purpose": "actual Python App source-only synthetic proof", + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T15:00:50+00:00", + "observed_at": "2026-10-16T14:58:50+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T15:01:50.071540+00:00", + "not_before": "2026-10-16T14:57:50.071540+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/response.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/response.json new file mode 100644 index 00000000..df55f685 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/response.json @@ -0,0 +1,251 @@ +{ + "id": "decision:d4c3d2d2f6827449", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T15:00:50+00:00", + "observed_at": "2026-10-16T14:58:50+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T15:01:50.071540+00:00", + "not_before": "2026-10-16T14:57:50.071540+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:fdce7858cbfb3b945694c27a808b3559ca0ad18ff397e88ace1b0a899f57db2a", + "approval_binding_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "submitted_request_digest": "sha256:55da69cfae4f20000177333d27d97c739e6ad55609f5fefe7f7b83503d7da4b5" + }, + "lifetime": { + "kind": "ttl", + "ttl": "59.4099177s", + "not_before": "2026-10-16T14:58:50.5900823Z", + "expires_at": "2026-10-16T14:59:50Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": true + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:3ccefed2cbf9583b74cd5dd1ed92c345f3612f2bdac965e111aeab96da029c27" + }, + "decision_time": "2026-10-16T14:58:50Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:fc4603a7a332ca886d7024b9bb16d76e82c467fc39048e2bcc7e0c8758a09f34", + "claim_digest": "sha256:51f0d25dd1dfaf28b6b411b1165976cfe028e7833ef4143d38fd5f35632d1b19", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T14:58:50Z", + "not_after": "2026-10-16T15:00:50Z", + "validated_at": "2026-10-16T14:58:50.578384809Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T15:00:50Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "tXOZgp1hZBoQBLg2ExMR404JIpUSCphWFAtV0z5IMezj-gm_Z2n9Z9Tv9L_qyef47CGjHyB64nReaVVez8FdDA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/source-record.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/source-record.json new file mode 100644 index 00000000..df55f685 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/actual-retained-python-source/source-record.json @@ -0,0 +1,251 @@ +{ + "id": "decision:d4c3d2d2f6827449", + "contract_version": "flex-auth.decision-record.v1", + "effect": "allow", + "reason": "exact_task_execution_matched", + "matched_policy_version": "v2", + "matched_rule": "exact_task_execution_matched", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "binding": { + "tenant": "tenant:platform", + "subject": { + "id": "secrets-engine", + "type": "service", + "tenant": "tenant:platform", + "attributes": { + "description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.", + "display_name": "secrets-engine service principal", + "groups": [ + "group:secrets-engine-lane-operators" + ], + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ] + } + }, + "action": "execute-ccr", + "resource": { + "id": "catalog:synthetic", + "type": "secret-catalog-lane", + "system": "secrets-engine", + "tenant": "tenant:platform", + "attributes": { + "stage": "prod" + } + }, + "context": { + "approval": { + "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", + "binding": { + "action": "execute-ccr", + "actor": "secrets-engine", + "digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "human_control": true, + "pdp_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "pdp_path": true, + "principal": "secrets-engine", + "purpose": "actual Python App source-only synthetic proof", + "target": { + "attributes": { + "stage": "prod" + }, + "id": "catalog:synthetic", + "system": "secrets-engine", + "tenant": "tenant:platform", + "type": "secret-catalog-lane" + } + }, + "consumed": false, + "freshness": { + "not_after": "2026-10-16T15:00:50+00:00", + "observed_at": "2026-10-16T14:58:50+00:00", + "ttl_seconds": 120 + }, + "issuer": "approval-engine", + "kind": "approval-claim", + "reason_code": "ok", + "schema_version": "0.1", + "state": "valid", + "valid_now": true, + "validity": { + "expires_at": "2026-10-16T15:01:50.071540+00:00", + "not_before": "2026-10-16T14:57:50.071540+00:00" + }, + "yields_to": "net-kingdom taxonomy request-claim schema (statute \u00a717; unassigned)" + }, + "catalog_target": { + "lane": "synthetic", + "stage": "prod" + }, + "execution_contract": { + "backend_budget": { + "ttl_seconds": 30, + "uses": 12 + }, + "backend_origin": "http://127.0.0.1:18200", + "catalog_sha256": "sha256:290e55b2258d66980f9443ba7d1436adfc77b8814bd161004d5af2ba272bc9cc", + "ccr_id": "CCR-2099-0001", + "ccr_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "cluster_uid": "synthetic-not-native", + "contract_version": "SECRETS-EXEC-20261011-v1", + "execution_actor": "service:secrets-engine", + "execution_grant_ccr_id": "CCR-2099-0002", + "expected_after": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected_before": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "grant_immutable_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "operation": "metadata-apply-ccr", + "operation_deadline": "2026-10-16T14:59:50+00:00", + "operation_id": "cbfbced0-09d5-48c8-a1e7-a2ea4dc69153", + "operations": [ + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "sys/policies/acl/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "PUT", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "method": "GET", + "path": "auth/synthetic/role/synthetic", + "payload_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + ], + "source_bundle": { + "engine_dispatcher": { + "path": "/synthetic/engine_dispatcher.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "platform_mediator": { + "path": "/synthetic/platform_mediator.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "policy_auth": { + "path": "/synthetic/policy_auth.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "typed_ccr": { + "path": "/synthetic/typed_ccr.py", + "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + }, + "human_control": true, + "purpose": "actual Python App source-only synthetic proof", + "service_actor_binding": { + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "version": "proposed-service-task-caller/v1" + } + }, + "request_digest": "sha256:fdce7858cbfb3b945694c27a808b3559ca0ad18ff397e88ace1b0a899f57db2a", + "approval_binding_digest": "sha256:cb2d195a1f974d8a769ca79c6aa8e2a5e6ee5528ad82a1232c418bd36fd8bf6e", + "submitted_request_digest": "sha256:55da69cfae4f20000177333d27d97c739e6ad55609f5fefe7f7b83503d7da4b5" + }, + "lifetime": { + "kind": "ttl", + "ttl": "59.4099177s", + "not_before": "2026-10-16T14:58:50.5900823Z", + "expires_at": "2026-10-16T14:59:50Z" + }, + "diagnostics": { + "action": "execute-ccr", + "matched_relationship": "", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_status": "proposed", + "registry_overrode": [], + "registry_resource": false, + "registry_subject": true + }, + "provenance": { + "evaluator": "flex-auth/local", + "mode": "standalone", + "policy_package": "secrets-engine.task-execution.service-pilot", + "policy_version": "v2", + "policy_package_digest": "sha256:de5683e5a34161b9349b10cd8e4abef704e0136e3b14b97719fcd7a79850fc4d", + "registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb", + "input_claim_digests": { + "context": "sha256:3ccefed2cbf9583b74cd5dd1ed92c345f3612f2bdac965e111aeab96da029c27" + }, + "decision_time": "2026-10-16T14:58:50Z", + "approval_observation": { + "source_id": "synthetic-actual-approval-app", + "source_revision": "actual-owner-python-source-fixture", + "configuration_digest": "sha256:fc4603a7a332ca886d7024b9bb16d76e82c467fc39048e2bcc7e0c8758a09f34", + "claim_digest": "sha256:51f0d25dd1dfaf28b6b411b1165976cfe028e7833ef4143d38fd5f35632d1b19", + "native_binding_digest": "sha256:19cfbe20e156cef1f4c0a37842c47666d2f5b2164d803d77a12346ed38a9444d", + "observed_at": "2026-10-16T14:58:50Z", + "not_after": "2026-10-16T15:00:50Z", + "validated_at": "2026-10-16T14:58:50.578384809Z" + }, + "caller": { + "mode": "enforce", + "principal": "system:serviceaccount:secrets-engine:secrets-engine", + "audience": "flex-auth", + "not_after": "2026-10-16T15:00:50Z" + } + }, + "signature": { + "mode": "signed", + "alg": "ed25519", + "kid": "synthetic-v2", + "value": "tXOZgp1hZBoQBLg2ExMR404JIpUSCphWFAtV0z5IMezj-gm_Z2n9Z9Tv9L_qyef47CGjHyB64nReaVVez8FdDA" + }, + "caring": { + "profile": "caring-0.4.0-rc2", + "conformance_findings": [ + { + "code": "CARING-DESCRIPTOR-MISSING", + "severity": "warning", + "message": "no CARING descriptor matched the request", + "fields": [ + "caring_context" + ] + } + ] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/initial-proof-assertion-failure.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/initial-proof-assertion-failure.json new file mode 100644 index 00000000..5c33a109 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/initial-proof-assertion-failure.json @@ -0,0 +1 @@ +{"outcome":"retained-registry Access ALLOW returned, proof assertion failed before recording passing evidence","reason":"inherited empty-registry harness incorrectly compared evaluated approval-free digest with locally computed original proposal digest","correction":"assert exact equality with authenticated producer preapproval receipt used at actual issue; retain separate original submitted digest assertion","native_effects":false} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/owner-preapproval-review.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/owner-preapproval-review.json new file mode 100644 index 00000000..c019ef3d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/owner-preapproval-review.json @@ -0,0 +1,36 @@ +{ + "owner": "Access Engine", + "status": "source proposal for independent review", + "base": "B2/B3 R2; prior failures/R1/R2 evidence retained", + "production_change": "existing claim-free typed v2 check emits explicit evaluated approval-free digest in signed DENY normalization receipt; validate fixed native act even before issue", + "contract": "docs/request-enrichment.md already requires issuer records producer output, never consumer recomputation of evaluated digest", + "preserved": [ + "retained registry bytes and authoritative enrichment", + "original claim-free proposal plus only original issued claim at final submit", + "same policy/evaluator and exact native/full/submitted/approval-free joins", + "fresh original/source validation without rewriting claims", + "R2 exact temporal bounds and precise notbefore", + "signature/API modules unchanged", + "DENY cannot satisfy execution authorization" + ], + "proof": [ + "actual retained registry HTTP and production serve signed DENY -> normalized native/PDP issue -> signed ALLOW", + "actual Python App TLS create/human/read + retained registry production serve proof; beforeissue existing producer signature verifier", + "registry drift with same original/fresh claims refuses", + "source/staleness/numeric-kind negative cases", + "exact response/record parity and producer signature verification" + ], + "secrets_owner_reported_full_strict_proof": { + "path": "/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/decision-trusted-successor/actual-retained-r4/receipt.json", + "sha256": "ef95d15bfe0a113ea0791320face5c2448b681c1b9a205837f31ce57102adf90", + "scope": "owner-reported actual builder/preapproval/Approval/human/ALLOW/strictCAS/GET; precision not accepted because natural latency crossed second" + }, + "precision": "unresolved Approval seconds truncation; Access does not backdate or relax start; active Approval owner preparing exact timestamp repair", + "remaining": [ + "independent coordinator/source review", + "Approval precise timestamp repair and final fast-CAS successor rerun", + "Platform full real backend proof with absolute per-call/cleanup budgets", + "separately admitted native source/trust/custody/image/rollout" + ], + "native_admission": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/r2-to-r3-preapproval.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/r2-to-r3-preapproval.patch new file mode 100644 index 00000000..12d88241 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/r2-to-r3-preapproval.patch @@ -0,0 +1,269 @@ +diff --git a/cmd/access-engine/trusted_service_http_test.go b/cmd/access-engine/trusted_service_http_test.go +--- a/cmd/access-engine/trusted_service_http_test.go ++++ b/cmd/access-engine/trusted_service_http_test.go +@@ -78,7 +78,11 @@ + r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("120"), "not_after": now.Add(119 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} + return r + } +-func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { ++func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { testTrustedServiceHTTP(t, false) } ++func TestTrustedServiceRetainedRegistryPreapprovalAndRefusals(t *testing.T) { ++ testTrustedServiceHTTP(t, true) ++} ++func testTrustedServiceHTTP(t *testing.T, retained bool) { + now := time.Now().UTC() + folder := t.TempDir() + reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +@@ -94,6 +98,9 @@ + os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) + os.WriteFile(token, []byte("synthetic-reviewer"), 0600) + original := trustedRequest(t, now) ++ if retained { ++ delete(original.Context, "approval") ++ } + claimRaw, _ := json.Marshal(original.Context["approval"]) + mode := "ok" + reads := 0 +@@ -142,7 +149,14 @@ + if err != nil { + t.Fatal(err) + } +- engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ store := registry.NewStore() ++ if retained { ++ store, err = registry.LoadFilePreservingNumbers(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ } ++ engine, err := decision.NewEngine(store, pkg) + if err != nil { + t.Fatal(err) + } +@@ -169,6 +183,49 @@ + body, _ := io.ReadAll(response.Body) + return response.StatusCode, body + } ++ var pre api.DecisionEnvelope ++ if retained { ++ preStatus, preRaw := post(original) ++ if preStatus != 200 { ++ t.Fatalf("preapproval status%d", preStatus) ++ } ++ pre, err = sign.DecodeEnvelope(preRaw) ++ if err != nil || sign.Verify(pre, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("preapproval not authenticated") ++ } ++ if pre.Effect != api.DecisionEffectDeny || pre.Lifetime != nil || pre.Binding.ApprovalBindingDigest == "" || pre.Binding.ApprovalBindingDigest != pre.Binding.RequestDigest { ++ t.Fatal("preapproval receipt is execution authority or lacks exactclaimfree digest") ++ } ++ if pre.Diagnostics["registry_subject"] != true || pre.Provenance.RegistrySnapshotDigest != store.Digest() { ++ t.Fatal("retained registry not applied") ++ } ++ rawOriginal, _ := json.Marshal(original) ++ var legacyPre api.CheckRequest ++ json.Unmarshal(rawOriginal, &legacyPre) ++ if pre.Binding.SubmittedRequestDigest != api.RequestDigest(legacyPre) || pre.Binding.RequestDigest == api.RequestDigest(legacyPre) { ++ t.Fatal("original/evaluated retained registry distinction lost") ++ } ++ carried := trustedRequest(t, now).Context["approval"].(map[string]any) ++ native := map[string]any{"action": pre.Binding.Action, "actor": pre.Binding.Subject.ID, "principal": "secrets-engine", "purpose": pre.Binding.Context["purpose"], "target": pre.Binding.Resource} ++ nativeRaw, _ := json.Marshal(native) ++ _, nativeDigest, err := approvalfact.CanonicalBinding(nativeRaw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ native["digest"] = nativeDigest ++ native["pdp_digest"] = pre.Binding.ApprovalBindingDigest ++ native["pdp_path"] = true ++ native["human_control"] = true ++ carried["binding"] = native ++ original.Context["approval"] = carried ++ claimRaw, _ = json.Marshal(carried) ++ originalBytes, _ := json.Marshal(original) ++ original, err = taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(originalBytes)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ ++ } + status, body := post(original) + if status != 200 { + t.Fatalf("positive status %d: %s", status, body) +@@ -187,9 +244,17 @@ + legacyRaw, _ := json.Marshal(original) + var legacyRequest api.CheckRequest + json.Unmarshal(legacyRaw, &legacyRequest) +- if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) { +- t.Fatal("published original/full/approval-free digest profile changed") +- } ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) { ++ t.Fatal("original submitted digest changed") ++ } ++ if retained { ++ if envelope.Binding.RequestDigest == api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != pre.Binding.ApprovalBindingDigest { ++ t.Fatal("normalized approval correspondence changed") ++ } ++ } else if envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) { ++ t.Fatal("published digest profile changed") ++ } ++ + if !reflect.DeepEqual(envelope.Binding.Context, original.Context) { + t.Fatal("original context changed") + } +@@ -203,12 +268,16 @@ + if err != nil { + t.Fatal(err) + } +- recordEnvelope, err := sign.DecodeEnvelope(bytes.TrimSpace(recorded)) +- if err != nil { +- t.Fatal(err) +- } +- if !reflect.DeepEqual(recordEnvelope, envelope) { +- t.Fatal("returned/recorded mismatch") ++ records, err := audit.NewJSONLDecisionLog(record).ReadAll() ++ expectedCount := 1 ++ if retained { ++ expectedCount = 2 ++ } ++ if err != nil || len(records) != expectedCount || !reflect.DeepEqual(records[len(records)-1], envelope) { ++ t.Fatal("returned record parity") ++ } ++ if retained && !reflect.DeepEqual(records[0], pre) { ++ t.Fatal("preapproval record parity") + } + for _, failure := range []string{"redirect", "duplicate", "changed", "expired", "short-fresh"} { + t.Run(failure, func(t *testing.T) { +@@ -220,6 +289,20 @@ + }) + } + mode = "ok" ++ if retained { ++ t.Run("registry-drift-cannot-rebind-approved-act", func(t *testing.T) { ++ subject, _ := store.Subject("secrets-engine") ++ originalSubject := subject ++ subject.Roles = append(append([]api.CanonicalRole{}, subject.Roles...), api.CanonicalRole("synthetic-owner-drift")) ++ if err := store.PutSubject(subject); err != nil { ++ t.Fatal(err) ++ } ++ defer store.PutSubject(originalSubject) ++ if status, _ := post(original); status == 200 { ++ t.Fatal("changed registry silently rebound original/fresh claim") ++ } ++ }) ++ } + t.Run("original-stale", func(t *testing.T) { + raw, _ := json.Marshal(original) + r, err := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) +@@ -254,7 +337,14 @@ + sourceJSON, _ := json.Marshal(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) + os.WriteFile(sourcePath, sourceJSON, 0600) + registryPath := filepath.Join(folder, "registry.json") +- os.WriteFile(registryPath, []byte(`{}`), 0600) ++ registryBytes := []byte(`{}`) ++ if retained { ++ registryBytes, err = os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ } ++ os.WriteFile(registryPath, registryBytes, 0600) + policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) + cliRecord := filepath.Join(folder, "cli-record.jsonl") + listener, err := net.Listen("tcp", "127.0.0.1:0") +diff --git a/docs/trusted-service-task-source.md b/docs/trusted-service-task-source.md +--- a/docs/trusted-service-task-source.md ++++ b/docs/trusted-service-task-source.md +@@ -72,3 +72,37 @@ + separate owner-admitted runtime operation. The earlier Platform positive proof + remains evidence only at its old ec21518e Access pin; its generic synthetic policy + is deliberately incompatible with the successor guarded source. ++ ++## Producer normalization before Approval issue ++ ++The retained registry enriches Secrets subject attributes. Consequently the ++submitted approval-free digest is not the evaluated approval-free digest. This ++is the existing `docs/request-enrichment.md` contract, not a reason to remove ++registry facts or copy its enrichment rules into Secrets. ++ ++The existing `/v1/check` typed v2 path accepts the exact original claim-free ++proposal and returns a signed DENY containing the authoritative normalized ++binding and original `submitted_request_digest`. For this v2 path it also emits ++`approval_binding_digest` when claim-free, equal to evaluated `request_digest`. ++That is a normalization receipt supplying facts only. It has no ALLOW lifetime ++and cannot satisfy execution authorization. Before issue, Secrets authenticates ++this receipt using its existing independent producer trust/policy/source pins, ++checks original request/context correspondence, caller/deadline/current-time ++bounds and normalized self-digests, then derives the native act and issue digest ++from the authenticated producer binding. It does not supply expected registry ++facts or expected digests to the producer. The Approval API records these exact ++facts at issue; human approval follows its unchanged authenticated contract. ++ ++The submitted proposal remains unchanged; the final request attaches only the ++original issued claim. Final ALLOW rechecks independent source facts, native/PDP ++correspondence, normalization and all temporal/caller conditions. A changed ++registry cannot silently refresh or replace an approved act: a changed evaluated ++digest/native target refuses even when the freshly fetched claim is still valid. ++The source tests cover the retained registry, signed DENY/ALLOW separation, ++original/evaluated digest distinction, unchanged context, source/registry pins, ++registry drift, stale/refreshed claims and exact response/record parity. Earlier ++empty-registry proofs remain explicitly scoped historical source fixtures. ++ ++R3 retains precise decision `not_before`; it does not floor that lower bound to ++mask Approval's current whole-second consumption receipt defect. Approval-owner ++precision repair and full owner integration remain separate reviewed evidence. +diff --git a/internal/decision/trusted_service.go b/internal/decision/trusted_service.go +--- a/internal/decision/trusted_service.go ++++ b/internal/decision/trusted_service.go +@@ -69,6 +69,10 @@ + if err != nil { + return api.DecisionEnvelope{}, err + } ++ native, err := nativeServiceAct(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } + ctx = callerauth.WithRecord(ctx, input.Caller()) + var approvalEvidence *api.ApprovalObservationProvenance + deadline := input.Caller().NotAfter +@@ -89,10 +93,6 @@ + id, ok := claim["approval_id"].(string) + if !ok { + return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource +- } +- native, err := nativeServiceAct(normalized) +- if err != nil { +- return api.DecisionEnvelope{}, err + } + originalRaw, err := json.Marshal(carried) + if err != nil { +@@ -138,9 +138,9 @@ + envelope := e.envelope(ctx, normalized, submitted, expectation, facts) + envelope.Binding.RequestDigest = api.RequestDigest(evaluated) + envelope.Binding.SubmittedRequestDigest = api.RequestDigest(original) +- if hasClaim { +- envelope.Binding.ApprovalBindingDigest = api.ApprovalBindingDigest(evaluated) +- } ++ // Claim-free signed DENY is the producer-owned normalization receipt used ++ // before Approval issue. It supplies facts only, never execution authority. ++ envelope.Binding.ApprovalBindingDigest = api.ApprovalBindingDigest(evaluated) + envelope.Provenance.ApprovalObservation = approvalEvidence + if envelope.Effect == api.DecisionEffectAllow { + if approvalEvidence == nil || envelope.Lifetime == nil { +diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -260,3 +260,13 @@ + The fixture declares backendTTL30/deadline60 and explicit source freshness120; + owner runtime source freshness configuration and full native integration remain + separate reviewed gates. See docs/trusted-service-task-source.md. ++ ++2026-10-16 B2/B3 R3 follows actual retained-registry integration failure: subject ++enrichment changed evaluated approval-free digest from the original proposal. ++The existing signed claim-free DENY now explicitly carries v2 approvalfree digest ++(equal evaluated requestdigest), enabling producer-owned preapproval facts under ++the already published enrichment contract. The final original proposal adds only ++the issued claim; registry drift/refreshed facts cannot silently rebind it. ++Retained-registry HTTP and production serve proofs and negative joins pass. ++R3 preserves precise notbefore pending separate Approval-owner timestamp fidelity ++repair; no rounding waiver, copied registry algorithm or empty-registry workaround. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/trusted-source-r3-over-b1-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/trusted-source-r3-over-b1-r2.patch new file mode 100644 index 00000000..c1165ef4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/r3/trusted-source-r3-over-b1-r2.patch @@ -0,0 +1,2592 @@ +diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml +--- a/.forgejo/workflows/image.yaml ++++ b/.forgejo/workflows/image.yaml +@@ -19,6 +19,7 @@ + - "cmd/**" + - "internal/**" + - "pkg/**" ++ - "third_party/**" + - "examples/**" + - "go.mod" + - "go.sum" +diff --git a/Containerfile b/Containerfile +--- a/Containerfile ++++ b/Containerfile +@@ -1,6 +1,8 @@ + FROM golang:1.26.7-alpine@sha256:28d89ee9cc0ff9fec75c82ca201e6bf7fdf9a679d4b7b24dfa04f2bb766bb468 AS build + WORKDIR /src + COPY go.mod go.sum ./ ++# Portable, unchanged Approval-owner pure data helper; available before download. ++COPY third_party/approvalfact ./third_party/approvalfact + RUN go mod download + COPY . . + RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/access-engine ./cmd/access-engine && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/flex-auth ./cmd/flex-auth +diff --git a/cmd/access-engine/main.go b/cmd/access-engine/main.go +--- a/cmd/access-engine/main.go ++++ b/cmd/access-engine/main.go +@@ -333,6 +333,7 @@ + logPath := fs.String("log", "", "optional JSONL decision log path") + signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file") + signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id") ++ approvalSourceFile := fs.String("approval-source-file", "", "owner fixed Approval HTTPS reader configuration file") + outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log") + emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine") + auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats") +@@ -402,7 +403,19 @@ + if err != nil { + return fail(stderr, err) + } +- mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) ++ var approvalReader *taskexecution.ApprovalReader ++ if engine.IsTrustedServicePilot() { ++ if *signingKeyFile == "" || *approvalSourceFile == "" { ++ return fail(stderr, fmt.Errorf("trusted task source requires signing and fixed Approval reader configuration")) ++ } ++ approvalReader, err = taskexecution.LoadApprovalReader(*approvalSourceFile) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ } else if *approvalSourceFile != "" { ++ return fail(stderr, fmt.Errorf("Approval reader requires exact trusted task source package")) ++ } ++ mux := newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, approvalReader) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -457,6 +470,9 @@ + } + + func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { ++ return newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, nil) ++} ++func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs, approvalReader *taskexecution.ApprovalReader) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -467,12 +483,16 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + if serviceInputs == nil { + http.Error(w, "service input unavailable", http.StatusServiceUnavailable) + return + } +- request, err := taskexecution.DecodeServiceRequest(r.Body) ++ decode := taskexecution.DecodeServiceRequest ++ if engine.IsTrustedServicePilot() { ++ decode = taskexecution.DecodeTrustedServiceRequest ++ } ++ request, err := decode(r.Body) + if err != nil || request.Purpose != "" { + http.Error(w, "invalid service task request", http.StatusBadRequest) + return +@@ -482,7 +502,12 @@ + http.Error(w, "service task input refused", http.StatusForbidden) + return + } +- decision, err := engine.CheckServicePilot(r.Context(), input) ++ var decision api.DecisionEnvelope ++ if engine.IsTrustedServicePilot() { ++ decision, err = engine.CheckTrustedService(r.Context(), input, approvalReader) ++ } else { ++ decision, err = engine.CheckServicePilot(r.Context(), input) ++ } + writeHTTP(w, decision, err) + return + } +@@ -506,7 +531,7 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + http.Error(w, "service task batch unavailable", http.StatusForbidden) + return + } +@@ -537,7 +562,7 @@ + // Only the existing exact service pilot package selects this source path. + // This is not an eligibility toggle; successful preparation still only DENYs. + func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) { +- if !engine.IsServicePilot() { ++ if !engine.IsTaskPin() { + return nil, nil + } + if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { +@@ -546,6 +571,9 @@ + reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) + if err != nil { + return nil, err ++ } ++ if engine.IsTrustedServicePilot() { ++ return taskexecution.NewTrustedServiceInputs(reviewer) + } + return taskexecution.NewServicePilotInputs(reviewer) + } +@@ -575,11 +603,15 @@ + } + + func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) { +- store, err := registry.LoadFile(registryPath) ++ pkg, err := policy.LoadAndValidateFile(ctx, policyPath) + if err != nil { + return nil, err + } +- pkg, err := policy.LoadAndValidateFile(ctx, policyPath) ++ load := registry.LoadFile ++ if pkg.Metadata.ID == taskexecution.ServicePilotPackageID && pkg.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion { ++ load = registry.LoadFilePreservingNumbers ++ } ++ store, err := load(registryPath) + if err != nil { + return nil, err + } +diff --git a/cmd/access-engine/trusted_service_http_test.go b/cmd/access-engine/trusted_service_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/trusted_service_http_test.go +@@ -0,0 +1,430 @@ ++package main ++ ++import ( ++ "bytes" ++ "context" ++ "crypto/ed25519" ++ "encoding/base64" ++ "encoding/json" ++ "encoding/pem" ++ "io" ++ "net" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "os/exec" ++ "path/filepath" ++ "reflect" ++ "strings" ++ "testing" ++ "time" ++ ++ "github.com/netkingdom/access-engine/internal/audit" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/decision" ++ "github.com/netkingdom/access-engine/internal/policy" ++ "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++func trustedRequest(t *testing.T, now time.Time) api.CheckRequest { ++ t.Helper() ++ r := pilotRequest(now) ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(60 * time.Second).UTC().Truncate(time.Second).Format(time.RFC3339) ++ r.Subject.Tenant = r.Tenant ++ r.Resource.Tenant = r.Tenant ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1.0") ++ c := r.Context["execution_contract"].(map[string]any) ++ c["operation_id"] = "11111111-2222-4333-8444-555555555555" ++ c["ccr_id"] = "synthetic-change" ++ c["execution_grant_ccr_id"] = "synthetic-grant" ++ hash := "sha256:" + strings.Repeat("1", 64) ++ for _, key := range []string{"ccr_immutable_sha256", "grant_immutable_sha256", "catalog_sha256", "expected_before", "expected_after"} { ++ c[key] = hash ++ } ++ c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("30"), "uses": json.Number("12")} ++ var ops []any ++ for i, method := range []string{"GET", "PUT", "GET", "GET", "PUT", "GET"} { ++ path := "sys/policies/acl/synthetic" ++ if i >= 3 { ++ path = "auth/synthetic/role/synthetic" ++ } ++ ops = append(ops, map[string]any{"method": method, "path": path, "payload_sha256": hash}) ++ } ++ c["operations"] = ops ++ delete(r.Context, "approval") ++ raw, _ := json.Marshal(map[string]any{"action": r.Action, "actor": r.Subject.ID, "principal": "secrets-engine", "purpose": "synthetic source-only proof", "target": r.Resource}) ++ r.Context["purpose"] = "synthetic source-only proof" ++ _, digest, err := approvalfact.CanonicalBinding(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ bindingValue, err := approvalfact.DecodeJSON(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ binding := bindingValue.(map[string]any) ++ // PDP profile intentionally retains the published legacy numeric projection. ++ encoded, _ := json.Marshal(r) ++ var legacy api.CheckRequest ++ json.Unmarshal(encoded, &legacy) ++ binding["digest"] = digest ++ binding["pdp_digest"] = api.ApprovalBindingDigest(legacy) ++ binding["pdp_path"] = true ++ binding["human_control"] = true ++ r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("120"), "not_after": now.Add(119 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} ++ return r ++} ++func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { testTrustedServiceHTTP(t, false) } ++func TestTrustedServiceRetainedRegistryPreapprovalAndRefusals(t *testing.T) { ++ testTrustedServiceHTTP(t, true) ++} ++func testTrustedServiceHTTP(t *testing.T, retained bool) { ++ now := time.Now().UTC() ++ folder := t.TempDir() ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ w.WriteHeader(403) ++ return ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": true, "audiences": []string{"flex-auth"}, "user": map[string]any{"username": taskexecution.ServicePilotPrincipal}}}) ++ })) ++ defer reviewer.Close() ++ ca := filepath.Join(folder, "review-ca") ++ token := filepath.Join(folder, "review-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(token, []byte("synthetic-reviewer"), 0600) ++ original := trustedRequest(t, now) ++ if retained { ++ delete(original.Context, "approval") ++ } ++ claimRaw, _ := json.Marshal(original.Context["approval"]) ++ mode := "ok" ++ reads := 0 ++ provider := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reads++ ++ if r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Header.Get("Authorization") != "Bearer synthetic-approval-reader" { ++ w.WriteHeader(403) ++ return ++ } ++ if mode == "redirect" { ++ http.Redirect(w, r, reviewer.URL, 302) ++ return ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode { ++ case "duplicate": ++ io.WriteString(w, `{"state":"valid","state":"invalid"}`) ++ case "changed": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["consumed"] = true ++ json.NewEncoder(w).Encode(v) ++ case "short-fresh": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ fresh := v.(map[string]any)["freshness"].(map[string]any) ++ fresh["ttl_seconds"] = json.Number("20") ++ fresh["not_after"] = now.Add(19 * time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) ++ case "expired": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["freshness"].(map[string]any)["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) ++ default: ++ w.Write(claimRaw) ++ } ++ })) ++ defer provider.Close() ++ pca := filepath.Join(folder, "approval-ca") ++ ptoken := filepath.Join(folder, "approval-token") ++ os.WriteFile(pca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: provider.Certificate().Raw}), 0600) ++ os.WriteFile(ptoken, []byte("synthetic-approval-reader"), 0440) ++ source, err := taskexecution.NewApprovalReader(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ store := registry.NewStore() ++ if retained { ++ store, err = registry.LoadFilePreservingNumbers(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ } ++ engine, err := decision.NewEngine(store, pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{19}, 32)) ++ engine.SetEnvelopeSigningKey(key, "synthetic-v2") ++ record := filepath.Join(folder, "record.jsonl") ++ engine.SetDecisionLog(audit.NewJSONLDecisionLog(record)) ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, token, ca, map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithTaskFacts(engine, callerauth.Disabled(), inputs, source)) ++ defer server.Close() ++ post := func(request api.CheckRequest) (int, []byte) { ++ t.Helper() ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ body, _ := io.ReadAll(response.Body) ++ return response.StatusCode, body ++ } ++ var pre api.DecisionEnvelope ++ if retained { ++ preStatus, preRaw := post(original) ++ if preStatus != 200 { ++ t.Fatalf("preapproval status%d", preStatus) ++ } ++ pre, err = sign.DecodeEnvelope(preRaw) ++ if err != nil || sign.Verify(pre, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("preapproval not authenticated") ++ } ++ if pre.Effect != api.DecisionEffectDeny || pre.Lifetime != nil || pre.Binding.ApprovalBindingDigest == "" || pre.Binding.ApprovalBindingDigest != pre.Binding.RequestDigest { ++ t.Fatal("preapproval receipt is execution authority or lacks exactclaimfree digest") ++ } ++ if pre.Diagnostics["registry_subject"] != true || pre.Provenance.RegistrySnapshotDigest != store.Digest() { ++ t.Fatal("retained registry not applied") ++ } ++ rawOriginal, _ := json.Marshal(original) ++ var legacyPre api.CheckRequest ++ json.Unmarshal(rawOriginal, &legacyPre) ++ if pre.Binding.SubmittedRequestDigest != api.RequestDigest(legacyPre) || pre.Binding.RequestDigest == api.RequestDigest(legacyPre) { ++ t.Fatal("original/evaluated retained registry distinction lost") ++ } ++ carried := trustedRequest(t, now).Context["approval"].(map[string]any) ++ native := map[string]any{"action": pre.Binding.Action, "actor": pre.Binding.Subject.ID, "principal": "secrets-engine", "purpose": pre.Binding.Context["purpose"], "target": pre.Binding.Resource} ++ nativeRaw, _ := json.Marshal(native) ++ _, nativeDigest, err := approvalfact.CanonicalBinding(nativeRaw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ native["digest"] = nativeDigest ++ native["pdp_digest"] = pre.Binding.ApprovalBindingDigest ++ native["pdp_path"] = true ++ native["human_control"] = true ++ carried["binding"] = native ++ original.Context["approval"] = carried ++ claimRaw, _ = json.Marshal(carried) ++ originalBytes, _ := json.Marshal(original) ++ original, err = taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(originalBytes)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ ++ } ++ status, body := post(original) ++ if status != 200 { ++ t.Fatalf("positive status %d: %s", status, body) ++ } ++ envelope, err := sign.DecodeEnvelope(body) ++ if err != nil { ++ t.Fatal(err) ++ } ++ t.Logf("actual HTTP source policy pin: %s %s %s", pkg.Metadata.ID, pkg.Metadata.Version, pkg.Digest()) ++ if envelope.Effect != api.DecisionEffectAllow || sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("not signed ALLOW: %s", body) ++ } ++ if envelope.Provenance.ApprovalObservation == nil || envelope.Provenance.ApprovalObservation.SourceID != "synthetic-approval" || reads != 1 { ++ t.Fatal("missing authenticated observation provenance") ++ } ++ legacyRaw, _ := json.Marshal(original) ++ var legacyRequest api.CheckRequest ++ json.Unmarshal(legacyRaw, &legacyRequest) ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) { ++ t.Fatal("original submitted digest changed") ++ } ++ if retained { ++ if envelope.Binding.RequestDigest == api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != pre.Binding.ApprovalBindingDigest { ++ t.Fatal("normalized approval correspondence changed") ++ } ++ } else if envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) { ++ t.Fatal("published digest profile changed") ++ } ++ ++ if !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original context changed") ++ } ++ if envelope.Lifetime.ExpiresAt != original.Context["execution_contract"].(map[string]any)["operation_deadline"] { ++ t.Fatal("decision does not bound entire exact operation") ++ } ++ if envelope.Binding.Resource.Attributes["numeric_fixture"] != json.Number("1.0") { ++ t.Fatal("native numeric kind lost") ++ } ++ recorded, err := os.ReadFile(record) ++ if err != nil { ++ t.Fatal(err) ++ } ++ records, err := audit.NewJSONLDecisionLog(record).ReadAll() ++ expectedCount := 1 ++ if retained { ++ expectedCount = 2 ++ } ++ if err != nil || len(records) != expectedCount || !reflect.DeepEqual(records[len(records)-1], envelope) { ++ t.Fatal("returned record parity") ++ } ++ if retained && !reflect.DeepEqual(records[0], pre) { ++ t.Fatal("preapproval record parity") ++ } ++ for _, failure := range []string{"redirect", "duplicate", "changed", "expired", "short-fresh"} { ++ t.Run(failure, func(t *testing.T) { ++ mode = failure ++ status, _ := post(original) ++ if status == 200 { ++ t.Fatal("bad source admitted") ++ } ++ }) ++ } ++ mode = "ok" ++ if retained { ++ t.Run("registry-drift-cannot-rebind-approved-act", func(t *testing.T) { ++ subject, _ := store.Subject("secrets-engine") ++ originalSubject := subject ++ subject.Roles = append(append([]api.CanonicalRole{}, subject.Roles...), api.CanonicalRole("synthetic-owner-drift")) ++ if err := store.PutSubject(subject); err != nil { ++ t.Fatal(err) ++ } ++ defer store.PutSubject(originalSubject) ++ if status, _ := post(original); status == 200 { ++ t.Fatal("changed registry silently rebound original/fresh claim") ++ } ++ }) ++ } ++ t.Run("original-stale", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, err := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ fresh := r.Context["approval"].(map[string]any)["freshness"].(map[string]any) ++ fresh["observed_at"] = now.Add(-121 * time.Second).Format(time.RFC3339Nano) ++ fresh["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("fresh provider rescued stale original") ++ } ++ }) ++ t.Run("native-number-kind", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, _ := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1") ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("changed native integer/float authority accepted") ++ } ++ }) ++ after, _ := os.ReadFile(record) ++ if !bytes.Equal(after, recorded) { ++ t.Fatal("refused calls recorded executable envelopes") ++ } ++ // Exercise actual production CLI configuration with no ambient credentials. ++ seedPath := filepath.Join(folder, "cli-seed") ++ os.WriteFile(seedPath, []byte(base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{19}, 32))), 0440) ++ sourcePath := filepath.Join(folder, "approval-source.json") ++ sourceJSON, _ := json.Marshal(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ os.WriteFile(sourcePath, sourceJSON, 0600) ++ registryPath := filepath.Join(folder, "registry.json") ++ registryBytes := []byte(`{}`) ++ if retained { ++ registryBytes, err = os.ReadFile(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ } ++ os.WriteFile(registryPath, registryBytes, 0600) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-record.jsonl") ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ args := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", token, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedPath, "--envelope-signing-kid", "synthetic-v2", "--approval-source-file", sourcePath} ++ argsJSON, _ := json.Marshal(args) ++ child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$") ++ child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)} ++ child.Stdout = io.Discard ++ child.Stderr = io.Discard ++ if err := child.Start(); err != nil { ++ t.Fatal(err) ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ response, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ response.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("trusted CLI not ready") ++ } ++ raw, _ := json.Marshal(original) ++ req, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ wire, _ := io.ReadAll(response.Body) ++ cliEnvelope, err := sign.DecodeEnvelope(wire) ++ if response.StatusCode != 200 || err != nil || cliEnvelope.Effect != api.DecisionEffectAllow || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("CLI did not return signed trusted ALLOW: %s", wire) ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("CLI response/readback record mismatch") ++ } ++ if !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("CLI changed original numeric context") ++ } ++ ++} ++ ++func TestTrustedServiceMissingAndGenericAdmissionRefuse(t *testing.T) { ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ request := trustedRequest(t, time.Now().UTC()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic list bypass") ++ } ++ if _, err := engine.CheckTrustedService(context.Background(), taskexecution.ServiceInput{}, nil); err == nil { ++ t.Fatal("missing typed input and fixed source accepted") ++ } ++ recorder := httptest.NewRecorder() ++ raw, _ := json.Marshal(request) ++ newServeMux(engine).ServeHTTP(recorder, httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw))) ++ if recorder.Code != 503 { ++ t.Fatal("unwired typed path accepted") ++ } ++} +diff --git a/docs/trusted-service-task-source.md b/docs/trusted-service-task-source.md +new file mode 100644 +--- /dev/null ++++ b/docs/trusted-service-task-source.md +@@ -0,0 +1,108 @@ ++# Trusted service task source proposal (v2) ++ ++The separate `secrets-engine.task-execution.service-pilot` v2 source package can ++return ALLOW only through the existing `/v1/check` typed task path. The original ++v1 package remains unchanged and DENY-only. No chart, runtime, source identity, ++credential custody or native execution admission is enabled by these sources. ++ ++The server requires enforced TokenReview, audience `flex-auth`, the exact retained ++Secrets service-account mapping, mounted envelope signing configuration and an ++owner-installed `--approval-source-file`. This public configuration contains ++`source_id`, `source_revision`, fixed HTTPS `origin`, `store_tenant` exactly ++`tenant:platform`, absolute `ca_file` and absolute mounted `token_file`. The ++credential must independently carry existing Approval `approval:read` authority ++for that store. Source labels describe the owner-admitted configuration; they do ++not perform its admission. No key or token value belongs in the configuration. ++ ++The reader sends one authenticated GET to the fixed ++`/v1/approvals/{UUID}/claim` endpoint, accepts only bounded strict JSON, uses only ++the configured CA, refuses redirects and environmental proxies, and has a total ++three-second HTTP timeout. It neither creates nor approves nor consumes. Mounted ++bearer files must be regular, bounded and have no group write or other access; ++0440 is supported. Public configuration/CA files also refuse group/other writes. ++Errors do not include response bodies, credentials or transport diagnostics. ++ ++The unchanged Approval-owner pure data helper is copied with exact source pins ++under `third_party/approvalfact`. The fixed reader establishes provider origin; ++the helper establishes schema and semantic correspondence. Both original and ++fresh claims must independently be currently valid, unconsumed, human-controlled ++PDP-path claims for the exact UUID, native five-field act and Access approval-free ++request digest. Nonfreshness authority must match exactly. The freshly fetched ++claim never replaces the submitted claim/context or renews original freshness. ++ ++The native act derives from the normalized subject/resource, fixed service ++principal, action and nonempty `context.purpose`. The HTTP decoder and task registry ++loader preserve raw JSON numeric representations. Registry facts loaded through ++the old lossy profile cannot enter the trusted path. Only the Approval-owner ++helper canonicalizes native act data. Existing PDP full/submitted/approval-free ++digests retain their published numeric profile through a digest-only legacy ++projection; the signed structured binding retains the original numeric kinds. ++Inputs unsupported by that existing PDP profile refuse; this proposal does not ++weaken any consumer numeric admission or invent numeric restrictions. ++ ++The same existing policy evaluator decides after those joins. Generic check, ++batch and list entry points refuse task execution across all policy names. No ++request-selected verified flag, provider origin or policy enable switch bypasses ++the typed path. Missing approval can receive signed DENY for preapproval binding; ++ALLOW requires authenticated matching original/fresh claims. Its expiry is exactly the immutable operation deadline, after validating that ++it fits the source-selected 120s hard maximum and every caller/original/fresh ++freshness/approval bound. At least the declared backend TTL plus the existing ++5s cleanup reserve must still fit after source I/O and final evaluation. The ++source maximum conservatively matches the existing 120s task-token cap; the ++structural backend budget maximum is not a promise every budget can fit. In ++particular backend TTL120 cannot fit this authority ceiling plus reserve. A ++fractional operation deadline is retained exactly rather than rounded earlier. ++Approval's default 30s observation freshness cannot cover the minimum30+5 ++operation budget. A separate owner-admitted source configuration must explicitly ++supply adequate original/fresh observation lifetime; fetching anew cannot renew ++an original claim. The disposable reviewed proof uses backendTTL30, immutable ++operationdeadline+60s and explicitly configured Approval freshness120. This is ++an entire-operation authority interval, not a decision-to-start window. Platform ++must retain native-entry and per-call deadline/cleanup gates; delays simply refuse. Existing source-owned signing covers the ++original binding and new `provenance.approval_observation` receipt. The receipt ++contains source/configuration identity, exact fetched-byte digest, native digest ++and observation/validation/deadline timestamps. JSONL readback and outbox replay ++preserve producer numeric signatures; no second signing algorithm is introduced. ++ ++Packaging must copy the local owner helper before `go mod download`. Existing ++ordinary image CI triggers include `third_party/**` so a helper update cannot ++silently leave the old image. No build publication is invoked by this preparation. ++Rollback selects the unchanged v1 package or removes the v2 admission; generic ++fallback remains refused. Rotating/withdrawing source or signing trust is a ++separate owner-admitted runtime operation. The earlier Platform positive proof ++remains evidence only at its old ec21518e Access pin; its generic synthetic policy ++is deliberately incompatible with the successor guarded source. ++ ++## Producer normalization before Approval issue ++ ++The retained registry enriches Secrets subject attributes. Consequently the ++submitted approval-free digest is not the evaluated approval-free digest. This ++is the existing `docs/request-enrichment.md` contract, not a reason to remove ++registry facts or copy its enrichment rules into Secrets. ++ ++The existing `/v1/check` typed v2 path accepts the exact original claim-free ++proposal and returns a signed DENY containing the authoritative normalized ++binding and original `submitted_request_digest`. For this v2 path it also emits ++`approval_binding_digest` when claim-free, equal to evaluated `request_digest`. ++That is a normalization receipt supplying facts only. It has no ALLOW lifetime ++and cannot satisfy execution authorization. Before issue, Secrets authenticates ++this receipt using its existing independent producer trust/policy/source pins, ++checks original request/context correspondence, caller/deadline/current-time ++bounds and normalized self-digests, then derives the native act and issue digest ++from the authenticated producer binding. It does not supply expected registry ++facts or expected digests to the producer. The Approval API records these exact ++facts at issue; human approval follows its unchanged authenticated contract. ++ ++The submitted proposal remains unchanged; the final request attaches only the ++original issued claim. Final ALLOW rechecks independent source facts, native/PDP ++correspondence, normalization and all temporal/caller conditions. A changed ++registry cannot silently refresh or replace an approved act: a changed evaluated ++digest/native target refuses even when the freshly fetched claim is still valid. ++The source tests cover the retained registry, signed DENY/ALLOW separation, ++original/evaluated digest distinction, unchanged context, source/registry pins, ++registry drift, stale/refreshed claims and exact response/record parity. Earlier ++empty-registry proofs remain explicitly scoped historical source fixtures. ++ ++R3 retains precise decision `not_before`; it does not floor that lower bound to ++mask Approval's current whole-second consumption receipt defect. Approval-owner ++precision repair and full owner integration remain separate reviewed evidence. +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +@@ -0,0 +1,8 @@ ++- id: unsigned-service-caller-not-authority ++ request: ++ tenant: tenant:platform ++ subject: {id: secrets-engine, type: service} ++ action: execute-ccr ++ resource: {id: catalog:test, type: secret-catalog-lane, system: secrets-engine} ++ context: {human_control: true, runtime_admission: true, verified: true} ++ expect: {effect: deny, reason: service_execution_contract_unmatched} +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_package.md b/examples/secrets-engine-service-task-execution-v2/policy_package.md +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_package.md +@@ -0,0 +1,120 @@ ++--- ++id: secrets-engine.task-execution.service-pilot ++name: Secrets admitted service task execution ++namespace: secrets-engine:secret-catalog-lane ++version: v2 ++status: proposed ++package: flexauth.secrets_engine.service_task_execution ++allow_ttl: 120s ++actions: [execute-ccr] ++owner: team:platform-security ++fixtures: [policy_fixtures.yaml] ++caring: ++ profile: caring-0.4.0-rc2 ++ enforce: false ++activation: ++ mode: local ++--- ++ ++# Trusted service task source successor ++ ++This source package is evaluated exclusively by the typed v2 HTTP path. That ++path requires exact TokenReview caller mapping, immutable numeric source ++representations, a fixed authenticated Approval reader, separate original and ++fresh claim validation and native/PDP correspondence before policy evaluation. ++Its 120s declared TTL is a hard source ceiling, not a blanket grant. The typed ++path derives actual expiry exactly from the immutable operation deadline and ++requires the entire declared backend TTL plus the existing 5s cleanup reserve ++within original/fresh claim, approval and caller bounds. Budgets that cannot fit ++refuse; the 120s structural backend maximum is not guaranteed admissible. ++Generic check, batch and list routes refuse task execution. Selecting this source ++package alone cannot bypass those joins. No deployment or trust admission is ++established by this package. The v1 package remains unchanged and DENY-only. ++ ++```rego ++default decision := {"effect": "deny", "reason": "service_execution_contract_unmatched"} ++ ++decision := {"effect": "allow", "reason": "exact_task_execution_matched"} { ++ candidate_matches ++} ++ ++hash_ok(x) { is_string(x); regex.match("^sha256:[0-9a-f]{64}$", x) } ++ ++candidate_matches { ++ input.action == "execute-ccr" ++ input.tenant == "tenant:platform" ++ input.subject.id == "secrets-engine" ++ input.subject.type == "service" ++ input.resource.system == "secrets-engine" ++ input.resource.type == "secret-catalog-lane" ++ input.context.human_control == true ++ c := input.context.execution_contract ++ c.contract_version == "SECRETS-EXEC-20261011-v1" ++ c.operation == "metadata-apply-ccr" ++ c.execution_actor == "service:secrets-engine" ++ regex.match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", c.operation_id) ++ c.ccr_id != c.execution_grant_ccr_id ++ hash_ok(c.ccr_immutable_sha256) ++ hash_ok(c.grant_immutable_sha256) ++ hash_ok(c.catalog_sha256) ++ hash_ok(c.expected_before) ++ hash_ok(c.expected_after) ++ c.backend_budget.ttl_seconds <= 120 ++ c.backend_budget.ttl_seconds >= 30 ++ c.backend_budget.uses == 12 ++ count(c.operations) == 6 ++ c.operations[0].method == "GET" ++ c.operations[1].method == "PUT" ++ c.operations[2].method == "GET" ++ c.operations[3].method == "GET" ++ c.operations[4].method == "PUT" ++ c.operations[5].method == "GET" ++ regex.match("^sys/policies/acl/[A-Za-z0-9_-]+$", c.operations[0].path) ++ regex.match("^auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+$", c.operations[3].path) ++ c.operations[0].path == c.operations[1].path ++ c.operations[1].path == c.operations[2].path ++ c.operations[3].path == c.operations[4].path ++ c.operations[4].path == c.operations[5].path ++ every_operation_hash_valid ++ b := input.context.service_actor_binding ++ count(b) == 5 ++ b.version == "proposed-service-task-caller/v1" ++ b.transport_principal == "system:serviceaccount:secrets-engine:secrets-engine" ++ b.subject_id == input.subject.id ++ b.subject_type == "service" ++ b.tenant == input.tenant ++ not input.context.task_execution_projection ++ not input.context.initiator ++ not input.context.represented_principal ++ not input.context.delegation ++ input.resource.attributes.stage == "prod" ++ time.parse_rfc3339_ns(c.operation_deadline) > time.now_ns() ++ time.parse_rfc3339_ns(c.operation_deadline) <= time.now_ns() + 120000000000 ++ time.parse_rfc3339_ns(c.operation_deadline) >= time.now_ns() + (c.backend_budget.ttl_seconds + 5) * 1000000000 ++ input.context.approval.schema_version == "0.1" ++ input.context.approval.kind == "approval-claim" ++ input.context.approval.issuer == "approval-engine" ++ input.context.approval.valid_now == true ++ input.context.approval.consumed == false ++ input.context.approval.reason_code == "ok" ++ input.context.approval.binding.pdp_path == true ++ hash_ok(input.context.approval.binding.pdp_digest) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) > time.now_ns() ++ time.parse_rfc3339_ns(input.context.approval.validity.expires_at) >= time.parse_rfc3339_ns(c.operation_deadline) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) >= time.parse_rfc3339_ns(c.operation_deadline) ++ input.context.approval.binding.human_control == true ++} ++ ++every_operation_hash_valid { ++ count([x | x := input.context.execution_contract.operations[_]; hash_ok(x.payload_sha256)]) == 6 ++} ++``` ++ ++```rego test ++import data.flexauth.secrets_engine.service_task_execution ++ ++test_unsigned_flags_cannot_enable { ++ service_task_execution.decision.effect == "deny" ++ with input as {"context": {"runtime_admission": true, "verified": true}} ++} ++``` +diff --git a/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +@@ -0,0 +1,6 @@ ++# Synthetic source-validation fixture only; no estate registry admission. ++resources: ++ - id: catalog:synthetic ++ system: secrets-engine ++ type: secret-catalog-lane ++ attributes: {stage: prod} +diff --git a/go.mod b/go.mod +--- a/go.mod ++++ b/go.mod +@@ -3,6 +3,7 @@ + go 1.26.7 + + require ( ++ github.com/netkingdom/approval-engine/adapters/go/approvalfact v0.0.0 + github.com/open-policy-agent/opa v1.4.0 + gopkg.in/yaml.v3 v3.0.1 + ) +@@ -39,3 +40,5 @@ + google.golang.org/protobuf v1.36.6 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect + ) ++ ++replace github.com/netkingdom/approval-engine/adapters/go/approvalfact => ./third_party/approvalfact +diff --git a/internal/audit/log.go b/internal/audit/log.go +--- a/internal/audit/log.go ++++ b/internal/audit/log.go +@@ -8,6 +8,7 @@ + "path/filepath" + "sync" + ++ "github.com/netkingdom/access-engine/internal/sign" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -67,8 +68,8 @@ + if scanner.Text() == "" { + continue + } +- var decision api.DecisionEnvelope +- if err := json.Unmarshal(scanner.Bytes(), &decision); err != nil { ++ decision, err := sign.DecodeEnvelope(scanner.Bytes()) ++ if err != nil { + return nil, fmt.Errorf("unmarshal decision log line %d: %w", len(decisions)+1, err) + } + decisions = append(decisions, decision) +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -158,6 +158,10 @@ + func (e *Engine) IsServicePilot() bool { + return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion + } ++func (e *Engine) IsTrustedServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion ++} ++func (e *Engine) IsTaskPin() bool { return e.IsServicePilot() || e.IsTrustedServicePilot() } + + // ErrServicePilotUnavailable withholds any result that is not this B1 slice's + // signed DENY. Provider joins and executable eligibility remain unimplemented. +@@ -189,7 +193,7 @@ + // Check evaluates one subject/action/resource request. The decision is + // released only after its required record commits (GH-DEC-2026-024). + func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return api.DecisionEnvelope{}, ErrServicePilotUnavailable + } + decision, err := e.decide(ctx, request) +@@ -205,7 +209,7 @@ + // BatchCheck evaluates one subject/action/context tuple against resources in + // request order, and commits the whole batch in one durable write. + func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) +@@ -232,7 +236,7 @@ + + // ListAllowed evaluates candidate resources and returns only allow decisions. + func (e *Engine) ListAllowed(ctx context.Context, request ListAllowedRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) +diff --git a/internal/decision/trusted_service.go b/internal/decision/trusted_service.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service.go +@@ -0,0 +1,193 @@ ++package decision ++ ++import ( ++ "context" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "time" ++) ++ ++// legacyDigestProjection preserves the published PDP digest profile while the ++// signed structured binding retains original numeric kinds. It is used only ++// for digest material, never to reconstruct an Approval-native act. ++func legacyDigestProjection(request api.CheckRequest) (api.CheckRequest, error) { ++ raw, err := json.Marshal(request) ++ if err != nil { ++ return api.CheckRequest{}, err ++ } ++ var projected api.CheckRequest ++ err = json.Unmarshal(raw, &projected) ++ return projected, err ++} ++func nativeServiceAct(request api.CheckRequest) ([]byte, error) { ++ purpose, ok := request.Context["purpose"].(string) ++ if !ok || purpose == "" || request.Subject.ID != "secrets-engine" || request.Subject.Type != "service" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ if principal, exists := request.Subject.Attributes["principal"]; exists && principal != "secrets-engine" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ // All map numbers originate from the number-preserving request/registry ++ // profile. This raw draft is canonicalized only by the Approval owner helper. ++ return json.Marshal(map[string]any{"action": request.Action, "actor": request.Subject.ID, "principal": "secrets-engine", "purpose": purpose, "target": request.Resource}) ++} ++func (e *Engine) signTaskEnvelope(envelope *api.DecisionEnvelope) error { ++ e.mu.RLock() ++ key, kid := e.signingKey, e.signingKID ++ e.mu.RUnlock() ++ if len(key) == 0 { ++ return ErrServicePilotUnavailable ++ } ++ return sign.Sign(envelope, key, kid) ++} ++ ++// CheckTrustedService is the only v2 task evaluation path. Its source reader ++// and immutable service input are server-created, not HTTP validation flags. ++// It never consumes an approval or causes a protected backend effect. ++func (e *Engine) CheckTrustedService(ctx context.Context, input taskexecution.ServiceInput, source *taskexecution.ApprovalReader) (api.DecisionEnvelope, error) { ++ if !e.IsTrustedServicePilot() || !input.NumberPreserved() || !source.Configured() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now := e.now() ++ submitted, err := input.RequestAt(now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ normalized, facts := e.normalizeRequest(submitted) ++ if (facts.resourceFound || facts.subjectFound) && !e.store.NumbersPreserved() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ original, err := legacyDigestProjection(submitted) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ evaluated, err := legacyDigestProjection(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ native, err := nativeServiceAct(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ ctx = callerauth.WithRecord(ctx, input.Caller()) ++ var approvalEvidence *api.ApprovalObservationProvenance ++ deadline := input.Caller().NotAfter ++ contract := submitted.Context["execution_contract"].(map[string]any) ++ operationEnd, err := time.Parse(time.RFC3339Nano, contract["operation_deadline"].(string)) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrServiceInput ++ } ++ if operationEnd.Before(deadline) { ++ deadline = operationEnd ++ } ++ carried, hasClaim := submitted.Context[api.ApprovalContextKey] ++ if hasClaim { ++ claim, ok := carried.(map[string]any) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ id, ok := claim["approval_id"].(string) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ originalRaw, err := json.Marshal(carried) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ observed, err := source.Observe(ctx, id) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ // One validation instant AFTER network I/O; a fresh read cannot rescue the ++ // original carried claim's freshness, source expiry or operation deadline. ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ verified, err := observed.Validate(originalRaw, approvalfact.Expected{ApprovalID: id, NativeBindingJSON: native, PDPDigest: api.ApprovalBindingDigest(evaluated)}, now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ factEnd, err := verified.Deadline() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if factEnd.Before(deadline) { ++ deadline = factEnd ++ } ++ approvalEvidence, err = verified.Provenance() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ } ++ expectation, err := e.policy.EvaluatePreservingNumbers(ctx, normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if !hasClaim && expectation.Effect == api.DecisionEffectAllow { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope := e.envelope(ctx, normalized, submitted, expectation, facts) ++ envelope.Binding.RequestDigest = api.RequestDigest(evaluated) ++ envelope.Binding.SubmittedRequestDigest = api.RequestDigest(original) ++ // Claim-free signed DENY is the producer-owned normalization receipt used ++ // before Approval issue. It supplies facts only, never execution authority. ++ envelope.Binding.ApprovalBindingDigest = api.ApprovalBindingDigest(evaluated) ++ envelope.Provenance.ApprovalObservation = approvalEvidence ++ if envelope.Effect == api.DecisionEffectAllow { ++ if approvalEvidence == nil || envelope.Lifetime == nil { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ lifetime, err := trustedTaskLifetime(contract, operationEnd, deadline, now, e.policy.Metadata.AllowTTL) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope.Lifetime = lifetime ++ } ++ if err := e.signTaskEnvelope(&envelope); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ ++// The source ceiling is deliberately no larger than the existing task-token ++// ceiling. Structural backend budgets do not guarantee an executable window. ++const trustedTaskMaxAuthority = 120 * time.Second ++const trustedTaskCleanupReserve = 5 * time.Second ++ ++// trustedTaskLifetime authorizes the entire exact operation, not merely its ++// start. It cannot extend any caller, original/fresh fact, or approval bound. ++func trustedTaskLifetime(contract map[string]any, operationEnd, authorityEnd, now time.Time, declared string) (*api.DecisionLifetime, error) { ++ maximum, err := api.ParseAllowTTL(declared) ++ if err != nil || maximum <= 0 || maximum > trustedTaskMaxAuthority || now.IsZero() || operationEnd.After(authorityEnd) || !operationEnd.After(now) || operationEnd.Sub(now) > maximum { ++ return nil, ErrServicePilotUnavailable ++ } ++ budget, ok := contract["backend_budget"].(map[string]any) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ number, ok := budget["ttl_seconds"].(json.Number) ++ if !ok { ++ return nil, ErrServicePilotUnavailable ++ } ++ seconds, err := number.Int64() ++ if err != nil || seconds < 30 || seconds > 120 { ++ return nil, ErrServicePilotUnavailable ++ } ++ remaining := operationEnd.Sub(now) ++ if remaining < time.Duration(seconds)*time.Second+trustedTaskCleanupReserve { ++ return nil, ErrServicePilotUnavailable ++ } ++ return &api.DecisionLifetime{Kind: api.DecisionLifetimeTTL, TTL: remaining.String(), NotBefore: now.UTC().Format(time.RFC3339Nano), ExpiresAt: operationEnd.UTC().Format(time.RFC3339Nano)}, nil ++} +diff --git a/internal/decision/trusted_service_time_test.go b/internal/decision/trusted_service_time_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service_time_test.go +@@ -0,0 +1,56 @@ ++package decision ++ ++import ( ++ "encoding/json" ++ "testing" ++ "time" ++) ++ ++func TestTrustedTaskWholeOperationLifetime(t *testing.T) { ++ now := time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) ++ contract := func(ttl string) map[string]any { ++ return map[string]any{"backend_budget": map[string]any{"ttl_seconds": json.Number(ttl), "uses": json.Number("12")}} ++ } ++ end := now.Add(60 * time.Second) ++ lifetime, err := trustedTaskLifetime(contract("30"), end, now.Add(120*time.Second), now, "120s") ++ if err != nil { ++ t.Fatal(err) ++ } ++ if lifetime.ExpiresAt != end.Format(time.RFC3339) || lifetime.TTL != "1m0s" || lifetime.NotBefore != now.Format(time.RFC3339) { ++ t.Fatal("lifetime not derived from exact operation") ++ } ++ fractional := end.Add(123456789 * time.Nanosecond) ++ lifetime, err = trustedTaskLifetime(contract("30"), fractional, fractional, now, "120s") ++ if err != nil || lifetime.ExpiresAt != fractional.Format(time.RFC3339Nano) { ++ t.Fatal("exact fractional deadline shortened or extended") ++ } ++ cases := []struct { ++ name string ++ ttl string ++ end, authority, time time.Time ++ maximum string ++ }{ ++ {"old-15s-conflict", "30", end, end, now, "15s"}, ++ {"insufficient-cleanup-reserve", "30", now.Add(35*time.Second - time.Nanosecond), end, now, "120s"}, ++ {"elapsed-consume-setup", "30", end, end, now.Add(26 * time.Second), "120s"}, ++ {"expired-operation", "30", now, now, now, "120s"}, ++ {"source-hard-maximum", "30", now.Add(120*time.Second + time.Nanosecond), now.Add(130 * time.Second), now, "120s"}, ++ {"attempted-policy-cap-bypass", "30", end, end, now, "121s"}, ++ {"shorter-original-or-fresh-fact", "30", end, end.Add(-time.Nanosecond), now, "120s"}, ++ {"structural-120-cannot-fit", "120", now.Add(120 * time.Second), now.Add(120 * time.Second), now, "120s"}, ++ {"noninteger-structural-ttl", "30.0", end, end, now, "120s"}, ++ } ++ for _, c := range cases { ++ t.Run(c.name, func(t *testing.T) { ++ if _, err := trustedTaskLifetime(contract(c.ttl), c.end, c.authority, c.time, c.maximum); err == nil { ++ t.Fatal("invalid temporal authority accepted") ++ } ++ }) ++ } ++ if _, err := trustedTaskLifetime(contract("30"), now.Add(35*time.Second), end, now, "120s"); err != nil { ++ t.Fatal("exact required lifetime refused") ++ } ++ if _, err := trustedTaskLifetime(contract("115"), now.Add(120*time.Second), now.Add(120*time.Second), now, "120s"); err != nil { ++ t.Fatal("max fitting declared budget refused") ++ } ++} +diff --git a/internal/emission/delivery.go b/internal/emission/delivery.go +--- a/internal/emission/delivery.go ++++ b/internal/emission/delivery.go +@@ -112,7 +112,9 @@ + return entry{}, 0, err + } + var e entry +- if err := json.Unmarshal(line, &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(line)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return entry{}, 0, err + } + if e.Seq != o.cursor.Seq+1 || e.Event.Source != o.source { +diff --git a/internal/emission/numeric_replay_test.go b/internal/emission/numeric_replay_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/emission/numeric_replay_test.go +@@ -0,0 +1,53 @@ ++package emission ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestOutboxReplayPreservesProducerNumericSignature(t *testing.T) { ++ folder := t.TempDir() ++ outbox, err := Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{21}, 32)) ++ envelope := api.DecisionEnvelope{ID: "decision:synthetic-numeric", Effect: api.DecisionEffectDeny, Resource: api.ResourceRef{ID: "synthetic", Type: "tenant", System: "tenant-engine"}} ++ envelope.Binding = &api.DecisionBinding{Context: map[string]any{"float": json.Number("1.0"), "negative_zero": json.Number("-0.0"), "integer": json.Number("9007199254740993"), "exponent": json.Number("1e-07")}} ++ if err := sign.Sign(&envelope, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ if err := outbox.Append(envelope); err != nil { ++ t.Fatal(err) ++ } ++ outbox.Close() ++ outbox, err = Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer outbox.Close() ++ entry, _, err := outbox.nextEntry() ++ if err != nil { ++ t.Fatal(err) ++ } ++ wire, err := json.Marshal(entry.Event.Data) ++ if err != nil { ++ t.Fatal(err) ++ } ++ replayed, err := sign.DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := sign.Verify(replayed, map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)}); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := sign.CanonicalBytes(envelope) ++ after, _ := sign.CanonicalBytes(replayed) ++ if !bytes.Equal(before, after) { ++ t.Fatal("replayed signed bytes changed") ++ } ++} +diff --git a/internal/emission/outbox.go b/internal/emission/outbox.go +--- a/internal/emission/outbox.go ++++ b/internal/emission/outbox.go +@@ -136,7 +136,9 @@ + return os.Truncate(o.path, o.size) + } + var e entry +- if err := json.Unmarshal(data[good:good+end], &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(data[good : good+end])) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return fmt.Errorf("outbox line %d is corrupt: %w", line, err) + } + if e.Seq != o.seq+1 || e.Event.Source != o.source { +diff --git a/internal/policy/package.go b/internal/policy/package.go +--- a/internal/policy/package.go ++++ b/internal/policy/package.go +@@ -1,6 +1,7 @@ + package policy + + import ( ++ "bytes" + "context" + "encoding/json" + "fmt" +@@ -375,7 +376,25 @@ + } + + func (p *Package) evaluateDecision(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, false) ++} ++ ++// EvaluatePreservingNumbers feeds the same evaluator the task profile's exact ++// numeric values. Ordinary evaluation behavior and canonical contracts stay put. ++func (p *Package) EvaluatePreservingNumbers(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, true) ++} ++func (p *Package) evaluateDecisionProfile(ctx context.Context, request api.CheckRequest, preserve bool) (api.DecisionExpectation, error) { + input, err := toRegoInput(request) ++ if preserve { ++ raw, e := json.Marshal(request) ++ if e != nil { ++ return api.DecisionExpectation{}, e ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ err = decoder.Decode(&input) ++ } + if err != nil { + return api.DecisionExpectation{}, err + } +diff --git a/internal/registry/numeric_source_test.go b/internal/registry/numeric_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/registry/numeric_source_test.go +@@ -0,0 +1,36 @@ ++package registry ++ ++import ( ++ "encoding/json" ++ "os" ++ "path/filepath" ++ "testing" ++) ++ ++func TestTaskRegistryNumericSourceAndStrictDecode(t *testing.T) { ++ path := filepath.Join(t.TempDir(), "snapshot.json") ++ raw := []byte(`{"resource_manifests":[{"id":"synthetic","system":"secrets-engine","resources":[{"id":"catalog:synthetic","type":"secret-catalog-lane","attributes":{"float":1.0,"integer":9007199254740993,"negative_zero":-0.0}}]}]}`) ++ os.WriteFile(path, raw, 0600) ++ store, err := LoadFilePreservingNumbers(path) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if !store.NumbersPreserved() { ++ t.Fatal("missing source profile") ++ } ++ resource, ok := store.Resource("secrets-engine", "catalog:synthetic") ++ if !ok { ++ t.Fatal("missing resource") ++ } ++ for k, v := range map[string]string{"float": "1.0", "integer": "9007199254740993", "negative_zero": "-0.0"} { ++ if resource.Attributes[k] != json.Number(v) { ++ t.Fatalf("lost native representation: %s", k) ++ } ++ } ++ for _, raw := range []string{`{} {}`, `{"subjects":[],"subjects":[]}`, `{"unknown":true}`} { ++ os.WriteFile(path, []byte(raw), 0600) ++ if _, err := LoadFilePreservingNumbers(path); err == nil { ++ t.Fatal("invalid authoritative registry accepted") ++ } ++ } ++} +diff --git a/internal/registry/store.go b/internal/registry/store.go +--- a/internal/registry/store.go ++++ b/internal/registry/store.go +@@ -1,6 +1,7 @@ + package registry + + import ( ++ "bytes" + "encoding/json" + "fmt" + "os" +@@ -8,10 +9,12 @@ + "sort" + + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + ) + + // Store is a deterministic local registry for standalone development and tests. + type Store struct { ++ numbersPreserved bool + systems map[string]api.ProtectedSystemManifest + resourceManifests map[string]api.ResourceManifest + resources map[string]api.Resource +@@ -66,6 +69,32 @@ + return store, nil + } + ++// LoadFilePreservingNumbers is the task-source profile: owner registry numeric ++// representations stay intact for native act correspondence. Ordinary loaders ++// and their existing snapshot/digest behavior remain unchanged. ++func LoadFilePreservingNumbers(path string) (*Store, error) { ++ raw, err := os.ReadFile(path) ++ if err != nil { ++ return nil, err ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, err ++ } ++ var snapshot Snapshot ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&snapshot); err != nil { ++ return nil, err ++ } ++ store := NewStore() ++ if err := store.ApplySnapshot(snapshot); err != nil { ++ return nil, err ++ } ++ store.numbersPreserved = true ++ return store, nil ++} ++ + // SaveFile writes the store as deterministic pretty-printed JSON. + func (s *Store) SaveFile(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { +@@ -380,3 +409,6 @@ + } + return out + } ++ ++// NumbersPreserved reports the source profile of file-backed registry facts. ++func (s *Store) NumbersPreserved() bool { return s != nil && s.numbersPreserved } +diff --git a/internal/sign/numeric_decode_test.go b/internal/sign/numeric_decode_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/sign/numeric_decode_test.go +@@ -0,0 +1,54 @@ ++package sign ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestProducerNumericEnvelopeDecodeCompatibility(t *testing.T) { ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{11}, 32)) ++ keys := map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)} ++ for _, raw := range []string{`1`, `1.0`, `-0.0`, `1e-07`, `1e+20`, `9007199254740993`, `1.7976931348623157e308`, `5e-324`, `true`, `"<>&\u2028\u2029 cafĂ©"`, `[1,1.0,-0.0]`, `{"nested":1.0}`} { ++ t.Run(raw, func(t *testing.T) { ++ var value any ++ decoder := json.NewDecoder(bytes.NewBufferString(raw)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&value); err != nil { ++ t.Fatal(err) ++ } ++ original := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"numeric": value}}} ++ if err := Sign(&original, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ wire, _ := json.Marshal(original) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := Verify(decoded, keys); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := CanonicalBytes(original) ++ after, _ := CanonicalBytes(decoded) ++ if !bytes.Equal(before, after) { ++ t.Fatal("producer canonical bytes changed") ++ } ++ }) ++ } ++ // Existing float64 producer profile still verifies through the new decoder. ++ legacy := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"number": float64(1), "exponent": float64(1e-7)}}} ++ Sign(&legacy, key, "synthetic-numeric") ++ wire, _ := json.Marshal(legacy) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil || Verify(decoded, keys) != nil { ++ t.Fatal("legacy producer compatibility failed") ++ } ++ for _, raw := range []string{`{} {}`, `{"new_unknown_authority":true}`} { ++ if _, err := DecodeEnvelope([]byte(raw)); err == nil { ++ t.Fatal("invalid envelope accepted") ++ } ++ } ++} +diff --git a/internal/sign/sign.go b/internal/sign/sign.go +--- a/internal/sign/sign.go ++++ b/internal/sign/sign.go +@@ -4,10 +4,12 @@ + package sign + + import ( ++ "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" ++ "io" + + "github.com/netkingdom/access-engine/pkg/api" + ) +@@ -74,3 +76,20 @@ + } + return nil + } ++ ++// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs. ++// Legacy producer envelopes remain byte-compatible. It does not invent another ++// signature canonicalization: CanonicalBytes still calls typed json.Marshal. ++func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) { ++ var envelope api.DecisionEnvelope ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&envelope); err != nil { ++ return envelope, err ++ } ++ if _, err := decoder.Token(); err != io.EOF { ++ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data") ++ } ++ return envelope, nil ++} +diff --git a/internal/taskexecution/approval_source.go b/internal/taskexecution/approval_source.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source.go +@@ -0,0 +1,215 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "crypto/sha256" ++ "crypto/tls" ++ "crypto/x509" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "mime" ++ "net/http" ++ "net/url" ++ "os" ++ "path/filepath" ++ "regexp" ++ "strings" ++ "time" ++ ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++var ErrApprovalSource = errors.New("trusted Approval source refused") ++var approvalUUID = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`) ++ ++type ApprovalSourceConfig struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ Origin string `json:"origin"` ++ StoreTenant string `json:"store_tenant"` ++ CAFile string `json:"ca_file"` ++ TokenFile string `json:"token_file"` ++} ++ ++// ApprovalReader is installed from owner configuration, never an HTTP request. ++// It reads one fixed PIP endpoint; no policy, retry, consume or effect lives here. ++type ApprovalReader struct { ++ config ApprovalSourceConfig ++ configurationDigest string ++ client *http.Client ++} ++ ++func NewApprovalReader(config ApprovalSourceConfig) (*ApprovalReader, error) { ++ u, err := url.Parse(config.Origin) ++ if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || strings.ContainsAny(config.Origin, "\r\n\t ") || config.SourceID == "" || config.SourceRevision == "" || config.StoreTenant != "tenant:platform" || !filepath.IsAbs(config.CAFile) || !filepath.IsAbs(config.TokenFile) { ++ return nil, ErrApprovalSource ++ } ++ ca, err := readPublicApprovalFile(config.CAFile, 1048576) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ pool := x509.NewCertPool() ++ if !pool.AppendCertsFromPEM(ca) { ++ return nil, ErrApprovalSource ++ } ++ config.Origin = strings.TrimRight(config.Origin, "/") ++ digest := api.CanonicalDigest(struct { ++ Config ApprovalSourceConfig `json:"configuration"` ++ CA string `json:"ca_sha256"` ++ }{config, byteDigest(ca)}) ++ return &ApprovalReader{config: config, configurationDigest: digest, client: &http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{Proxy: nil, DisableCompression: true, TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}}}}, nil ++} ++func readPublicApprovalFile(path string, limit int64) ([]byte, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 { ++ return nil, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, limit+1)) ++ if err != nil || int64(len(raw)) > limit { ++ return nil, ErrApprovalSource ++ } ++ return raw, nil ++} ++func LoadApprovalReader(path string) (*ApprovalReader, error) { ++ if !filepath.IsAbs(path) { ++ return nil, ErrApprovalSource ++ } ++ raw, err := readPublicApprovalFile(path, 65536) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, ErrApprovalSource ++ } ++ var config ApprovalSourceConfig ++ decoder := json.NewDecoder(strings.NewReader(string(raw))) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&config) != nil { ++ return nil, ErrApprovalSource ++ } ++ return NewApprovalReader(config) ++} ++func (r *ApprovalReader) Configured() bool { ++ return r != nil && r.client != nil && r.configurationDigest != "" ++} ++func byteDigest(raw []byte) string { ++ h := sha256.Sum256(raw) ++ return "sha256:" + hex.EncodeToString(h[:]) ++} ++func readApprovalToken(path string) (string, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return "", ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 { ++ return "", ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, 8193)) ++ if err != nil || len(raw) > 8192 { ++ return "", ErrApprovalSource ++ } ++ token := strings.TrimSpace(string(raw)) ++ if token == "" || strings.ContainsAny(token, " \t\r\n") { ++ return "", ErrApprovalSource ++ } ++ return token, nil ++} ++ ++// ApprovalObservation cannot be created by caller JSON or a public verified flag. ++// It is raw fact data plus the source/configuration used by this fixed reader. ++type ApprovalObservation struct { ++ raw []byte ++ sourceID, revision, configurationDigest string ++} ++ ++func (r *ApprovalReader) Observe(ctx context.Context, id string) (ApprovalObservation, error) { ++ if !r.Configured() || !approvalUUID.MatchString(id) { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ token, err := readApprovalToken(r.config.TokenFile) ++ if err != nil { ++ return ApprovalObservation{}, err ++ } ++ req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.config.Origin+"/v1/approvals/"+id+"/claim", nil) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ req.Header.Set("Authorization", "Bearer "+token) ++ req.Header.Set("Accept", "application/json") ++ response, err := r.client.Do(req) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ defer response.Body.Close() ++ types := response.Header.Values("Content-Type") ++ if len(types) != 1 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ media, parameters, err := mime.ParseMediaType(types[0]) ++ if err != nil || media != "application/json" || (parameters["charset"] != "" && !strings.EqualFold(parameters["charset"], "utf-8")) || (len(parameters) != 0 && (len(parameters) != 1 || parameters["charset"] == "")) || response.StatusCode != 200 || response.Header.Get("Content-Encoding") != "" { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(response.Body, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ return ApprovalObservation{raw: append([]byte(nil), raw...), sourceID: r.config.SourceID, revision: r.config.SourceRevision, configurationDigest: r.configurationDigest}, nil ++} ++ ++// VerifiedApprovalFacts establishes typed fact correspondence, not a decision. ++// Only an authenticated fixed-reader observation can construct this handle. ++type VerifiedApprovalFacts struct { ++ original, fetched approvalfact.Fact ++ evidence api.ApprovalObservationProvenance ++ valid bool ++} ++ ++func (o ApprovalObservation) Validate(original []byte, expected approvalfact.Expected, now time.Time) (VerifiedApprovalFacts, error) { ++ if len(o.raw) == 0 || o.sourceID == "" || o.revision == "" || o.configurationDigest == "" { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ originalFact, err := approvalfact.ValidateClaim(original, expected, now) ++ if err != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ fetched, err := approvalfact.ValidateClaim(o.raw, expected, now) ++ if err != nil || approvalfact.MatchObserved(originalFact, fetched) != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ limit := originalFact.Deadline() ++ if fetched.Deadline().Before(limit) { ++ limit = fetched.Deadline() ++ } ++ evidence := api.ApprovalObservationProvenance{SourceID: o.sourceID, SourceRevision: o.revision, ConfigurationDigest: o.configurationDigest, ClaimDigest: byteDigest(o.raw), NativeBindingDigest: fetched.NativeDigest(), ObservedAt: fetched.ObservedAt().UTC().Format(time.RFC3339Nano), NotAfter: limit.UTC().Format(time.RFC3339Nano), ValidatedAt: now.UTC().Format(time.RFC3339Nano)} ++ return VerifiedApprovalFacts{original: originalFact, fetched: fetched, evidence: evidence, valid: true}, nil ++} ++func (f VerifiedApprovalFacts) Deadline() (time.Time, error) { ++ if !f.valid { ++ return time.Time{}, ErrApprovalSource ++ } ++ a := f.original.Deadline() ++ if f.fetched.Deadline().Before(a) { ++ a = f.fetched.Deadline() ++ } ++ return a, nil ++} ++func (f VerifiedApprovalFacts) Provenance() (*api.ApprovalObservationProvenance, error) { ++ if !f.valid { ++ return nil, ErrApprovalSource ++ } ++ copy := f.evidence ++ return ©, nil ++} +diff --git a/internal/taskexecution/approval_source_test.go b/internal/taskexecution/approval_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source_test.go +@@ -0,0 +1,118 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "encoding/pem" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "path/filepath" ++ "strings" ++ "sync/atomic" ++ "testing" ++ "time" ++) ++ ++func TestFixedApprovalReaderRefusalBoundary(t *testing.T) { ++ folder := t.TempDir() ++ token := filepath.Join(folder, "token") ++ ca := filepath.Join(folder, "ca") ++ if err := os.WriteFile(token, []byte("synthetic-source-token"), 0440); err != nil { ++ t.Fatal(err) ++ } ++ var mode atomic.Value ++ mode.Store("ok") ++ var foreignCalls atomic.Int64 ++ foreign := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ foreignCalls.Add(1) ++ t.Error("credential escaped fixed origin") ++ })) ++ defer foreign.Close() ++ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.Header.Get("Authorization") != "Bearer synthetic-source-token" || r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Method != "GET" { ++ t.Error("wrong auth/path/method") ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode.Load().(string) { ++ case "redirect": ++ w.Header().Set("Location", foreign.URL) ++ w.WriteHeader(302) ++ return ++ case "status": ++ w.WriteHeader(403) ++ case "large": ++ w.Write([]byte(strings.Repeat(" ", 524289))) ++ case "duplicate": ++ w.Write([]byte(`{"a":true,"a":false}`)) ++ case "invalid-utf8": ++ w.Write([]byte{'"', 255, '"'}) ++ case "media": ++ w.Header().Set("Content-Type", "application/json; arbitrary=authority") ++ w.Write([]byte(`{}`)) ++ case "duplicate-media": ++ w.Header().Add("Content-Type", "application/json") ++ w.Write([]byte(`{}`)) ++ case "compressed": ++ w.Header().Set("Content-Encoding", "gzip") ++ w.Write([]byte(`{}`)) ++ case "slow": ++ select { ++ case <-r.Context().Done(): ++ case <-time.After(time.Second): ++ } ++ default: ++ w.Write([]byte(`{}`)) ++ } ++ })) ++ defer server.Close() ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0600) ++ config := ApprovalSourceConfig{SourceID: "synthetic-source", SourceRevision: "synthetic-revision", Origin: server.URL, StoreTenant: "tenant:platform", CAFile: ca, TokenFile: token} ++ reader, err := NewApprovalReader(config) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err != nil { ++ t.Fatal(err) ++ } ++ for _, name := range []string{"redirect", "status", "large", "duplicate", "invalid-utf8", "media", "duplicate-media", "compressed", "slow"} { ++ t.Run(name, func(t *testing.T) { ++ mode.Store(name) ++ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) ++ defer cancel() ++ if _, err := reader.Observe(ctx, "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("invalid provider response admitted") ++ } ++ }) ++ } ++ if foreignCalls.Load() != 0 { ++ t.Fatal("redirect followed") ++ } ++ mode.Store("ok") ++ for _, bad := range []string{"../consume", "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/consume", ""} { ++ if _, err := reader.Observe(context.Background(), bad); err == nil { ++ t.Fatal("request selected route") ++ } ++ } ++ for _, mode := range []os.FileMode{0460, 0644, 0620} { ++ os.Chmod(token, mode) ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("unsafe token permissions") ++ } ++ } ++ for _, origin := range []string{"http://127.0.0.1", "https://user:password@localhost", "https://localhost/path", "https://localhost?query=1", "https://localhost#fragment"} { ++ bad := config ++ bad.Origin = origin ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("unsafe origin") ++ } ++ } ++ bad := config ++ bad.StoreTenant = "tenant:foreign" ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("foreign tenant configured") ++ } ++ if _, err := (ApprovalObservation{}).Validate([]byte(`{}`), approvalfact.Expected{}, time.Now()); err == nil { ++ t.Fatal("zero observation admitted") ++ } ++} +diff --git a/internal/taskexecution/service_input.go b/internal/taskexecution/service_input.go +--- a/internal/taskexecution/service_input.go ++++ b/internal/taskexecution/service_input.go +@@ -7,6 +7,7 @@ + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + "io" + "time" + ) +@@ -15,6 +16,7 @@ + + const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" + const ServicePilotPackageVersion = "v1" ++const TrustedServicePilotPackageVersion = "v2" + + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" +@@ -24,7 +26,10 @@ + // It installs the explicit + // proposed fixed subject-to-transport mapping, never a request-selected mapping. + // No constructor parameter can select warn/disabled mode or another subject. +-type ServicePilotInputs struct{ auth *callerauth.Authenticator } ++type ServicePilotInputs struct { ++ auth *callerauth.Authenticator ++ preserveNumbers bool ++} + + func NewServicePilotInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { + a, e := callerauth.New(callerauth.ModeEnforce, reviewer, "flex-auth", map[string]string{"secrets-engine": ServicePilotPrincipal}, nil) +@@ -34,12 +39,24 @@ + return &ServicePilotInputs{auth: a}, nil + } + ++// NewTrustedServiceInputs preserves numeric representations only for the exact ++// v2 task path; it grants no Approval-source trust or executable eligibility. ++func NewTrustedServiceInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { ++ input, err := NewServicePilotInputs(reviewer) ++ if err != nil { ++ return nil, err ++ } ++ input.preserveNumbers = true ++ return input, nil ++} ++ + // ServiceInput cannot be fabricated through exported fields. It proves only + // source input preparation through TokenReview, not approval provenance/consume + // or execution authority. Default zero value is not an authenticated input. + type ServiceInput struct { +- serialized []byte +- caller callerauth.Record ++ serialized []byte ++ caller callerauth.Record ++ preserveNumbers bool + } + + func (s ServiceInput) Request() (api.CheckRequest, error) { +@@ -47,7 +64,11 @@ + return api.CheckRequest{}, ErrServiceInput + } + var r api.CheckRequest +- if json.Unmarshal(s.serialized, &r) != nil { ++ decoder := json.NewDecoder(bytes.NewReader(s.serialized)) ++ if s.preserveNumbers { ++ decoder.UseNumber() ++ } ++ if decoder.Decode(&r) != nil { + return r, ErrServiceInput + } + return r, nil +@@ -142,5 +163,27 @@ + if err != nil { + return refuse() + } +- return ServiceInput{serialized: raw, caller: record}, nil ++ return ServiceInput{serialized: raw, caller: record, preserveNumbers: s.preserveNumbers}, nil + } ++ ++func (s ServiceInput) NumberPreserved() bool { return s.preserveNumbers } ++ ++// DecodeTrustedServiceRequest preserves numbers in the exact new task profile. ++// Owner helper validates strict Unicode/JSON; this is not a new canonicalizer. ++func DecodeTrustedServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ var request api.CheckRequest ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} +diff --git a/pkg/api/canonical.go b/pkg/api/canonical.go +--- a/pkg/api/canonical.go ++++ b/pkg/api/canonical.go +@@ -635,16 +635,30 @@ + + // DecisionProvenance captures evaluator and policy provenance. + type DecisionProvenance struct { +- Evaluator string `json:"evaluator" yaml:"evaluator"` +- Mode string `json:"mode" yaml:"mode"` +- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` +- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` +- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` +- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` +- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` +- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` +- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` +- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++ Evaluator string `json:"evaluator" yaml:"evaluator"` ++ Mode string `json:"mode" yaml:"mode"` ++ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` ++ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` ++ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` ++ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` ++ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` ++ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` ++ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` ++ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"` ++ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++} ++ ++// ApprovalObservationProvenance binds fresh server-owned fact evidence without ++// rewriting the original carried claim/context or either PDP request digest. ++type ApprovalObservationProvenance struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ ConfigurationDigest string `json:"configuration_digest"` ++ ClaimDigest string `json:"claim_digest"` ++ NativeBindingDigest string `json:"native_binding_digest"` ++ ObservedAt string `json:"observed_at"` ++ NotAfter string `json:"not_after"` ++ ValidatedAt string `json:"validated_at"` + } + + // CallerProvenance records how the request was authenticated to flex-auth. +diff --git a/third_party/approvalfact/README.md b/third_party/approvalfact/README.md +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/README.md +@@ -0,0 +1,55 @@ ++# Approval fact data adapter ++ ++Provider-owned pure Go module for Access's Approval B2 source reader. No network, ++credentials, subprocess, policy evaluator, server or consume mutation exists here. ++Python `approval_engine.binding` remains the authoritative native format. ++ ++API: ++ ++- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes ++ and sha256 digest; extra binding metadata is excluded. ++- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path ++ claim against independently owner-derived `Expected.NativeBindingJSON`, exact ++ approval ID and actual Access approval-free PDP digest. ++- `MatchObserved(submitted, observed)` requires two validated facts at the same ++ captured instant and identical nonfreshness authority facts. No request is ++ rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines ++ preserve each input's own freshness and approval expiry. ++ ++`Fact` means validated data, never authenticated provider origin. Access must ++fetch provider bytes through its independently admitted fixed origin/identity, ++then wrap this data in its own unforgeable source handle. A caller-supplied claim ++cannot become source truth merely by passing this helper. Access remains the ++single evaluator and must bound decision/operation/caller lifetimes separately. ++PEP consume CAS and live one-shot journal remain required before any effect. ++ ++Expected native act bytes must preserve the actual owner/compiler/normalization ++representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and ++large integers. Raw submitted resource is sufficient only when Access proves ++the active registry cannot override that target and applies its exact existing ++normalization. Otherwise authoritative raw enrichment/compiler evidence is ++needed. This helper supplies no fallback or invented restricted numeric domain. ++ ++The strict parser retains integer/floating lexical types, arbitrary integer ++magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values. ++Canonical float/string formatting follows Python source, including negative ++floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate ++object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. ++Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at ++most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++These explicit parser/resource limits fail closed, never normalize authority. ++ ++`go test -race ./...` checks the portable producer vectors, typed fact pairing, ++strict JSON/Unicode and exact deadline semantics. From the owner repo root, ++`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case ++development-only differential corpus against the actual Python producer, ++including random finite IEEE bit patterns, exponent boundaries/subnormals and ++Unicode. It uses a test-only local oracle file; production imports no oracle or ++Python runtime. Sample conformance is evidence, not a formal proof over all JSON. ++ ++The module pins Go 1.26.7 to match the existing Access consumer toolchain. ++`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner ++fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`. ++Transport cases requiring HTTP remain the Access adapter's tests; this module ++checks their duplicate/nonfinite bodies only. Import/deployment/native identity ++admission and positive policy gates require separate owner evidence. +diff --git a/third_party/approvalfact/SOURCE.json b/third_party/approvalfact/SOURCE.json +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/SOURCE.json +@@ -0,0 +1,13 @@ ++{ ++ "owner": "approval-engine", ++ "module": "github.com/netkingdom/approval-engine/adapters/go/approvalfact", ++ "source_artifact": "approval-go-fact/manifest.json", ++ "review_status": "owner-frozen; independent coordinator review required", ++ "role": "pure data canonicalization and fact correspondence only; not provider authentication or PDP", ++ "files_sha256": { ++ "canonical.go": "d48588fb26e9037c8e83421586b8a818f4c0a52afb874f3fadf7229fc971c282", ++ "claim.go": "007a2494f79c47c104635402407e9e68f17d8599d23e0f2c8ebf6cb1b47e3621", ++ "go.mod": "bb09c53ffea926fbfba6e580ed3bd71ca358575401717389b3c7c49c0c674b43", ++ "README.md": "5d936b7f3de87bbf4e946f0dc44823d2f69b62b4122b55b9064791b7a19fd4f5" ++ } ++} +diff --git a/third_party/approvalfact/canonical.go b/third_party/approvalfact/canonical.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/canonical.go +@@ -0,0 +1,295 @@ ++// Package approvalfact provides data-only Approval claim correspondence. ++// It neither authenticates a provider nor decides permission or consumes objects. ++package approvalfact ++ ++import ( ++ "bytes" ++ "crypto/sha256" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "math" ++ "math/big" ++ "sort" ++ "strconv" ++ "strings" ++ "unicode/utf8" ++) ++ ++const MaxJSONBytes = 1 << 20 ++ ++var errJSON = errors.New("invalid Approval JSON") ++ ++// DecodeJSON retains numeric lexical types, refuses duplicates and invalid ++// Unicode, and bounds work. A Go map previously decoded into float64 cannot ++// recover the producer's integer-versus-floating identity; pass source bytes. ++func DecodeJSON(raw []byte) (any, error) { ++ if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) { ++ return nil, errJSON ++ } ++ d := json.NewDecoder(bytes.NewReader(raw)) ++ d.UseNumber() ++ var read func(int) (any, error) ++ read = func(depth int) (any, error) { ++ if depth > 128 { ++ return nil, errJSON ++ } ++ t, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ switch x := t.(type) { ++ case json.Delim: ++ if x == '{' { ++ m := map[string]any{} ++ for d.More() { ++ k, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ s, ok := k.(string) ++ if !ok { ++ return nil, errJSON ++ } ++ if _, ok = m[s]; ok { ++ return nil, errJSON ++ } ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ m[s] = v ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim('}') { ++ return nil, errJSON ++ } ++ return m, nil ++ } ++ if x == '[' { ++ a := []any{} ++ for d.More() { ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ a = append(a, v) ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim(']') { ++ return nil, errJSON ++ } ++ return a, nil ++ } ++ return nil, errJSON ++ case json.Number: ++ if _, e := pythonNumber(x); e != nil { ++ return nil, e ++ } ++ return x, nil ++ case string, bool, nil: ++ return x, nil ++ default: ++ return nil, errJSON ++ } ++ } ++ v, e := read(0) ++ if e != nil { ++ return nil, e ++ } ++ if _, e = d.Token(); e != io.EOF { ++ return nil, errJSON ++ } ++ return v, nil ++} ++ ++// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer ++// cannot encode them as UTF-8. Reject before the decoder can erase that fact. ++func validSurrogates(raw []byte) bool { ++ in := false ++ for i := 0; i < len(raw); i++ { ++ if raw[i] == '"' { ++ in = !in ++ continue ++ } ++ if !in || raw[i] != '\\' { ++ continue ++ } ++ i++ ++ if i >= len(raw) { ++ return false ++ } ++ if raw[i] != 'u' { ++ continue ++ } ++ if i+4 >= len(raw) { ++ return false ++ } ++ n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16) ++ if e != nil { ++ return false ++ } ++ i += 4 ++ if n >= 0xdc00 && n <= 0xdfff { ++ return false ++ } ++ if n < 0xd800 || n > 0xdbff { ++ continue ++ } ++ if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' { ++ return false ++ } ++ low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16) ++ if e != nil || low < 0xdc00 || low > 0xdfff { ++ return false ++ } ++ i += 6 ++ } ++ return !in ++} ++ ++func pythonNumber(n json.Number) (string, error) { ++ s := string(n) ++ if !strings.ContainsAny(s, ".eE") { ++ i, ok := new(big.Int).SetString(s, 10) ++ if !ok { ++ return "", errJSON ++ } ++ return i.String(), nil ++ } ++ f, e := strconv.ParseFloat(s, 64) ++ if e != nil || math.IsInf(f, 0) || math.IsNaN(f) { ++ return "", errJSON ++ } ++ // Python repr uses shortest round-trip digits, fixed form for exponents ++ // -4..15, a mandatory .0 for integral floats, and two exponent digits. ++ sci := strconv.FormatFloat(f, 'e', -1, 64) ++ at := strings.IndexByte(sci, 'e') ++ exp, _ := strconv.Atoi(sci[at+1:]) ++ if exp >= -4 && exp < 16 { ++ s = strconv.FormatFloat(f, 'f', -1, 64) ++ if !strings.Contains(s, ".") { ++ s += ".0" ++ } ++ return s, nil ++ } ++ sign := "+" ++ if exp < 0 { ++ sign = "-" ++ exp = -exp ++ } ++ digits := strconv.Itoa(exp) ++ if len(digits) < 2 { ++ digits = "0" + digits ++ } ++ return sci[:at] + "e" + sign + digits, nil ++} ++ ++func quote(s string) string { ++ var b strings.Builder ++ b.WriteByte('"') ++ for _, r := range s { ++ switch r { ++ case '"', '\\': ++ b.WriteByte('\\') ++ b.WriteRune(r) ++ case '\b': ++ b.WriteString(`\b`) ++ case '\f': ++ b.WriteString(`\f`) ++ case '\n': ++ b.WriteString(`\n`) ++ case '\r': ++ b.WriteString(`\r`) ++ case '\t': ++ b.WriteString(`\t`) ++ default: ++ if r < 32 { ++ b.WriteString(`\u00`) ++ const h = "0123456789abcdef" ++ b.WriteByte(h[byte(r)>>4]) ++ b.WriteByte(h[byte(r)&15]) ++ } else { ++ b.WriteRune(r) ++ } ++ } ++ } ++ b.WriteByte('"') ++ return b.String() ++} ++ ++func canonical(v any) (string, error) { ++ switch x := v.(type) { ++ case nil: ++ return "null", nil ++ case bool: ++ if x { ++ return "true", nil ++ } ++ return "false", nil ++ case string: ++ return quote(x), nil ++ case json.Number: ++ return pythonNumber(x) ++ case []any: ++ a := make([]string, len(x)) ++ for i, v := range x { ++ s, e := canonical(v) ++ if e != nil { ++ return "", e ++ } ++ a[i] = s ++ } ++ return "[" + strings.Join(a, ",") + "]", nil ++ case map[string]any: ++ keys := make([]string, 0, len(x)) ++ for k := range x { ++ keys = append(keys, k) ++ } ++ sort.Strings(keys) ++ a := make([]string, 0, len(keys)) ++ for _, k := range keys { ++ s, e := canonical(x[k]) ++ if e != nil { ++ return "", e ++ } ++ a = append(a, quote(k)+":"+s) ++ } ++ return "{" + strings.Join(a, ",") + "}", nil ++ default: ++ return "", errJSON ++ } ++} ++ ++// CanonicalBinding reproduces approval_engine.binding.binding_digest over the ++// five native act fields. Extra binding metadata is excluded by that contract. ++func CanonicalBinding(raw []byte) ([]byte, string, error) { ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return nil, "", e ++ } ++ m, ok := v.(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act := map[string]any{} ++ for _, k := range []string{"action", "actor", "principal", "purpose"} { ++ s, ok := m[k].(string) ++ if !ok || s == "" { ++ return nil, "", errJSON ++ } ++ act[k] = s ++ } ++ target, ok := m["target"].(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act["target"] = target ++ s, e := canonical(act) ++ if e != nil { ++ return nil, "", e ++ } ++ sum := sha256.Sum256([]byte(s)) ++ return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil ++} +diff --git a/third_party/approvalfact/claim.go b/third_party/approvalfact/claim.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/claim.go +@@ -0,0 +1,170 @@ ++package approvalfact ++ ++import ( ++ "encoding/json" ++ "errors" ++ "regexp" ++ "strconv" ++ "time" ++) ++ ++var errClaim = errors.New("Approval fact does not match required claim") ++var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) ++var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) ++var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`) ++ ++// Expected contains independently owner-derived native act bytes and the actual ++// Access approval-free digest. Marshaling an already float64-decoded Go request ++// is not an authoritative reconstruction of the native act. ++type Expected struct { ++ ApprovalID string ++ NativeBindingJSON []byte ++ PDPDigest string ++} ++ ++// Fact is validated data shape/correspondence, NOT authenticated provider origin ++// or execution authority. Access must wrap it only after its own source admission. ++type Fact struct { ++ raw []byte ++ stable string ++ digest string ++ observed, timeLimit, validatedAt time.Time ++} ++ ++func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) } ++func (f Fact) ObservedAt() time.Time { return f.observed } ++func (f Fact) Deadline() time.Time { return f.timeLimit } ++func (f Fact) NativeDigest() string { return f.digest } ++ ++func members(m map[string]any, required, optional []string) bool { ++ allowed := map[string]bool{} ++ for _, k := range required { ++ allowed[k] = true ++ if _, ok := m[k]; !ok { ++ return false ++ } ++ } ++ for _, k := range optional { ++ allowed[k] = true ++ } ++ for k := range m { ++ if !allowed[k] { ++ return false ++ } ++ } ++ return true ++} ++func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok } ++func stamp(v any) (time.Time, error) { ++ s, ok := v.(string) ++ if !ok || !timePattern.MatchString(s) { ++ return time.Time{}, errClaim ++ } ++ t, e := time.Parse(time.RFC3339Nano, s) ++ if e != nil { ++ return time.Time{}, errClaim ++ } ++ return t, nil ++} ++ ++// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim ++// at a caller-captured instant, using the existing Approval producer semantics. ++// It has no I/O, policy evaluator, credentials, cache, or consume mutation. ++func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) { ++ if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) { ++ return Fact{}, errClaim ++ } ++ _, want, e := CanonicalBinding(expected.NativeBindingJSON) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ m, ok := object(v) ++ if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) { ++ return Fact{}, errClaim ++ } ++ if y, ok := m["yields_to"]; ok { ++ if _, ok = y.(string); !ok { ++ return Fact{}, errClaim ++ } ++ } ++ if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" { ++ return Fact{}, errClaim ++ } ++ b, ok := object(m["binding"]) ++ if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) { ++ return Fact{}, errClaim ++ } ++ if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true { ++ return Fact{}, errClaim ++ } ++ text, e := canonical(b) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ _, got, e := CanonicalBinding([]byte(text)) ++ if e != nil || got != want { ++ return Fact{}, errClaim ++ } ++ f, ok := object(m["freshness"]) ++ if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) { ++ return Fact{}, errClaim ++ } ++ observed, e := stamp(f["observed_at"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ notAfter, e := stamp(f["not_after"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ ttl, ok := f["ttl_seconds"].(json.Number) ++ if !ok { ++ return Fact{}, errClaim ++ } ++ seconds, e := strconv.ParseInt(string(ttl), 10, 64) ++ if e != nil || seconds < 1 { ++ return Fact{}, errClaim ++ } ++ // Bound arithmetic to time.Duration's representable seconds, not a new ++ // semantic freshness grant or configured TTL. Unsupported extremes refuse. ++ if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) { ++ return Fact{}, errClaim ++ } ++ validity, ok := object(m["validity"]) ++ if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) { ++ return Fact{}, errClaim ++ } ++ before, e := stamp(validity["not_before"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ expires, e := stamp(validity["expires_at"]) ++ if e != nil || now.Before(before) || !now.Before(expires) { ++ return Fact{}, errClaim ++ } ++ limit := notAfter ++ if expires.Before(limit) { ++ limit = expires ++ } ++ delete(m, "freshness") ++ delete(m, "yields_to") ++ stable, e := canonical(m) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil ++} ++ ++// MatchObserved requires independently validated original/fetched facts at the ++// same captured instant and exact nonfreshness authority correspondence. It ++// never rewrites the original request or extends its original deadline. ++func MatchObserved(submitted, observed Fact) error { ++ if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable { ++ return errClaim ++ } ++ return nil ++} +diff --git a/third_party/approvalfact/go.mod b/third_party/approvalfact/go.mod +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/go.mod +@@ -0,0 +1,3 @@ ++module github.com/netkingdom/approval-engine/adapters/go/approvalfact ++ ++go 1.26.7 +diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -237,3 +237,36 @@ + and canonical vectors; B3 additionally needs exact source/normalization numeric + representation and full trusted provider joins. No current policy false gate, + production trust, native deployment or protected effect is enabled by B1. ++ ++2026-10-16 B2/B3 source successor preparation (separate from frozen B1): the ++isolated v2 source path now joins the fixed authenticated Approval reader, ++unchanged Approval-owner canonical/fact helper, original/fresh correspondence, ++raw numeric request/registry source retention, same policy evaluator and signed ++source provenance. Disposable actual HTTP and production serve subprocess prove ++synthetic signed ALLOW and fail-closed negative joins with exact response/record ++parity. See `docs/trusted-service-task-source.md`. T06 remains `progress`: this ++source proposal requires independent cross-owner review and full consumer/Platform ++successor integration, then separately admitted runtime custody, source trust, ++image provenance and rollout. No source-only proof establishes those runtime gates. ++ ++ ++2026-10-16 B2/B3 R2 temporal correction (R1 retained): whole-stack review exposed ++that R1's 15s ALLOW and default Approval freshness30 cannot fit the declared ++minimum backendTTL30 plus existing cleanupreserve5. The v2 successor now derives ++exact ALLOW expiry from the immutable operationdeadline, requiring all original/ ++fresh approval/freshness/caller bounds, a conservative source maximum120 and ++remaining declaredTTL+5 after source I/O/evaluation. No consumer/native deadline ++or cleanup guard is relaxed. Max structural budgets that cannot fit refuse. ++The fixture declares backendTTL30/deadline60 and explicit source freshness120; ++owner runtime source freshness configuration and full native integration remain ++separate reviewed gates. See docs/trusted-service-task-source.md. ++ ++2026-10-16 B2/B3 R3 follows actual retained-registry integration failure: subject ++enrichment changed evaluated approval-free digest from the original proposal. ++The existing signed claim-free DENY now explicitly carries v2 approvalfree digest ++(equal evaluated requestdigest), enabling producer-owned preapproval facts under ++the already published enrichment contract. The final original proposal adds only ++the issued claim; registry drift/refreshed facts cannot silently rebind it. ++Retained-registry HTTP and production serve proofs and negative joins pass. ++R3 preserves precise notbefore pending separate Approval-owner timestamp fidelity ++repair; no rounding waiver, copied registry algorithm or empty-registry workaround. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/trusted-source-over-b1-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/trusted-source-over-b1-r2.patch new file mode 100644 index 00000000..dbf9c13d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-b3/trusted-source-over-b1-r2.patch @@ -0,0 +1,2332 @@ +diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml +--- a/.forgejo/workflows/image.yaml ++++ b/.forgejo/workflows/image.yaml +@@ -19,6 +19,7 @@ + - "cmd/**" + - "internal/**" + - "pkg/**" ++ - "third_party/**" + - "examples/**" + - "go.mod" + - "go.sum" +diff --git a/Containerfile b/Containerfile +--- a/Containerfile ++++ b/Containerfile +@@ -1,6 +1,8 @@ + FROM golang:1.26.7-alpine@sha256:28d89ee9cc0ff9fec75c82ca201e6bf7fdf9a679d4b7b24dfa04f2bb766bb468 AS build + WORKDIR /src + COPY go.mod go.sum ./ ++# Portable, unchanged Approval-owner pure data helper; available before download. ++COPY third_party/approvalfact ./third_party/approvalfact + RUN go mod download + COPY . . + RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/access-engine ./cmd/access-engine && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/flex-auth ./cmd/flex-auth +diff --git a/cmd/access-engine/main.go b/cmd/access-engine/main.go +--- a/cmd/access-engine/main.go ++++ b/cmd/access-engine/main.go +@@ -333,6 +333,7 @@ + logPath := fs.String("log", "", "optional JSONL decision log path") + signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file") + signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id") ++ approvalSourceFile := fs.String("approval-source-file", "", "owner fixed Approval HTTPS reader configuration file") + outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log") + emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine") + auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats") +@@ -402,7 +403,19 @@ + if err != nil { + return fail(stderr, err) + } +- mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) ++ var approvalReader *taskexecution.ApprovalReader ++ if engine.IsTrustedServicePilot() { ++ if *signingKeyFile == "" || *approvalSourceFile == "" { ++ return fail(stderr, fmt.Errorf("trusted task source requires signing and fixed Approval reader configuration")) ++ } ++ approvalReader, err = taskexecution.LoadApprovalReader(*approvalSourceFile) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ } else if *approvalSourceFile != "" { ++ return fail(stderr, fmt.Errorf("Approval reader requires exact trusted task source package")) ++ } ++ mux := newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, approvalReader) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -457,6 +470,9 @@ + } + + func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { ++ return newServeMuxWithTaskFacts(engine, authenticator, serviceInputs, nil) ++} ++func newServeMuxWithTaskFacts(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs, approvalReader *taskexecution.ApprovalReader) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -467,12 +483,16 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + if serviceInputs == nil { + http.Error(w, "service input unavailable", http.StatusServiceUnavailable) + return + } +- request, err := taskexecution.DecodeServiceRequest(r.Body) ++ decode := taskexecution.DecodeServiceRequest ++ if engine.IsTrustedServicePilot() { ++ decode = taskexecution.DecodeTrustedServiceRequest ++ } ++ request, err := decode(r.Body) + if err != nil || request.Purpose != "" { + http.Error(w, "invalid service task request", http.StatusBadRequest) + return +@@ -482,7 +502,12 @@ + http.Error(w, "service task input refused", http.StatusForbidden) + return + } +- decision, err := engine.CheckServicePilot(r.Context(), input) ++ var decision api.DecisionEnvelope ++ if engine.IsTrustedServicePilot() { ++ decision, err = engine.CheckTrustedService(r.Context(), input, approvalReader) ++ } else { ++ decision, err = engine.CheckServicePilot(r.Context(), input) ++ } + writeHTTP(w, decision, err) + return + } +@@ -506,7 +531,7 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { +- if engine.IsServicePilot() { ++ if engine.IsTaskPin() { + http.Error(w, "service task batch unavailable", http.StatusForbidden) + return + } +@@ -537,7 +562,7 @@ + // Only the existing exact service pilot package selects this source path. + // This is not an eligibility toggle; successful preparation still only DENYs. + func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) { +- if !engine.IsServicePilot() { ++ if !engine.IsTaskPin() { + return nil, nil + } + if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { +@@ -546,6 +571,9 @@ + reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) + if err != nil { + return nil, err ++ } ++ if engine.IsTrustedServicePilot() { ++ return taskexecution.NewTrustedServiceInputs(reviewer) + } + return taskexecution.NewServicePilotInputs(reviewer) + } +@@ -575,11 +603,15 @@ + } + + func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) { +- store, err := registry.LoadFile(registryPath) ++ pkg, err := policy.LoadAndValidateFile(ctx, policyPath) + if err != nil { + return nil, err + } +- pkg, err := policy.LoadAndValidateFile(ctx, policyPath) ++ load := registry.LoadFile ++ if pkg.Metadata.ID == taskexecution.ServicePilotPackageID && pkg.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion { ++ load = registry.LoadFilePreservingNumbers ++ } ++ store, err := load(registryPath) + if err != nil { + return nil, err + } +diff --git a/cmd/access-engine/trusted_service_http_test.go b/cmd/access-engine/trusted_service_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/trusted_service_http_test.go +@@ -0,0 +1,330 @@ ++package main ++ ++import ( ++ "bytes" ++ "context" ++ "crypto/ed25519" ++ "encoding/base64" ++ "encoding/json" ++ "encoding/pem" ++ "io" ++ "net" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "os/exec" ++ "path/filepath" ++ "reflect" ++ "strings" ++ "testing" ++ "time" ++ ++ "github.com/netkingdom/access-engine/internal/audit" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/decision" ++ "github.com/netkingdom/access-engine/internal/policy" ++ "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++func trustedRequest(t *testing.T, now time.Time) api.CheckRequest { ++ t.Helper() ++ r := pilotRequest(now) ++ r.Subject.Tenant = r.Tenant ++ r.Resource.Tenant = r.Tenant ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1.0") ++ c := r.Context["execution_contract"].(map[string]any) ++ c["operation_id"] = "11111111-2222-4333-8444-555555555555" ++ c["ccr_id"] = "synthetic-change" ++ c["execution_grant_ccr_id"] = "synthetic-grant" ++ hash := "sha256:" + strings.Repeat("1", 64) ++ for _, key := range []string{"ccr_immutable_sha256", "grant_immutable_sha256", "catalog_sha256", "expected_before", "expected_after"} { ++ c[key] = hash ++ } ++ c["backend_budget"] = map[string]any{"ttl_seconds": json.Number("60"), "uses": json.Number("12")} ++ var ops []any ++ for i, method := range []string{"GET", "PUT", "GET", "GET", "PUT", "GET"} { ++ path := "sys/policies/acl/synthetic" ++ if i >= 3 { ++ path = "auth/synthetic/role/synthetic" ++ } ++ ops = append(ops, map[string]any{"method": method, "path": path, "payload_sha256": hash}) ++ } ++ c["operations"] = ops ++ delete(r.Context, "approval") ++ raw, _ := json.Marshal(map[string]any{"action": r.Action, "actor": r.Subject.ID, "principal": "secrets-engine", "purpose": "synthetic source-only proof", "target": r.Resource}) ++ r.Context["purpose"] = "synthetic source-only proof" ++ _, digest, err := approvalfact.CanonicalBinding(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ bindingValue, err := approvalfact.DecodeJSON(raw) ++ if err != nil { ++ t.Fatal(err) ++ } ++ binding := bindingValue.(map[string]any) ++ // PDP profile intentionally retains the published legacy numeric projection. ++ encoded, _ := json.Marshal(r) ++ var legacy api.CheckRequest ++ json.Unmarshal(encoded, &legacy) ++ binding["digest"] = digest ++ binding["pdp_digest"] = api.ApprovalBindingDigest(legacy) ++ binding["pdp_path"] = true ++ binding["human_control"] = true ++ r.Context["approval"] = map[string]any{"schema_version": "0.1", "kind": "approval-claim", "issuer": "approval-engine", "approval_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", "state": "valid", "valid_now": true, "consumed": false, "reason_code": "ok", "binding": binding, "freshness": map[string]any{"observed_at": now.Add(-time.Second).Format(time.RFC3339Nano), "ttl_seconds": json.Number("60"), "not_after": now.Add(59 * time.Second).Format(time.RFC3339Nano)}, "validity": map[string]any{"not_before": now.Add(-time.Hour).Format(time.RFC3339Nano), "expires_at": now.Add(time.Hour).Format(time.RFC3339Nano)}} ++ return r ++} ++func TestTrustedServiceActualHTTPPositiveAndRefusals(t *testing.T) { ++ now := time.Now().UTC() ++ folder := t.TempDir() ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ w.WriteHeader(403) ++ return ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": true, "audiences": []string{"flex-auth"}, "user": map[string]any{"username": taskexecution.ServicePilotPrincipal}}}) ++ })) ++ defer reviewer.Close() ++ ca := filepath.Join(folder, "review-ca") ++ token := filepath.Join(folder, "review-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(token, []byte("synthetic-reviewer"), 0600) ++ original := trustedRequest(t, now) ++ claimRaw, _ := json.Marshal(original.Context["approval"]) ++ mode := "ok" ++ reads := 0 ++ provider := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reads++ ++ if r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Header.Get("Authorization") != "Bearer synthetic-approval-reader" { ++ w.WriteHeader(403) ++ return ++ } ++ if mode == "redirect" { ++ http.Redirect(w, r, reviewer.URL, 302) ++ return ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode { ++ case "duplicate": ++ io.WriteString(w, `{"state":"valid","state":"invalid"}`) ++ case "changed": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["consumed"] = true ++ json.NewEncoder(w).Encode(v) ++ case "expired": ++ v, _ := approvalfact.DecodeJSON(claimRaw) ++ v.(map[string]any)["freshness"].(map[string]any)["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ json.NewEncoder(w).Encode(v) ++ default: ++ w.Write(claimRaw) ++ } ++ })) ++ defer provider.Close() ++ pca := filepath.Join(folder, "approval-ca") ++ ptoken := filepath.Join(folder, "approval-token") ++ os.WriteFile(pca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: provider.Certificate().Raw}), 0600) ++ os.WriteFile(ptoken, []byte("synthetic-approval-reader"), 0440) ++ source, err := taskexecution.NewApprovalReader(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{19}, 32)) ++ engine.SetEnvelopeSigningKey(key, "synthetic-v2") ++ record := filepath.Join(folder, "record.jsonl") ++ engine.SetDecisionLog(audit.NewJSONLDecisionLog(record)) ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, token, ca, map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithTaskFacts(engine, callerauth.Disabled(), inputs, source)) ++ defer server.Close() ++ post := func(request api.CheckRequest) (int, []byte) { ++ t.Helper() ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ body, _ := io.ReadAll(response.Body) ++ return response.StatusCode, body ++ } ++ status, body := post(original) ++ if status != 200 { ++ t.Fatalf("positive status %d: %s", status, body) ++ } ++ envelope, err := sign.DecodeEnvelope(body) ++ if err != nil { ++ t.Fatal(err) ++ } ++ t.Logf("actual HTTP source policy pin: %s %s %s", pkg.Metadata.ID, pkg.Metadata.Version, pkg.Digest()) ++ if envelope.Effect != api.DecisionEffectAllow || sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("not signed ALLOW: %s", body) ++ } ++ if envelope.Provenance.ApprovalObservation == nil || envelope.Provenance.ApprovalObservation.SourceID != "synthetic-approval" || reads != 1 { ++ t.Fatal("missing authenticated observation provenance") ++ } ++ legacyRaw, _ := json.Marshal(original) ++ var legacyRequest api.CheckRequest ++ json.Unmarshal(legacyRaw, &legacyRequest) ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.RequestDigest != api.RequestDigest(legacyRequest) || envelope.Binding.ApprovalBindingDigest != api.ApprovalBindingDigest(legacyRequest) { ++ t.Fatal("published original/full/approval-free digest profile changed") ++ } ++ if !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original context changed") ++ } ++ if envelope.Binding.Resource.Attributes["numeric_fixture"] != json.Number("1.0") { ++ t.Fatal("native numeric kind lost") ++ } ++ recorded, err := os.ReadFile(record) ++ if err != nil { ++ t.Fatal(err) ++ } ++ recordEnvelope, err := sign.DecodeEnvelope(bytes.TrimSpace(recorded)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if !reflect.DeepEqual(recordEnvelope, envelope) { ++ t.Fatal("returned/recorded mismatch") ++ } ++ for _, failure := range []string{"redirect", "duplicate", "changed", "expired"} { ++ t.Run(failure, func(t *testing.T) { ++ mode = failure ++ status, _ := post(original) ++ if status == 200 { ++ t.Fatal("bad source admitted") ++ } ++ }) ++ } ++ mode = "ok" ++ t.Run("original-stale", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, err := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ fresh := r.Context["approval"].(map[string]any)["freshness"].(map[string]any) ++ fresh["observed_at"] = now.Add(-61 * time.Second).Format(time.RFC3339Nano) ++ fresh["not_after"] = now.Add(-time.Second).Format(time.RFC3339Nano) ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("fresh provider rescued stale original") ++ } ++ }) ++ t.Run("native-number-kind", func(t *testing.T) { ++ raw, _ := json.Marshal(original) ++ r, _ := taskexecution.DecodeTrustedServiceRequest(bytes.NewReader(raw)) ++ r.Resource.Attributes["numeric_fixture"] = json.Number("1") ++ status, _ := post(r) ++ if status == 200 { ++ t.Fatal("changed native integer/float authority accepted") ++ } ++ }) ++ after, _ := os.ReadFile(record) ++ if !bytes.Equal(after, recorded) { ++ t.Fatal("refused calls recorded executable envelopes") ++ } ++ // Exercise actual production CLI configuration with no ambient credentials. ++ seedPath := filepath.Join(folder, "cli-seed") ++ os.WriteFile(seedPath, []byte(base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{19}, 32))), 0440) ++ sourcePath := filepath.Join(folder, "approval-source.json") ++ sourceJSON, _ := json.Marshal(taskexecution.ApprovalSourceConfig{SourceID: "synthetic-approval", SourceRevision: "test-source-revision", Origin: provider.URL, StoreTenant: "tenant:platform", CAFile: pca, TokenFile: ptoken}) ++ os.WriteFile(sourcePath, sourceJSON, 0600) ++ registryPath := filepath.Join(folder, "registry.json") ++ os.WriteFile(registryPath, []byte(`{}`), 0600) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-record.jsonl") ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ args := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", token, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedPath, "--envelope-signing-kid", "synthetic-v2", "--approval-source-file", sourcePath} ++ argsJSON, _ := json.Marshal(args) ++ child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$") ++ child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)} ++ child.Stdout = io.Discard ++ child.Stderr = io.Discard ++ if err := child.Start(); err != nil { ++ t.Fatal(err) ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ response, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ response.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("trusted CLI not ready") ++ } ++ raw, _ := json.Marshal(original) ++ req, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ req.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ response, err := http.DefaultClient.Do(req) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer response.Body.Close() ++ wire, _ := io.ReadAll(response.Body) ++ cliEnvelope, err := sign.DecodeEnvelope(wire) ++ if response.StatusCode != 200 || err != nil || cliEnvelope.Effect != api.DecisionEffectAllow || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-v2": key.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatalf("CLI did not return signed trusted ALLOW: %s", wire) ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("CLI response/readback record mismatch") ++ } ++ if !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("CLI changed original numeric context") ++ } ++ ++} ++ ++func TestTrustedServiceMissingAndGenericAdmissionRefuse(t *testing.T) { ++ pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution-v2", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), pkg) ++ if err != nil { ++ t.Fatal(err) ++ } ++ request := trustedRequest(t, time.Now().UTC()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic list bypass") ++ } ++ if _, err := engine.CheckTrustedService(context.Background(), taskexecution.ServiceInput{}, nil); err == nil { ++ t.Fatal("missing typed input and fixed source accepted") ++ } ++ recorder := httptest.NewRecorder() ++ raw, _ := json.Marshal(request) ++ newServeMux(engine).ServeHTTP(recorder, httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw))) ++ if recorder.Code != 503 { ++ t.Fatal("unwired typed path accepted") ++ } ++} +diff --git a/docs/trusted-service-task-source.md b/docs/trusted-service-task-source.md +new file mode 100644 +--- /dev/null ++++ b/docs/trusted-service-task-source.md +@@ -0,0 +1,62 @@ ++# Trusted service task source proposal (v2) ++ ++The separate `secrets-engine.task-execution.service-pilot` v2 source package can ++return ALLOW only through the existing `/v1/check` typed task path. The original ++v1 package remains unchanged and DENY-only. No chart, runtime, source identity, ++credential custody or native execution admission is enabled by these sources. ++ ++The server requires enforced TokenReview, audience `flex-auth`, the exact retained ++Secrets service-account mapping, mounted envelope signing configuration and an ++owner-installed `--approval-source-file`. This public configuration contains ++`source_id`, `source_revision`, fixed HTTPS `origin`, `store_tenant` exactly ++`tenant:platform`, absolute `ca_file` and absolute mounted `token_file`. The ++credential must independently carry existing Approval `approval:read` authority ++for that store. Source labels describe the owner-admitted configuration; they do ++not perform its admission. No key or token value belongs in the configuration. ++ ++The reader sends one authenticated GET to the fixed ++`/v1/approvals/{UUID}/claim` endpoint, accepts only bounded strict JSON, uses only ++the configured CA, refuses redirects and environmental proxies, and has a total ++three-second HTTP timeout. It neither creates nor approves nor consumes. Mounted ++bearer files must be regular, bounded and have no group write or other access; ++0440 is supported. Public configuration/CA files also refuse group/other writes. ++Errors do not include response bodies, credentials or transport diagnostics. ++ ++The unchanged Approval-owner pure data helper is copied with exact source pins ++under `third_party/approvalfact`. The fixed reader establishes provider origin; ++the helper establishes schema and semantic correspondence. Both original and ++fresh claims must independently be currently valid, unconsumed, human-controlled ++PDP-path claims for the exact UUID, native five-field act and Access approval-free ++request digest. Nonfreshness authority must match exactly. The freshly fetched ++claim never replaces the submitted claim/context or renews original freshness. ++ ++The native act derives from the normalized subject/resource, fixed service ++principal, action and nonempty `context.purpose`. The HTTP decoder and task registry ++loader preserve raw JSON numeric representations. Registry facts loaded through ++the old lossy profile cannot enter the trusted path. Only the Approval-owner ++helper canonicalizes native act data. Existing PDP full/submitted/approval-free ++digests retain their published numeric profile through a digest-only legacy ++projection; the signed structured binding retains the original numeric kinds. ++Inputs unsupported by that existing PDP profile refuse; this proposal does not ++weaken any consumer numeric admission or invent numeric restrictions. ++ ++The same existing policy evaluator decides after those joins. Generic check, ++batch and list entry points refuse task execution across all policy names. No ++request-selected verified flag, provider origin or policy enable switch bypasses ++the typed path. Missing approval can receive signed DENY for preapproval binding; ++ALLOW requires authenticated matching original/fresh claims. Its expiry is capped ++by policy TTL, caller expiry, operation deadline and both claims' freshness and ++validity, rechecked after I/O/evaluation. Existing source-owned signing covers the ++original binding and new `provenance.approval_observation` receipt. The receipt ++contains source/configuration identity, exact fetched-byte digest, native digest ++and observation/validation/deadline timestamps. JSONL readback and outbox replay ++preserve producer numeric signatures; no second signing algorithm is introduced. ++ ++Packaging must copy the local owner helper before `go mod download`. Existing ++ordinary image CI triggers include `third_party/**` so a helper update cannot ++silently leave the old image. No build publication is invoked by this preparation. ++Rollback selects the unchanged v1 package or removes the v2 admission; generic ++fallback remains refused. Rotating/withdrawing source or signing trust is a ++separate owner-admitted runtime operation. The earlier Platform positive proof ++remains evidence only at its old ec21518e Access pin; its generic synthetic policy ++is deliberately incompatible with the successor guarded source. +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_fixtures.yaml +@@ -0,0 +1,8 @@ ++- id: unsigned-service-caller-not-authority ++ request: ++ tenant: tenant:platform ++ subject: {id: secrets-engine, type: service} ++ action: execute-ccr ++ resource: {id: catalog:test, type: secret-catalog-lane, system: secrets-engine} ++ context: {human_control: true, runtime_admission: true, verified: true} ++ expect: {effect: deny, reason: service_execution_contract_unmatched} +diff --git a/examples/secrets-engine-service-task-execution-v2/policy_package.md b/examples/secrets-engine-service-task-execution-v2/policy_package.md +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/policy_package.md +@@ -0,0 +1,112 @@ ++--- ++id: secrets-engine.task-execution.service-pilot ++name: Secrets admitted service task execution ++namespace: secrets-engine:secret-catalog-lane ++version: v2 ++status: proposed ++package: flexauth.secrets_engine.service_task_execution ++allow_ttl: 15s ++actions: [execute-ccr] ++owner: team:platform-security ++fixtures: [policy_fixtures.yaml] ++caring: ++ profile: caring-0.4.0-rc2 ++ enforce: false ++activation: ++ mode: local ++--- ++ ++# Trusted service task source successor ++ ++This source package is evaluated exclusively by the typed v2 HTTP path. That ++path requires exact TokenReview caller mapping, immutable numeric source ++representations, a fixed authenticated Approval reader, separate original and ++fresh claim validation and native/PDP correspondence before policy evaluation. ++Generic check, batch and list routes refuse task execution. Selecting this source ++package alone cannot bypass those joins. No deployment or trust admission is ++established by this package. The v1 package remains unchanged and DENY-only. ++ ++```rego ++default decision := {"effect": "deny", "reason": "service_execution_contract_unmatched"} ++ ++decision := {"effect": "allow", "reason": "exact_task_execution_matched"} { ++ candidate_matches ++} ++ ++hash_ok(x) { is_string(x); regex.match("^sha256:[0-9a-f]{64}$", x) } ++ ++candidate_matches { ++ input.action == "execute-ccr" ++ input.tenant == "tenant:platform" ++ input.subject.id == "secrets-engine" ++ input.subject.type == "service" ++ input.resource.system == "secrets-engine" ++ input.resource.type == "secret-catalog-lane" ++ input.context.human_control == true ++ c := input.context.execution_contract ++ c.contract_version == "SECRETS-EXEC-20261011-v1" ++ c.operation == "metadata-apply-ccr" ++ c.execution_actor == "service:secrets-engine" ++ regex.match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", c.operation_id) ++ c.ccr_id != c.execution_grant_ccr_id ++ hash_ok(c.ccr_immutable_sha256) ++ hash_ok(c.grant_immutable_sha256) ++ hash_ok(c.catalog_sha256) ++ hash_ok(c.expected_before) ++ hash_ok(c.expected_after) ++ c.backend_budget.ttl_seconds <= 120 ++ c.backend_budget.ttl_seconds >= 30 ++ c.backend_budget.uses == 12 ++ count(c.operations) == 6 ++ c.operations[0].method == "GET" ++ c.operations[1].method == "PUT" ++ c.operations[2].method == "GET" ++ c.operations[3].method == "GET" ++ c.operations[4].method == "PUT" ++ c.operations[5].method == "GET" ++ regex.match("^sys/policies/acl/[A-Za-z0-9_-]+$", c.operations[0].path) ++ regex.match("^auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+$", c.operations[3].path) ++ c.operations[0].path == c.operations[1].path ++ c.operations[1].path == c.operations[2].path ++ c.operations[3].path == c.operations[4].path ++ c.operations[4].path == c.operations[5].path ++ every_operation_hash_valid ++ b := input.context.service_actor_binding ++ count(b) == 5 ++ b.version == "proposed-service-task-caller/v1" ++ b.transport_principal == "system:serviceaccount:secrets-engine:secrets-engine" ++ b.subject_id == input.subject.id ++ b.subject_type == "service" ++ b.tenant == input.tenant ++ not input.context.task_execution_projection ++ not input.context.initiator ++ not input.context.represented_principal ++ not input.context.delegation ++ input.resource.attributes.stage == "prod" ++ time.parse_rfc3339_ns(c.operation_deadline) > time.now_ns() ++ input.context.approval.schema_version == "0.1" ++ input.context.approval.kind == "approval-claim" ++ input.context.approval.issuer == "approval-engine" ++ input.context.approval.valid_now == true ++ input.context.approval.consumed == false ++ input.context.approval.reason_code == "ok" ++ input.context.approval.binding.pdp_path == true ++ hash_ok(input.context.approval.binding.pdp_digest) ++ time.parse_rfc3339_ns(input.context.approval.freshness.not_after) > time.now_ns() ++ time.parse_rfc3339_ns(input.context.approval.validity.expires_at) >= time.parse_rfc3339_ns(c.operation_deadline) ++ input.context.approval.binding.human_control == true ++} ++ ++every_operation_hash_valid { ++ count([x | x := input.context.execution_contract.operations[_]; hash_ok(x.payload_sha256)]) == 6 ++} ++``` ++ ++```rego test ++import data.flexauth.secrets_engine.service_task_execution ++ ++test_unsigned_flags_cannot_enable { ++ service_task_execution.decision.effect == "deny" ++ with input as {"context": {"runtime_admission": true, "verified": true}} ++} ++``` +diff --git a/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine-service-task-execution-v2/resource_manifest.yaml +@@ -0,0 +1,6 @@ ++# Synthetic source-validation fixture only; no estate registry admission. ++resources: ++ - id: catalog:synthetic ++ system: secrets-engine ++ type: secret-catalog-lane ++ attributes: {stage: prod} +diff --git a/go.mod b/go.mod +--- a/go.mod ++++ b/go.mod +@@ -3,6 +3,7 @@ + go 1.26.7 + + require ( ++ github.com/netkingdom/approval-engine/adapters/go/approvalfact v0.0.0 + github.com/open-policy-agent/opa v1.4.0 + gopkg.in/yaml.v3 v3.0.1 + ) +@@ -39,3 +40,5 @@ + google.golang.org/protobuf v1.36.6 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect + ) ++ ++replace github.com/netkingdom/approval-engine/adapters/go/approvalfact => ./third_party/approvalfact +diff --git a/internal/audit/log.go b/internal/audit/log.go +--- a/internal/audit/log.go ++++ b/internal/audit/log.go +@@ -8,6 +8,7 @@ + "path/filepath" + "sync" + ++ "github.com/netkingdom/access-engine/internal/sign" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -67,8 +68,8 @@ + if scanner.Text() == "" { + continue + } +- var decision api.DecisionEnvelope +- if err := json.Unmarshal(scanner.Bytes(), &decision); err != nil { ++ decision, err := sign.DecodeEnvelope(scanner.Bytes()) ++ if err != nil { + return nil, fmt.Errorf("unmarshal decision log line %d: %w", len(decisions)+1, err) + } + decisions = append(decisions, decision) +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -158,6 +158,10 @@ + func (e *Engine) IsServicePilot() bool { + return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion + } ++func (e *Engine) IsTrustedServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.TrustedServicePilotPackageVersion ++} ++func (e *Engine) IsTaskPin() bool { return e.IsServicePilot() || e.IsTrustedServicePilot() } + + // ErrServicePilotUnavailable withholds any result that is not this B1 slice's + // signed DENY. Provider joins and executable eligibility remain unimplemented. +@@ -189,7 +193,7 @@ + // Check evaluates one subject/action/resource request. The decision is + // released only after its required record commits (GH-DEC-2026-024). + func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return api.DecisionEnvelope{}, ErrServicePilotUnavailable + } + decision, err := e.decide(ctx, request) +@@ -205,7 +209,7 @@ + // BatchCheck evaluates one subject/action/context tuple against resources in + // request order, and commits the whole batch in one durable write. + func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) +@@ -232,7 +236,7 @@ + + // ListAllowed evaluates candidate resources and returns only allow decisions. + func (e *Engine) ListAllowed(ctx context.Context, request ListAllowedRequest) ([]api.DecisionEnvelope, error) { +- if e.IsServicePilot() || request.Action == "execute-ccr" { ++ if e.IsTaskPin() || request.Action == "execute-ccr" { + return nil, ErrServicePilotUnavailable + } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) +diff --git a/internal/decision/trusted_service.go b/internal/decision/trusted_service.go +new file mode 100644 +--- /dev/null ++++ b/internal/decision/trusted_service.go +@@ -0,0 +1,170 @@ ++package decision ++ ++import ( ++ "context" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/callerauth" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/internal/taskexecution" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "time" ++) ++ ++// legacyDigestProjection preserves the published PDP digest profile while the ++// signed structured binding retains original numeric kinds. It is used only ++// for digest material, never to reconstruct an Approval-native act. ++func legacyDigestProjection(request api.CheckRequest) (api.CheckRequest, error) { ++ raw, err := json.Marshal(request) ++ if err != nil { ++ return api.CheckRequest{}, err ++ } ++ var projected api.CheckRequest ++ err = json.Unmarshal(raw, &projected) ++ return projected, err ++} ++func nativeServiceAct(request api.CheckRequest) ([]byte, error) { ++ purpose, ok := request.Context["purpose"].(string) ++ if !ok || purpose == "" || request.Subject.ID != "secrets-engine" || request.Subject.Type != "service" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ if principal, exists := request.Subject.Attributes["principal"]; exists && principal != "secrets-engine" { ++ return nil, taskexecution.ErrServiceInput ++ } ++ // All map numbers originate from the number-preserving request/registry ++ // profile. This raw draft is canonicalized only by the Approval owner helper. ++ return json.Marshal(map[string]any{"action": request.Action, "actor": request.Subject.ID, "principal": "secrets-engine", "purpose": purpose, "target": request.Resource}) ++} ++func (e *Engine) signTaskEnvelope(envelope *api.DecisionEnvelope) error { ++ e.mu.RLock() ++ key, kid := e.signingKey, e.signingKID ++ e.mu.RUnlock() ++ if len(key) == 0 { ++ return ErrServicePilotUnavailable ++ } ++ return sign.Sign(envelope, key, kid) ++} ++ ++// CheckTrustedService is the only v2 task evaluation path. Its source reader ++// and immutable service input are server-created, not HTTP validation flags. ++// It never consumes an approval or causes a protected backend effect. ++func (e *Engine) CheckTrustedService(ctx context.Context, input taskexecution.ServiceInput, source *taskexecution.ApprovalReader) (api.DecisionEnvelope, error) { ++ if !e.IsTrustedServicePilot() || !input.NumberPreserved() || !source.Configured() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now := e.now() ++ submitted, err := input.RequestAt(now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ normalized, facts := e.normalizeRequest(submitted) ++ if (facts.resourceFound || facts.subjectFound) && !e.store.NumbersPreserved() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ original, err := legacyDigestProjection(submitted) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ evaluated, err := legacyDigestProjection(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ ctx = callerauth.WithRecord(ctx, input.Caller()) ++ var approvalEvidence *api.ApprovalObservationProvenance ++ deadline := input.Caller().NotAfter ++ contract := submitted.Context["execution_contract"].(map[string]any) ++ operationEnd, err := time.Parse(time.RFC3339Nano, contract["operation_deadline"].(string)) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrServiceInput ++ } ++ if operationEnd.Before(deadline) { ++ deadline = operationEnd ++ } ++ carried, hasClaim := submitted.Context[api.ApprovalContextKey] ++ if hasClaim { ++ claim, ok := carried.(map[string]any) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ id, ok := claim["approval_id"].(string) ++ if !ok { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ native, err := nativeServiceAct(normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ originalRaw, err := json.Marshal(carried) ++ if err != nil { ++ return api.DecisionEnvelope{}, taskexecution.ErrApprovalSource ++ } ++ observed, err := source.Observe(ctx, id) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ // One validation instant AFTER network I/O; a fresh read cannot rescue the ++ // original carried claim's freshness, source expiry or operation deadline. ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ verified, err := observed.Validate(originalRaw, approvalfact.Expected{ApprovalID: id, NativeBindingJSON: native, PDPDigest: api.ApprovalBindingDigest(evaluated)}, now) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ factEnd, err := verified.Deadline() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if factEnd.Before(deadline) { ++ deadline = factEnd ++ } ++ approvalEvidence, err = verified.Provenance() ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ } ++ expectation, err := e.policy.EvaluatePreservingNumbers(ctx, normalized) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if !hasClaim && expectation.Effect == api.DecisionEffectAllow { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ now = e.now() ++ if _, err := input.RequestAt(now); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope := e.envelope(ctx, normalized, submitted, expectation, facts) ++ envelope.Binding.RequestDigest = api.RequestDigest(evaluated) ++ envelope.Binding.SubmittedRequestDigest = api.RequestDigest(original) ++ if hasClaim { ++ envelope.Binding.ApprovalBindingDigest = api.ApprovalBindingDigest(evaluated) ++ } ++ envelope.Provenance.ApprovalObservation = approvalEvidence ++ if envelope.Effect == api.DecisionEffectAllow { ++ if approvalEvidence == nil || envelope.Lifetime == nil || !deadline.After(now) { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ end, err := time.Parse(time.RFC3339, envelope.Lifetime.ExpiresAt) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if deadline.Before(end) { ++ end = deadline ++ } ++ // Floor expiry to whole seconds, never extend an authoritative deadline. ++ end = end.Truncate(time.Second) ++ if !end.After(now) { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ envelope.Lifetime.ExpiresAt = end.UTC().Format(time.RFC3339) ++ } ++ if err := e.signTaskEnvelope(&envelope); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} +diff --git a/internal/emission/delivery.go b/internal/emission/delivery.go +--- a/internal/emission/delivery.go ++++ b/internal/emission/delivery.go +@@ -112,7 +112,9 @@ + return entry{}, 0, err + } + var e entry +- if err := json.Unmarshal(line, &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(line)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return entry{}, 0, err + } + if e.Seq != o.cursor.Seq+1 || e.Event.Source != o.source { +diff --git a/internal/emission/numeric_replay_test.go b/internal/emission/numeric_replay_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/emission/numeric_replay_test.go +@@ -0,0 +1,53 @@ ++package emission ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/internal/sign" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestOutboxReplayPreservesProducerNumericSignature(t *testing.T) { ++ folder := t.TempDir() ++ outbox, err := Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{21}, 32)) ++ envelope := api.DecisionEnvelope{ID: "decision:synthetic-numeric", Effect: api.DecisionEffectDeny, Resource: api.ResourceRef{ID: "synthetic", Type: "tenant", System: "tenant-engine"}} ++ envelope.Binding = &api.DecisionBinding{Context: map[string]any{"float": json.Number("1.0"), "negative_zero": json.Number("-0.0"), "integer": json.Number("9007199254740993"), "exponent": json.Number("1e-07")}} ++ if err := sign.Sign(&envelope, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ if err := outbox.Append(envelope); err != nil { ++ t.Fatal(err) ++ } ++ outbox.Close() ++ outbox, err = Open(folder, "synthetic-source") ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer outbox.Close() ++ entry, _, err := outbox.nextEntry() ++ if err != nil { ++ t.Fatal(err) ++ } ++ wire, err := json.Marshal(entry.Event.Data) ++ if err != nil { ++ t.Fatal(err) ++ } ++ replayed, err := sign.DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := sign.Verify(replayed, map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)}); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := sign.CanonicalBytes(envelope) ++ after, _ := sign.CanonicalBytes(replayed) ++ if !bytes.Equal(before, after) { ++ t.Fatal("replayed signed bytes changed") ++ } ++} +diff --git a/internal/emission/outbox.go b/internal/emission/outbox.go +--- a/internal/emission/outbox.go ++++ b/internal/emission/outbox.go +@@ -136,7 +136,9 @@ + return os.Truncate(o.path, o.size) + } + var e entry +- if err := json.Unmarshal(data[good:good+end], &e); err != nil { ++ decoder := json.NewDecoder(bytes.NewReader(data[good : good+end])) ++ decoder.UseNumber() ++ if err := decoder.Decode(&e); err != nil { + return fmt.Errorf("outbox line %d is corrupt: %w", line, err) + } + if e.Seq != o.seq+1 || e.Event.Source != o.source { +diff --git a/internal/policy/package.go b/internal/policy/package.go +--- a/internal/policy/package.go ++++ b/internal/policy/package.go +@@ -1,6 +1,7 @@ + package policy + + import ( ++ "bytes" + "context" + "encoding/json" + "fmt" +@@ -375,7 +376,25 @@ + } + + func (p *Package) evaluateDecision(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, false) ++} ++ ++// EvaluatePreservingNumbers feeds the same evaluator the task profile's exact ++// numeric values. Ordinary evaluation behavior and canonical contracts stay put. ++func (p *Package) EvaluatePreservingNumbers(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) { ++ return p.evaluateDecisionProfile(ctx, request, true) ++} ++func (p *Package) evaluateDecisionProfile(ctx context.Context, request api.CheckRequest, preserve bool) (api.DecisionExpectation, error) { + input, err := toRegoInput(request) ++ if preserve { ++ raw, e := json.Marshal(request) ++ if e != nil { ++ return api.DecisionExpectation{}, e ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ err = decoder.Decode(&input) ++ } + if err != nil { + return api.DecisionExpectation{}, err + } +diff --git a/internal/registry/numeric_source_test.go b/internal/registry/numeric_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/registry/numeric_source_test.go +@@ -0,0 +1,36 @@ ++package registry ++ ++import ( ++ "encoding/json" ++ "os" ++ "path/filepath" ++ "testing" ++) ++ ++func TestTaskRegistryNumericSourceAndStrictDecode(t *testing.T) { ++ path := filepath.Join(t.TempDir(), "snapshot.json") ++ raw := []byte(`{"resource_manifests":[{"id":"synthetic","system":"secrets-engine","resources":[{"id":"catalog:synthetic","type":"secret-catalog-lane","attributes":{"float":1.0,"integer":9007199254740993,"negative_zero":-0.0}}]}]}`) ++ os.WriteFile(path, raw, 0600) ++ store, err := LoadFilePreservingNumbers(path) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if !store.NumbersPreserved() { ++ t.Fatal("missing source profile") ++ } ++ resource, ok := store.Resource("secrets-engine", "catalog:synthetic") ++ if !ok { ++ t.Fatal("missing resource") ++ } ++ for k, v := range map[string]string{"float": "1.0", "integer": "9007199254740993", "negative_zero": "-0.0"} { ++ if resource.Attributes[k] != json.Number(v) { ++ t.Fatalf("lost native representation: %s", k) ++ } ++ } ++ for _, raw := range []string{`{} {}`, `{"subjects":[],"subjects":[]}`, `{"unknown":true}`} { ++ os.WriteFile(path, []byte(raw), 0600) ++ if _, err := LoadFilePreservingNumbers(path); err == nil { ++ t.Fatal("invalid authoritative registry accepted") ++ } ++ } ++} +diff --git a/internal/registry/store.go b/internal/registry/store.go +--- a/internal/registry/store.go ++++ b/internal/registry/store.go +@@ -1,6 +1,7 @@ + package registry + + import ( ++ "bytes" + "encoding/json" + "fmt" + "os" +@@ -8,10 +9,12 @@ + "sort" + + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + ) + + // Store is a deterministic local registry for standalone development and tests. + type Store struct { ++ numbersPreserved bool + systems map[string]api.ProtectedSystemManifest + resourceManifests map[string]api.ResourceManifest + resources map[string]api.Resource +@@ -66,6 +69,32 @@ + return store, nil + } + ++// LoadFilePreservingNumbers is the task-source profile: owner registry numeric ++// representations stay intact for native act correspondence. Ordinary loaders ++// and their existing snapshot/digest behavior remain unchanged. ++func LoadFilePreservingNumbers(path string) (*Store, error) { ++ raw, err := os.ReadFile(path) ++ if err != nil { ++ return nil, err ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, err ++ } ++ var snapshot Snapshot ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&snapshot); err != nil { ++ return nil, err ++ } ++ store := NewStore() ++ if err := store.ApplySnapshot(snapshot); err != nil { ++ return nil, err ++ } ++ store.numbersPreserved = true ++ return store, nil ++} ++ + // SaveFile writes the store as deterministic pretty-printed JSON. + func (s *Store) SaveFile(path string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { +@@ -380,3 +409,6 @@ + } + return out + } ++ ++// NumbersPreserved reports the source profile of file-backed registry facts. ++func (s *Store) NumbersPreserved() bool { return s != nil && s.numbersPreserved } +diff --git a/internal/sign/numeric_decode_test.go b/internal/sign/numeric_decode_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/sign/numeric_decode_test.go +@@ -0,0 +1,54 @@ ++package sign ++ ++import ( ++ "bytes" ++ "crypto/ed25519" ++ "encoding/json" ++ "github.com/netkingdom/access-engine/pkg/api" ++ "testing" ++) ++ ++func TestProducerNumericEnvelopeDecodeCompatibility(t *testing.T) { ++ key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{11}, 32)) ++ keys := map[string]ed25519.PublicKey{"synthetic-numeric": key.Public().(ed25519.PublicKey)} ++ for _, raw := range []string{`1`, `1.0`, `-0.0`, `1e-07`, `1e+20`, `9007199254740993`, `1.7976931348623157e308`, `5e-324`, `true`, `"<>&\u2028\u2029 cafĂ©"`, `[1,1.0,-0.0]`, `{"nested":1.0}`} { ++ t.Run(raw, func(t *testing.T) { ++ var value any ++ decoder := json.NewDecoder(bytes.NewBufferString(raw)) ++ decoder.UseNumber() ++ if err := decoder.Decode(&value); err != nil { ++ t.Fatal(err) ++ } ++ original := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"numeric": value}}} ++ if err := Sign(&original, key, "synthetic-numeric"); err != nil { ++ t.Fatal(err) ++ } ++ wire, _ := json.Marshal(original) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if err := Verify(decoded, keys); err != nil { ++ t.Fatal(err) ++ } ++ before, _ := CanonicalBytes(original) ++ after, _ := CanonicalBytes(decoded) ++ if !bytes.Equal(before, after) { ++ t.Fatal("producer canonical bytes changed") ++ } ++ }) ++ } ++ // Existing float64 producer profile still verifies through the new decoder. ++ legacy := api.DecisionEnvelope{Binding: &api.DecisionBinding{Context: map[string]any{"number": float64(1), "exponent": float64(1e-7)}}} ++ Sign(&legacy, key, "synthetic-numeric") ++ wire, _ := json.Marshal(legacy) ++ decoded, err := DecodeEnvelope(wire) ++ if err != nil || Verify(decoded, keys) != nil { ++ t.Fatal("legacy producer compatibility failed") ++ } ++ for _, raw := range []string{`{} {}`, `{"new_unknown_authority":true}`} { ++ if _, err := DecodeEnvelope([]byte(raw)); err == nil { ++ t.Fatal("invalid envelope accepted") ++ } ++ } ++} +diff --git a/internal/sign/sign.go b/internal/sign/sign.go +--- a/internal/sign/sign.go ++++ b/internal/sign/sign.go +@@ -4,10 +4,12 @@ + package sign + + import ( ++ "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" ++ "io" + + "github.com/netkingdom/access-engine/pkg/api" + ) +@@ -74,3 +76,20 @@ + } + return nil + } ++ ++// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs. ++// Legacy producer envelopes remain byte-compatible. It does not invent another ++// signature canonicalization: CanonicalBytes still calls typed json.Marshal. ++func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) { ++ var envelope api.DecisionEnvelope ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if err := decoder.Decode(&envelope); err != nil { ++ return envelope, err ++ } ++ if _, err := decoder.Token(); err != io.EOF { ++ return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data") ++ } ++ return envelope, nil ++} +diff --git a/internal/taskexecution/approval_source.go b/internal/taskexecution/approval_source.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source.go +@@ -0,0 +1,215 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "crypto/sha256" ++ "crypto/tls" ++ "crypto/x509" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "mime" ++ "net/http" ++ "net/url" ++ "os" ++ "path/filepath" ++ "regexp" ++ "strings" ++ "time" ++ ++ "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++) ++ ++var ErrApprovalSource = errors.New("trusted Approval source refused") ++var approvalUUID = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`) ++ ++type ApprovalSourceConfig struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ Origin string `json:"origin"` ++ StoreTenant string `json:"store_tenant"` ++ CAFile string `json:"ca_file"` ++ TokenFile string `json:"token_file"` ++} ++ ++// ApprovalReader is installed from owner configuration, never an HTTP request. ++// It reads one fixed PIP endpoint; no policy, retry, consume or effect lives here. ++type ApprovalReader struct { ++ config ApprovalSourceConfig ++ configurationDigest string ++ client *http.Client ++} ++ ++func NewApprovalReader(config ApprovalSourceConfig) (*ApprovalReader, error) { ++ u, err := url.Parse(config.Origin) ++ if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || strings.ContainsAny(config.Origin, "\r\n\t ") || config.SourceID == "" || config.SourceRevision == "" || config.StoreTenant != "tenant:platform" || !filepath.IsAbs(config.CAFile) || !filepath.IsAbs(config.TokenFile) { ++ return nil, ErrApprovalSource ++ } ++ ca, err := readPublicApprovalFile(config.CAFile, 1048576) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ pool := x509.NewCertPool() ++ if !pool.AppendCertsFromPEM(ca) { ++ return nil, ErrApprovalSource ++ } ++ config.Origin = strings.TrimRight(config.Origin, "/") ++ digest := api.CanonicalDigest(struct { ++ Config ApprovalSourceConfig `json:"configuration"` ++ CA string `json:"ca_sha256"` ++ }{config, byteDigest(ca)}) ++ return &ApprovalReader{config: config, configurationDigest: digest, client: &http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{Proxy: nil, DisableCompression: true, TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}}}}, nil ++} ++func readPublicApprovalFile(path string, limit int64) ([]byte, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 { ++ return nil, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, limit+1)) ++ if err != nil || int64(len(raw)) > limit { ++ return nil, ErrApprovalSource ++ } ++ return raw, nil ++} ++func LoadApprovalReader(path string) (*ApprovalReader, error) { ++ if !filepath.IsAbs(path) { ++ return nil, ErrApprovalSource ++ } ++ raw, err := readPublicApprovalFile(path, 65536) ++ if err != nil { ++ return nil, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return nil, ErrApprovalSource ++ } ++ var config ApprovalSourceConfig ++ decoder := json.NewDecoder(strings.NewReader(string(raw))) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&config) != nil { ++ return nil, ErrApprovalSource ++ } ++ return NewApprovalReader(config) ++} ++func (r *ApprovalReader) Configured() bool { ++ return r != nil && r.client != nil && r.configurationDigest != "" ++} ++func byteDigest(raw []byte) string { ++ h := sha256.Sum256(raw) ++ return "sha256:" + hex.EncodeToString(h[:]) ++} ++func readApprovalToken(path string) (string, error) { ++ file, err := os.Open(path) ++ if err != nil { ++ return "", ErrApprovalSource ++ } ++ defer file.Close() ++ info, err := file.Stat() ++ if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 { ++ return "", ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(file, 8193)) ++ if err != nil || len(raw) > 8192 { ++ return "", ErrApprovalSource ++ } ++ token := strings.TrimSpace(string(raw)) ++ if token == "" || strings.ContainsAny(token, " \t\r\n") { ++ return "", ErrApprovalSource ++ } ++ return token, nil ++} ++ ++// ApprovalObservation cannot be created by caller JSON or a public verified flag. ++// It is raw fact data plus the source/configuration used by this fixed reader. ++type ApprovalObservation struct { ++ raw []byte ++ sourceID, revision, configurationDigest string ++} ++ ++func (r *ApprovalReader) Observe(ctx context.Context, id string) (ApprovalObservation, error) { ++ if !r.Configured() || !approvalUUID.MatchString(id) { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ token, err := readApprovalToken(r.config.TokenFile) ++ if err != nil { ++ return ApprovalObservation{}, err ++ } ++ req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.config.Origin+"/v1/approvals/"+id+"/claim", nil) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ req.Header.Set("Authorization", "Bearer "+token) ++ req.Header.Set("Accept", "application/json") ++ response, err := r.client.Do(req) ++ if err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ defer response.Body.Close() ++ types := response.Header.Values("Content-Type") ++ if len(types) != 1 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ media, parameters, err := mime.ParseMediaType(types[0]) ++ if err != nil || media != "application/json" || (parameters["charset"] != "" && !strings.EqualFold(parameters["charset"], "utf-8")) || (len(parameters) != 0 && (len(parameters) != 1 || parameters["charset"] == "")) || response.StatusCode != 200 || response.Header.Get("Content-Encoding") != "" { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ raw, err := io.ReadAll(io.LimitReader(response.Body, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return ApprovalObservation{}, ErrApprovalSource ++ } ++ return ApprovalObservation{raw: append([]byte(nil), raw...), sourceID: r.config.SourceID, revision: r.config.SourceRevision, configurationDigest: r.configurationDigest}, nil ++} ++ ++// VerifiedApprovalFacts establishes typed fact correspondence, not a decision. ++// Only an authenticated fixed-reader observation can construct this handle. ++type VerifiedApprovalFacts struct { ++ original, fetched approvalfact.Fact ++ evidence api.ApprovalObservationProvenance ++ valid bool ++} ++ ++func (o ApprovalObservation) Validate(original []byte, expected approvalfact.Expected, now time.Time) (VerifiedApprovalFacts, error) { ++ if len(o.raw) == 0 || o.sourceID == "" || o.revision == "" || o.configurationDigest == "" { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ originalFact, err := approvalfact.ValidateClaim(original, expected, now) ++ if err != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ fetched, err := approvalfact.ValidateClaim(o.raw, expected, now) ++ if err != nil || approvalfact.MatchObserved(originalFact, fetched) != nil { ++ return VerifiedApprovalFacts{}, ErrApprovalSource ++ } ++ limit := originalFact.Deadline() ++ if fetched.Deadline().Before(limit) { ++ limit = fetched.Deadline() ++ } ++ evidence := api.ApprovalObservationProvenance{SourceID: o.sourceID, SourceRevision: o.revision, ConfigurationDigest: o.configurationDigest, ClaimDigest: byteDigest(o.raw), NativeBindingDigest: fetched.NativeDigest(), ObservedAt: fetched.ObservedAt().UTC().Format(time.RFC3339Nano), NotAfter: limit.UTC().Format(time.RFC3339Nano), ValidatedAt: now.UTC().Format(time.RFC3339Nano)} ++ return VerifiedApprovalFacts{original: originalFact, fetched: fetched, evidence: evidence, valid: true}, nil ++} ++func (f VerifiedApprovalFacts) Deadline() (time.Time, error) { ++ if !f.valid { ++ return time.Time{}, ErrApprovalSource ++ } ++ a := f.original.Deadline() ++ if f.fetched.Deadline().Before(a) { ++ a = f.fetched.Deadline() ++ } ++ return a, nil ++} ++func (f VerifiedApprovalFacts) Provenance() (*api.ApprovalObservationProvenance, error) { ++ if !f.valid { ++ return nil, ErrApprovalSource ++ } ++ copy := f.evidence ++ return ©, nil ++} +diff --git a/internal/taskexecution/approval_source_test.go b/internal/taskexecution/approval_source_test.go +new file mode 100644 +--- /dev/null ++++ b/internal/taskexecution/approval_source_test.go +@@ -0,0 +1,118 @@ ++package taskexecution ++ ++import ( ++ "context" ++ "encoding/pem" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" ++ "net/http" ++ "net/http/httptest" ++ "os" ++ "path/filepath" ++ "strings" ++ "sync/atomic" ++ "testing" ++ "time" ++) ++ ++func TestFixedApprovalReaderRefusalBoundary(t *testing.T) { ++ folder := t.TempDir() ++ token := filepath.Join(folder, "token") ++ ca := filepath.Join(folder, "ca") ++ if err := os.WriteFile(token, []byte("synthetic-source-token"), 0440); err != nil { ++ t.Fatal(err) ++ } ++ var mode atomic.Value ++ mode.Store("ok") ++ var foreignCalls atomic.Int64 ++ foreign := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ foreignCalls.Add(1) ++ t.Error("credential escaped fixed origin") ++ })) ++ defer foreign.Close() ++ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ if r.Header.Get("Authorization") != "Bearer synthetic-source-token" || r.URL.Path != "/v1/approvals/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/claim" || r.Method != "GET" { ++ t.Error("wrong auth/path/method") ++ } ++ w.Header().Set("Content-Type", "application/json") ++ switch mode.Load().(string) { ++ case "redirect": ++ w.Header().Set("Location", foreign.URL) ++ w.WriteHeader(302) ++ return ++ case "status": ++ w.WriteHeader(403) ++ case "large": ++ w.Write([]byte(strings.Repeat(" ", 524289))) ++ case "duplicate": ++ w.Write([]byte(`{"a":true,"a":false}`)) ++ case "invalid-utf8": ++ w.Write([]byte{'"', 255, '"'}) ++ case "media": ++ w.Header().Set("Content-Type", "application/json; arbitrary=authority") ++ w.Write([]byte(`{}`)) ++ case "duplicate-media": ++ w.Header().Add("Content-Type", "application/json") ++ w.Write([]byte(`{}`)) ++ case "compressed": ++ w.Header().Set("Content-Encoding", "gzip") ++ w.Write([]byte(`{}`)) ++ case "slow": ++ select { ++ case <-r.Context().Done(): ++ case <-time.After(time.Second): ++ } ++ default: ++ w.Write([]byte(`{}`)) ++ } ++ })) ++ defer server.Close() ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}), 0600) ++ config := ApprovalSourceConfig{SourceID: "synthetic-source", SourceRevision: "synthetic-revision", Origin: server.URL, StoreTenant: "tenant:platform", CAFile: ca, TokenFile: token} ++ reader, err := NewApprovalReader(config) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err != nil { ++ t.Fatal(err) ++ } ++ for _, name := range []string{"redirect", "status", "large", "duplicate", "invalid-utf8", "media", "duplicate-media", "compressed", "slow"} { ++ t.Run(name, func(t *testing.T) { ++ mode.Store(name) ++ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) ++ defer cancel() ++ if _, err := reader.Observe(ctx, "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("invalid provider response admitted") ++ } ++ }) ++ } ++ if foreignCalls.Load() != 0 { ++ t.Fatal("redirect followed") ++ } ++ mode.Store("ok") ++ for _, bad := range []string{"../consume", "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/consume", ""} { ++ if _, err := reader.Observe(context.Background(), bad); err == nil { ++ t.Fatal("request selected route") ++ } ++ } ++ for _, mode := range []os.FileMode{0460, 0644, 0620} { ++ os.Chmod(token, mode) ++ if _, err := reader.Observe(context.Background(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); err == nil { ++ t.Fatal("unsafe token permissions") ++ } ++ } ++ for _, origin := range []string{"http://127.0.0.1", "https://user:password@localhost", "https://localhost/path", "https://localhost?query=1", "https://localhost#fragment"} { ++ bad := config ++ bad.Origin = origin ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("unsafe origin") ++ } ++ } ++ bad := config ++ bad.StoreTenant = "tenant:foreign" ++ if _, err := NewApprovalReader(bad); err == nil { ++ t.Fatal("foreign tenant configured") ++ } ++ if _, err := (ApprovalObservation{}).Validate([]byte(`{}`), approvalfact.Expected{}, time.Now()); err == nil { ++ t.Fatal("zero observation admitted") ++ } ++} +diff --git a/internal/taskexecution/service_input.go b/internal/taskexecution/service_input.go +--- a/internal/taskexecution/service_input.go ++++ b/internal/taskexecution/service_input.go +@@ -7,6 +7,7 @@ + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "github.com/netkingdom/approval-engine/adapters/go/approvalfact" + "io" + "time" + ) +@@ -15,6 +16,7 @@ + + const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" + const ServicePilotPackageVersion = "v1" ++const TrustedServicePilotPackageVersion = "v2" + + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" +@@ -24,7 +26,10 @@ + // It installs the explicit + // proposed fixed subject-to-transport mapping, never a request-selected mapping. + // No constructor parameter can select warn/disabled mode or another subject. +-type ServicePilotInputs struct{ auth *callerauth.Authenticator } ++type ServicePilotInputs struct { ++ auth *callerauth.Authenticator ++ preserveNumbers bool ++} + + func NewServicePilotInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { + a, e := callerauth.New(callerauth.ModeEnforce, reviewer, "flex-auth", map[string]string{"secrets-engine": ServicePilotPrincipal}, nil) +@@ -34,12 +39,24 @@ + return &ServicePilotInputs{auth: a}, nil + } + ++// NewTrustedServiceInputs preserves numeric representations only for the exact ++// v2 task path; it grants no Approval-source trust or executable eligibility. ++func NewTrustedServiceInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) { ++ input, err := NewServicePilotInputs(reviewer) ++ if err != nil { ++ return nil, err ++ } ++ input.preserveNumbers = true ++ return input, nil ++} ++ + // ServiceInput cannot be fabricated through exported fields. It proves only + // source input preparation through TokenReview, not approval provenance/consume + // or execution authority. Default zero value is not an authenticated input. + type ServiceInput struct { +- serialized []byte +- caller callerauth.Record ++ serialized []byte ++ caller callerauth.Record ++ preserveNumbers bool + } + + func (s ServiceInput) Request() (api.CheckRequest, error) { +@@ -47,7 +64,11 @@ + return api.CheckRequest{}, ErrServiceInput + } + var r api.CheckRequest +- if json.Unmarshal(s.serialized, &r) != nil { ++ decoder := json.NewDecoder(bytes.NewReader(s.serialized)) ++ if s.preserveNumbers { ++ decoder.UseNumber() ++ } ++ if decoder.Decode(&r) != nil { + return r, ErrServiceInput + } + return r, nil +@@ -142,5 +163,27 @@ + if err != nil { + return refuse() + } +- return ServiceInput{serialized: raw, caller: record}, nil ++ return ServiceInput{serialized: raw, caller: record, preserveNumbers: s.preserveNumbers}, nil + } ++ ++func (s ServiceInput) NumberPreserved() bool { return s.preserveNumbers } ++ ++// DecodeTrustedServiceRequest preserves numbers in the exact new task profile. ++// Owner helper validates strict Unicode/JSON; this is not a new canonicalizer. ++func DecodeTrustedServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ if err != nil || len(raw) > 524288 { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ if _, err := approvalfact.DecodeJSON(raw); err != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ var request api.CheckRequest ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.UseNumber() ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} +diff --git a/pkg/api/canonical.go b/pkg/api/canonical.go +--- a/pkg/api/canonical.go ++++ b/pkg/api/canonical.go +@@ -635,16 +635,30 @@ + + // DecisionProvenance captures evaluator and policy provenance. + type DecisionProvenance struct { +- Evaluator string `json:"evaluator" yaml:"evaluator"` +- Mode string `json:"mode" yaml:"mode"` +- PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` +- PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` +- PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` +- RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` +- DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` +- InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` +- DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` +- Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++ Evaluator string `json:"evaluator" yaml:"evaluator"` ++ Mode string `json:"mode" yaml:"mode"` ++ PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"` ++ PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"` ++ PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"` ++ RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"` ++ DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"` ++ InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"` ++ DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"` ++ ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"` ++ Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"` ++} ++ ++// ApprovalObservationProvenance binds fresh server-owned fact evidence without ++// rewriting the original carried claim/context or either PDP request digest. ++type ApprovalObservationProvenance struct { ++ SourceID string `json:"source_id"` ++ SourceRevision string `json:"source_revision"` ++ ConfigurationDigest string `json:"configuration_digest"` ++ ClaimDigest string `json:"claim_digest"` ++ NativeBindingDigest string `json:"native_binding_digest"` ++ ObservedAt string `json:"observed_at"` ++ NotAfter string `json:"not_after"` ++ ValidatedAt string `json:"validated_at"` + } + + // CallerProvenance records how the request was authenticated to flex-auth. +diff --git a/third_party/approvalfact/README.md b/third_party/approvalfact/README.md +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/README.md +@@ -0,0 +1,55 @@ ++# Approval fact data adapter ++ ++Provider-owned pure Go module for Access's Approval B2 source reader. No network, ++credentials, subprocess, policy evaluator, server or consume mutation exists here. ++Python `approval_engine.binding` remains the authoritative native format. ++ ++API: ++ ++- `CanonicalBinding(rawJSON)` returns native five-field canonical UTF-8 bytes ++ and sha256 digest; extra binding metadata is excluded. ++- `ValidateClaim(rawClaim, Expected, now)` validates one exact human/PDP-path ++ claim against independently owner-derived `Expected.NativeBindingJSON`, exact ++ approval ID and actual Access approval-free PDP digest. ++- `MatchObserved(submitted, observed)` requires two validated facts at the same ++ captured instant and identical nonfreshness authority facts. No request is ++ rewritten. `Fact.RawJSON()` returns a detached original byte copy; deadlines ++ preserve each input's own freshness and approval expiry. ++ ++`Fact` means validated data, never authenticated provider origin. Access must ++fetch provider bytes through its independently admitted fixed origin/identity, ++then wrap this data in its own unforgeable source handle. A caller-supplied claim ++cannot become source truth merely by passing this helper. Access remains the ++single evaluator and must bound decision/operation/caller lifetimes separately. ++PEP consume CAS and live one-shot journal remain required before any effect. ++ ++Expected native act bytes must preserve the actual owner/compiler/normalization ++representation. Marshaling float64-decoded CheckRequest maps loses 1.0/1 and ++large integers. Raw submitted resource is sufficient only when Access proves ++the active registry cannot override that target and applies its exact existing ++normalization. Otherwise authoritative raw enrichment/compiler evidence is ++needed. This helper supplies no fallback or invented restricted numeric domain. ++ ++The strict parser retains integer/floating lexical types, arbitrary integer ++magnitudes, finite IEEE-754 float semantics and valid Unicode scalar values. ++Canonical float/string formatting follows Python source, including negative ++floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate ++object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. ++Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at ++most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. ++These explicit parser/resource limits fail closed, never normalize authority. ++ ++`go test -race ./...` checks the portable producer vectors, typed fact pairing, ++strict JSON/Unicode and exact deadline semantics. From the owner repo root, ++`python3 scripts/check_go_approvalfact.py` runs a deterministic 14,114-case ++development-only differential corpus against the actual Python producer, ++including random finite IEEE bit patterns, exponent boundaries/subnormals and ++Unicode. It uses a test-only local oracle file; production imports no oracle or ++Python runtime. Sample conformance is evidence, not a formal proof over all JSON. ++ ++The module pins Go 1.26.7 to match the existing Access consumer toolchain. ++`testdata/owner-vectors.json` is an unchanged portable copy of the Approval owner ++fixture, SHA256 `dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323`. ++Transport cases requiring HTTP remain the Access adapter's tests; this module ++checks their duplicate/nonfinite bodies only. Import/deployment/native identity ++admission and positive policy gates require separate owner evidence. +diff --git a/third_party/approvalfact/SOURCE.json b/third_party/approvalfact/SOURCE.json +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/SOURCE.json +@@ -0,0 +1,13 @@ ++{ ++ "owner": "approval-engine", ++ "module": "github.com/netkingdom/approval-engine/adapters/go/approvalfact", ++ "source_artifact": "approval-go-fact/manifest.json", ++ "review_status": "owner-frozen; independent coordinator review required", ++ "role": "pure data canonicalization and fact correspondence only; not provider authentication or PDP", ++ "files_sha256": { ++ "canonical.go": "d48588fb26e9037c8e83421586b8a818f4c0a52afb874f3fadf7229fc971c282", ++ "claim.go": "007a2494f79c47c104635402407e9e68f17d8599d23e0f2c8ebf6cb1b47e3621", ++ "go.mod": "bb09c53ffea926fbfba6e580ed3bd71ca358575401717389b3c7c49c0c674b43", ++ "README.md": "5d936b7f3de87bbf4e946f0dc44823d2f69b62b4122b55b9064791b7a19fd4f5" ++ } ++} +diff --git a/third_party/approvalfact/canonical.go b/third_party/approvalfact/canonical.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/canonical.go +@@ -0,0 +1,295 @@ ++// Package approvalfact provides data-only Approval claim correspondence. ++// It neither authenticates a provider nor decides permission or consumes objects. ++package approvalfact ++ ++import ( ++ "bytes" ++ "crypto/sha256" ++ "encoding/hex" ++ "encoding/json" ++ "errors" ++ "io" ++ "math" ++ "math/big" ++ "sort" ++ "strconv" ++ "strings" ++ "unicode/utf8" ++) ++ ++const MaxJSONBytes = 1 << 20 ++ ++var errJSON = errors.New("invalid Approval JSON") ++ ++// DecodeJSON retains numeric lexical types, refuses duplicates and invalid ++// Unicode, and bounds work. A Go map previously decoded into float64 cannot ++// recover the producer's integer-versus-floating identity; pass source bytes. ++func DecodeJSON(raw []byte) (any, error) { ++ if len(raw) > MaxJSONBytes || !utf8.Valid(raw) || !validSurrogates(raw) { ++ return nil, errJSON ++ } ++ d := json.NewDecoder(bytes.NewReader(raw)) ++ d.UseNumber() ++ var read func(int) (any, error) ++ read = func(depth int) (any, error) { ++ if depth > 128 { ++ return nil, errJSON ++ } ++ t, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ switch x := t.(type) { ++ case json.Delim: ++ if x == '{' { ++ m := map[string]any{} ++ for d.More() { ++ k, e := d.Token() ++ if e != nil { ++ return nil, errJSON ++ } ++ s, ok := k.(string) ++ if !ok { ++ return nil, errJSON ++ } ++ if _, ok = m[s]; ok { ++ return nil, errJSON ++ } ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ m[s] = v ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim('}') { ++ return nil, errJSON ++ } ++ return m, nil ++ } ++ if x == '[' { ++ a := []any{} ++ for d.More() { ++ v, e := read(depth + 1) ++ if e != nil { ++ return nil, e ++ } ++ a = append(a, v) ++ } ++ end, e := d.Token() ++ if e != nil || end != json.Delim(']') { ++ return nil, errJSON ++ } ++ return a, nil ++ } ++ return nil, errJSON ++ case json.Number: ++ if _, e := pythonNumber(x); e != nil { ++ return nil, e ++ } ++ return x, nil ++ case string, bool, nil: ++ return x, nil ++ default: ++ return nil, errJSON ++ } ++ } ++ v, e := read(0) ++ if e != nil { ++ return nil, e ++ } ++ if _, e = d.Token(); e != io.EOF { ++ return nil, errJSON ++ } ++ return v, nil ++} ++ ++// encoding/json replaces lone UTF-16 surrogates, whereas the Python producer ++// cannot encode them as UTF-8. Reject before the decoder can erase that fact. ++func validSurrogates(raw []byte) bool { ++ in := false ++ for i := 0; i < len(raw); i++ { ++ if raw[i] == '"' { ++ in = !in ++ continue ++ } ++ if !in || raw[i] != '\\' { ++ continue ++ } ++ i++ ++ if i >= len(raw) { ++ return false ++ } ++ if raw[i] != 'u' { ++ continue ++ } ++ if i+4 >= len(raw) { ++ return false ++ } ++ n, e := strconv.ParseUint(string(raw[i+1:i+5]), 16, 16) ++ if e != nil { ++ return false ++ } ++ i += 4 ++ if n >= 0xdc00 && n <= 0xdfff { ++ return false ++ } ++ if n < 0xd800 || n > 0xdbff { ++ continue ++ } ++ if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' { ++ return false ++ } ++ low, e := strconv.ParseUint(string(raw[i+3:i+7]), 16, 16) ++ if e != nil || low < 0xdc00 || low > 0xdfff { ++ return false ++ } ++ i += 6 ++ } ++ return !in ++} ++ ++func pythonNumber(n json.Number) (string, error) { ++ s := string(n) ++ if !strings.ContainsAny(s, ".eE") { ++ i, ok := new(big.Int).SetString(s, 10) ++ if !ok { ++ return "", errJSON ++ } ++ return i.String(), nil ++ } ++ f, e := strconv.ParseFloat(s, 64) ++ if e != nil || math.IsInf(f, 0) || math.IsNaN(f) { ++ return "", errJSON ++ } ++ // Python repr uses shortest round-trip digits, fixed form for exponents ++ // -4..15, a mandatory .0 for integral floats, and two exponent digits. ++ sci := strconv.FormatFloat(f, 'e', -1, 64) ++ at := strings.IndexByte(sci, 'e') ++ exp, _ := strconv.Atoi(sci[at+1:]) ++ if exp >= -4 && exp < 16 { ++ s = strconv.FormatFloat(f, 'f', -1, 64) ++ if !strings.Contains(s, ".") { ++ s += ".0" ++ } ++ return s, nil ++ } ++ sign := "+" ++ if exp < 0 { ++ sign = "-" ++ exp = -exp ++ } ++ digits := strconv.Itoa(exp) ++ if len(digits) < 2 { ++ digits = "0" + digits ++ } ++ return sci[:at] + "e" + sign + digits, nil ++} ++ ++func quote(s string) string { ++ var b strings.Builder ++ b.WriteByte('"') ++ for _, r := range s { ++ switch r { ++ case '"', '\\': ++ b.WriteByte('\\') ++ b.WriteRune(r) ++ case '\b': ++ b.WriteString(`\b`) ++ case '\f': ++ b.WriteString(`\f`) ++ case '\n': ++ b.WriteString(`\n`) ++ case '\r': ++ b.WriteString(`\r`) ++ case '\t': ++ b.WriteString(`\t`) ++ default: ++ if r < 32 { ++ b.WriteString(`\u00`) ++ const h = "0123456789abcdef" ++ b.WriteByte(h[byte(r)>>4]) ++ b.WriteByte(h[byte(r)&15]) ++ } else { ++ b.WriteRune(r) ++ } ++ } ++ } ++ b.WriteByte('"') ++ return b.String() ++} ++ ++func canonical(v any) (string, error) { ++ switch x := v.(type) { ++ case nil: ++ return "null", nil ++ case bool: ++ if x { ++ return "true", nil ++ } ++ return "false", nil ++ case string: ++ return quote(x), nil ++ case json.Number: ++ return pythonNumber(x) ++ case []any: ++ a := make([]string, len(x)) ++ for i, v := range x { ++ s, e := canonical(v) ++ if e != nil { ++ return "", e ++ } ++ a[i] = s ++ } ++ return "[" + strings.Join(a, ",") + "]", nil ++ case map[string]any: ++ keys := make([]string, 0, len(x)) ++ for k := range x { ++ keys = append(keys, k) ++ } ++ sort.Strings(keys) ++ a := make([]string, 0, len(keys)) ++ for _, k := range keys { ++ s, e := canonical(x[k]) ++ if e != nil { ++ return "", e ++ } ++ a = append(a, quote(k)+":"+s) ++ } ++ return "{" + strings.Join(a, ",") + "}", nil ++ default: ++ return "", errJSON ++ } ++} ++ ++// CanonicalBinding reproduces approval_engine.binding.binding_digest over the ++// five native act fields. Extra binding metadata is excluded by that contract. ++func CanonicalBinding(raw []byte) ([]byte, string, error) { ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return nil, "", e ++ } ++ m, ok := v.(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act := map[string]any{} ++ for _, k := range []string{"action", "actor", "principal", "purpose"} { ++ s, ok := m[k].(string) ++ if !ok || s == "" { ++ return nil, "", errJSON ++ } ++ act[k] = s ++ } ++ target, ok := m["target"].(map[string]any) ++ if !ok { ++ return nil, "", errJSON ++ } ++ act["target"] = target ++ s, e := canonical(act) ++ if e != nil { ++ return nil, "", e ++ } ++ sum := sha256.Sum256([]byte(s)) ++ return []byte(s), "sha256:" + hex.EncodeToString(sum[:]), nil ++} +diff --git a/third_party/approvalfact/claim.go b/third_party/approvalfact/claim.go +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/claim.go +@@ -0,0 +1,170 @@ ++package approvalfact ++ ++import ( ++ "encoding/json" ++ "errors" ++ "regexp" ++ "strconv" ++ "time" ++) ++ ++var errClaim = errors.New("Approval fact does not match required claim") ++var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) ++var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) ++var timePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$`) ++ ++// Expected contains independently owner-derived native act bytes and the actual ++// Access approval-free digest. Marshaling an already float64-decoded Go request ++// is not an authoritative reconstruction of the native act. ++type Expected struct { ++ ApprovalID string ++ NativeBindingJSON []byte ++ PDPDigest string ++} ++ ++// Fact is validated data shape/correspondence, NOT authenticated provider origin ++// or execution authority. Access must wrap it only after its own source admission. ++type Fact struct { ++ raw []byte ++ stable string ++ digest string ++ observed, timeLimit, validatedAt time.Time ++} ++ ++func (f Fact) RawJSON() []byte { return append([]byte(nil), f.raw...) } ++func (f Fact) ObservedAt() time.Time { return f.observed } ++func (f Fact) Deadline() time.Time { return f.timeLimit } ++func (f Fact) NativeDigest() string { return f.digest } ++ ++func members(m map[string]any, required, optional []string) bool { ++ allowed := map[string]bool{} ++ for _, k := range required { ++ allowed[k] = true ++ if _, ok := m[k]; !ok { ++ return false ++ } ++ } ++ for _, k := range optional { ++ allowed[k] = true ++ } ++ for k := range m { ++ if !allowed[k] { ++ return false ++ } ++ } ++ return true ++} ++func object(v any) (map[string]any, bool) { m, ok := v.(map[string]any); return m, ok } ++func stamp(v any) (time.Time, error) { ++ s, ok := v.(string) ++ if !ok || !timePattern.MatchString(s) { ++ return time.Time{}, errClaim ++ } ++ t, e := time.Parse(time.RFC3339Nano, s) ++ if e != nil { ++ return time.Time{}, errClaim ++ } ++ return t, nil ++} ++ ++// ValidateClaim checks one original or fetched schema-0.1 human/PDP-path claim ++// at a caller-captured instant, using the existing Approval producer semantics. ++// It has no I/O, policy evaluator, credentials, cache, or consume mutation. ++func ValidateClaim(raw []byte, expected Expected, now time.Time) (Fact, error) { ++ if now.IsZero() || !uuidPattern.MatchString(expected.ApprovalID) || !digestPattern.MatchString(expected.PDPDigest) { ++ return Fact{}, errClaim ++ } ++ _, want, e := CanonicalBinding(expected.NativeBindingJSON) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ v, e := DecodeJSON(raw) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ m, ok := object(v) ++ if !ok || !members(m, []string{"schema_version", "kind", "issuer", "approval_id", "state", "valid_now", "consumed", "binding", "freshness", "validity", "reason_code"}, []string{"yields_to"}) { ++ return Fact{}, errClaim ++ } ++ if y, ok := m["yields_to"]; ok { ++ if _, ok = y.(string); !ok { ++ return Fact{}, errClaim ++ } ++ } ++ if m["schema_version"] != "0.1" || m["kind"] != "approval-claim" || m["issuer"] != "approval-engine" || m["approval_id"] != expected.ApprovalID || m["state"] != "valid" || m["valid_now"] != true || m["consumed"] != false || m["reason_code"] != "ok" { ++ return Fact{}, errClaim ++ } ++ b, ok := object(m["binding"]) ++ if !ok || !members(b, []string{"action", "actor", "principal", "purpose", "target", "digest", "pdp_digest", "pdp_path", "human_control"}, nil) { ++ return Fact{}, errClaim ++ } ++ if b["digest"] != want || b["pdp_digest"] != expected.PDPDigest || b["pdp_path"] != true || b["human_control"] != true { ++ return Fact{}, errClaim ++ } ++ text, e := canonical(b) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ _, got, e := CanonicalBinding([]byte(text)) ++ if e != nil || got != want { ++ return Fact{}, errClaim ++ } ++ f, ok := object(m["freshness"]) ++ if !ok || !members(f, []string{"observed_at", "ttl_seconds", "not_after"}, nil) { ++ return Fact{}, errClaim ++ } ++ observed, e := stamp(f["observed_at"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ notAfter, e := stamp(f["not_after"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ ttl, ok := f["ttl_seconds"].(json.Number) ++ if !ok { ++ return Fact{}, errClaim ++ } ++ seconds, e := strconv.ParseInt(string(ttl), 10, 64) ++ if e != nil || seconds < 1 { ++ return Fact{}, errClaim ++ } ++ // Bound arithmetic to time.Duration's representable seconds, not a new ++ // semantic freshness grant or configured TTL. Unsupported extremes refuse. ++ if seconds > int64((1<<63-1)/int64(time.Second)) || !observed.Add(time.Duration(seconds)*time.Second).Equal(notAfter) || now.Before(observed) || !now.Before(notAfter) { ++ return Fact{}, errClaim ++ } ++ validity, ok := object(m["validity"]) ++ if !ok || !members(validity, []string{"not_before", "expires_at"}, nil) { ++ return Fact{}, errClaim ++ } ++ before, e := stamp(validity["not_before"]) ++ if e != nil { ++ return Fact{}, e ++ } ++ expires, e := stamp(validity["expires_at"]) ++ if e != nil || now.Before(before) || !now.Before(expires) { ++ return Fact{}, errClaim ++ } ++ limit := notAfter ++ if expires.Before(limit) { ++ limit = expires ++ } ++ delete(m, "freshness") ++ delete(m, "yields_to") ++ stable, e := canonical(m) ++ if e != nil { ++ return Fact{}, errClaim ++ } ++ return Fact{raw: append([]byte(nil), raw...), stable: stable, digest: want, observed: observed, timeLimit: limit, validatedAt: now}, nil ++} ++ ++// MatchObserved requires independently validated original/fetched facts at the ++// same captured instant and exact nonfreshness authority correspondence. It ++// never rewrites the original request or extends its original deadline. ++func MatchObserved(submitted, observed Fact) error { ++ if submitted.stable == "" || observed.stable == "" || !submitted.validatedAt.Equal(observed.validatedAt) || submitted.stable != observed.stable { ++ return errClaim ++ } ++ return nil ++} +diff --git a/third_party/approvalfact/go.mod b/third_party/approvalfact/go.mod +new file mode 100644 +--- /dev/null ++++ b/third_party/approvalfact/go.mod +@@ -0,0 +1,3 @@ ++module github.com/netkingdom/approval-engine/adapters/go/approvalfact ++ ++go 1.26.7 +diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -237,3 +237,14 @@ + and canonical vectors; B3 additionally needs exact source/normalization numeric + representation and full trusted provider joins. No current policy false gate, + production trust, native deployment or protected effect is enabled by B1. ++ ++2026-10-16 B2/B3 source successor preparation (separate from frozen B1): the ++isolated v2 source path now joins the fixed authenticated Approval reader, ++unchanged Approval-owner canonical/fact helper, original/fresh correspondence, ++raw numeric request/registry source retention, same policy evaluator and signed ++source provenance. Disposable actual HTTP and production serve subprocess prove ++synthetic signed ALLOW and fail-closed negative joins with exact response/record ++parity. See `docs/trusted-service-task-source.md`. T06 remains `progress`: this ++source proposal requires independent cross-owner review and full consumer/Platform ++successor integration, then separately admitted runtime custody, source trust, ++image provenance and rollout. No source-only proof establishes those runtime gates. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/manifest.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/manifest.json new file mode 100644 index 00000000..5bc40008 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/manifest.json @@ -0,0 +1,14 @@ +{ + "schema": "access-engine.b2-source-preparation-manifest.v1", + "B1_frozen_delta_sha256": "574c6396f6ea5f9876fef0583f8fdc38da094d0f666cf09922771a6099a9b267", + "source_vector_sha256": "dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323", + "artifact_sha256": { + "raw-representation-results-r1.json": "9ef987a7422bcfc252e40ac32f3ce7802e60b4a775408ab0aaa3af1edaf01989", + "run-representation-probe.py": "632050cf7a2be033dff44b06a412f82fe026f6e08d2407ec46970a6d17290ec6", + "raw-representation-probe.go": "71221b2339b0f204f776edbf979f4a6d780864faacb52a41410eac09d1c635d9", + "trusted-positive-path-r1.json": "0bb33d6f0fe69ea716c15e7ceea4a5d19529c110bcdb2d07b046c6b5ca71a229" + }, + "runtime_source_changes": false, + "policy_enablement": false, + "live_actions": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-probe.go b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-probe.go new file mode 100644 index 00000000..ed16f090 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-probe.go @@ -0,0 +1,11 @@ +// Synthetic representation experiment only. No policy, crypto, credential or transport. +package main +import("bytes";"encoding/json";"os") +type vector struct {Target string `json:"input_target_json"`; Binding string `json:"canonical_binding_utf8"`} +func main(){var input struct{Vectors []vector `json:"native_binding_vectors"`};if json.NewDecoder(os.Stdin).Decode(&input)!=nil{os.Exit(2)};rows:=[]map[string]string{} + for _,v:=range input.Vectors{var normal any;if json.Unmarshal([]byte(v.Target),&normal)!=nil{os.Exit(2)};defaultTarget,_:=json.Marshal(normal) + var numbers any;d:=json.NewDecoder(bytes.NewBufferString(v.Target));d.UseNumber();if d.Decode(&numbers)!=nil{os.Exit(2)};numberTarget,_:=json.Marshal(numbers) + var act map[string]json.RawMessage;if json.Unmarshal([]byte(v.Binding),&act)!=nil{os.Exit(2)};act["target"]=json.RawMessage(v.Target);rawAct,_:=json.Marshal(act) + rows=append(rows,map[string]string{"raw_target":v.Target,"go_default_target":string(defaultTarget),"use_number_target":string(numberTarget),"raw_message_act_draft":string(rawAct)}) + };json.NewEncoder(os.Stdout).Encode(rows) +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-results-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-results-r1.json new file mode 100644 index 00000000..af2a9cc8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/raw-representation-results-r1.json @@ -0,0 +1,136 @@ +{ + "schema": "access-engine.raw-native-representation-experiment.v1", + "source_vectors_sha256": "dfb0a8810cd91847d7af6a7af6a51607a67a66fad7b86fc57365308c36bd9323", + "source_vectors_producer_revision": "eece796b8bbee19551f6aa07ad389cbf83cabcdb", + "cases": [ + { + "input_target_json": "{\"value\":true}", + "producer_native_digest": "sha256:e1f537daa849e5da023efb5f3d62cc33d94cce20f9ca9c5a708c76e225fd702f", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":true}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":true}}" + }, + { + "input_target_json": "{\"value\":1}", + "producer_native_digest": "sha256:3a3d2da6267e4b917505a7c2247e6ce36fd8ee074eef37251bcc66839b602332", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":1}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1}}" + }, + { + "input_target_json": "{\"value\":1.0}", + "producer_native_digest": "sha256:21c2cdb9b161071ebfe361e50703511b2abf6c6f327f153282d247aa92c3898e", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": false, + "go_default_target": "{\"value\":1}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1.0}}" + }, + { + "input_target_json": "{\"value\":false}", + "producer_native_digest": "sha256:278cbcd013f7923db5258dd860f911d688505a8259f15ccb77983b044f312c74", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":false}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":false}}" + }, + { + "input_target_json": "{\"value\":0}", + "producer_native_digest": "sha256:e691b8fb55d4bf82a310dee37c045331100c8031e26ea6f51bdcceeeb3b1f20e", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":0}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":0}}" + }, + { + "input_target_json": "{\"value\":-0.0}", + "producer_native_digest": "sha256:eccde458c686bff67506bb2d94c750e648a311bf23967cb22b37f11e84b6844f", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": false, + "go_default_target": "{\"value\":-0}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":-0.0}}" + }, + { + "input_target_json": "{\"value\":1e-7}", + "producer_native_digest": "sha256:779042b6c0e675f6aacdcd3a02199aab66f534bb84dd410a95c18657afae563c", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":1e-7}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-7}}" + }, + { + "input_target_json": "{\"value\":1e-6}", + "producer_native_digest": "sha256:d021f402231fcd59e764b31e6f08a5298ebb328dfd93da58ecde8feb11fde0b5", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":0.000001}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e-6}}" + }, + { + "input_target_json": "{\"value\":1e20}", + "producer_native_digest": "sha256:14270ffca4ab10e9d9302d52e988b4aa3e2facd494c12a60e2edafef70277a20", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": false, + "go_default_target": "{\"value\":100000000000000000000}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e20}}" + }, + { + "input_target_json": "{\"value\":1e21}", + "producer_native_digest": "sha256:75a60a595b2578430b0be466b6bf9a46b62ddc99bbbcebd7fa0e8d22e3078c0a", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":1e+21}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":1e21}}" + }, + { + "input_target_json": "{\"value\":2.5e-7}", + "producer_native_digest": "sha256:50af37ff75a3e7a22629e746f814995c0c2cd96adb7f1ed5b26999b71d0ac498", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"value\":2.5e-7}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":2.5e-7}}" + }, + { + "input_target_json": "{\"value\":9007199254740993}", + "producer_native_digest": "sha256:765360dc5a1d949bd114c6b749d83069123d8b4b6b1d939f831428a800f50dab", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": false, + "go_default_target": "{\"value\":9007199254740992}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"value\":9007199254740993}}" + }, + { + "input_target_json": "{\"html\":\"<>&\", \"unicode\":\"\u00e9\u96ea\ud83d\ude00\", \"separator\":\"\\u2028\\u2029\"}", + "producer_native_digest": "sha256:c203d934743e2954342038a3746b6c37a0f3101e886ef855b1c6ad824adf5dea", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": true, + "go_default_target": "{\"html\":\"\\u003c\\u003e\\u0026\",\"separator\":\"\\u2028\\u2029\",\"unicode\":\"\u00e9\u96ea\ud83d\ude00\"}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"html\":\"\\u003c\\u003e\\u0026\",\"unicode\":\"\u00e9\u96ea\ud83d\ude00\",\"separator\":\"\\u2028\\u2029\"}}" + }, + { + "input_target_json": "{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}", + "producer_native_digest": "sha256:56306505c882af6edfae5f3d2a5b42e92737a0859236fe5574a21d1874655d51", + "raw_message_draft_matches_owner": true, + "use_number_roundtrip_matches_owner": true, + "default_float64_roundtrip_matches_owner": false, + "go_default_target": "{\"a\":{},\"b\":[true,1,1,null,{\"a\":\"y\",\"z\":\"x\"}]}", + "go_raw_draft": "{\"action\":\"secrets.kv.destroy\",\"actor\":\"agt-secrets-engine\",\"principal\":\"bernd\",\"purpose\":\"rotate-exposed-key\",\"target\":{\"b\":[true,1,1.0,null,{\"z\":\"x\",\"a\":\"y\"}],\"a\":{}}}" + } + ], + "raw_preservation_passed": 14, + "default_roundtrip_mismatches": 5, + "boundary": "Synthetic raw draft preservation only. Actual native digest recomputed with Approval owner Python binding_digest; no Go canonicalization implementation or source-origin trust. Does not prove typed Access signed-envelope preservation, registry/compiler correspondence, trusted provider facts or positive eligibility." +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/run-representation-probe.py b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/run-representation-probe.py new file mode 100644 index 00000000..4be0bcff --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b2-preparation/run-representation-probe.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Producer-vector experiment; no runtime source mutation or credential access.""" +import argparse,hashlib,json,os,subprocess,sys +from pathlib import Path +p=argparse.ArgumentParser();p.add_argument('--approval-source',type=Path,required=True);p.add_argument('--vectors',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() +sys.path.insert(0,str(a.approval_source));from approval_engine.binding import binding_digest +vectors=json.loads(a.vectors.read_bytes());env={k:os.environ[k] for k in ('PATH','HOME') if k in os.environ};env.update(GOCACHE='/tmp/netkingdom-go-cache',GOFLAGS='-buildvcs=false',GOMAXPROCS='2') +probe=Path(__file__).with_name('raw-representation-probe.go') +rows=json.loads(subprocess.check_output(['go','run',str(probe)],input=a.vectors.read_bytes(),env=env)) +results=[] +for vector,row in zip(vectors['native_binding_vectors'],rows,strict=True): + def owner_digest(target): + return binding_digest({**vector['binding'],'target':json.loads(target)}) + raw_digest=binding_digest(json.loads(row['raw_message_act_draft'])) + default_digest=owner_digest(row['go_default_target']);number_digest=owner_digest(row['use_number_target']) + assert raw_digest==vector['native_digest'];assert number_digest==vector['native_digest'] + results.append({'input_target_json':vector['input_target_json'],'producer_native_digest':vector['native_digest'],'raw_message_draft_matches_owner':True,'use_number_roundtrip_matches_owner':True,'default_float64_roundtrip_matches_owner':default_digest==vector['native_digest'],'go_default_target':row['go_default_target'],'go_raw_draft':row['raw_message_act_draft']}) +assert any(not x['default_float64_roundtrip_matches_owner'] for x in results) +result={'schema':'access-engine.raw-native-representation-experiment.v1','source_vectors_sha256':hashlib.sha256(a.vectors.read_bytes()).hexdigest(),'source_vectors_producer_revision':vectors['producer_revision'],'cases':results,'raw_preservation_passed':len(results),'default_roundtrip_mismatches':sum(not x['default_float64_roundtrip_matches_owner'] for x in results),'boundary':'Synthetic raw draft preservation only. Actual native digest recomputed with Approval owner Python binding_digest; no Go canonicalization implementation or source-origin trust. Does not prove typed Access signed-envelope preservation, registry/compiler correspondence, trusted provider facts or positive eligibility.'} +a.output.write_text(json.dumps(result,indent=2)+'\n');print({'raw_preservation_passed':len(results),'default_roundtrip_mismatches':result['default_roundtrip_mismatches']}) diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/existing-ci-public-metadata-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-source/existing-ci-public-metadata-r1.json new file mode 100644 index 00000000..462b7b1d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/existing-ci-public-metadata-r1.json @@ -0,0 +1,28 @@ +{ + "schema": "access-engine.ci-public-metadata-read.v1", + "observed_at": "2026-10-16T07:34:51.204550+00:00", + "credential_use": "none; anonymous requests only, no auth challenge followup", + "queries": { + "registry_manifest": { + "status": 401, + "url": "https://forgejo.coulomb.social/v2/coulomb/flex-auth/manifests/main-ec21518", + "auth_challenge_present": true + }, + "workflow_runs": { + "status": 404, + "url": "https://forgejo.coulomb.social/api/v1/repos/coulomb/access-engine/actions/runs?limit=20", + "auth_challenge_present": false + }, + "package_versions": { + "status": 401, + "url": "https://forgejo.coulomb.social/api/v1/packages/coulomb?type=container&q=flex-auth&limit=50", + "auth_challenge_present": false + } + }, + "local_cached_image_metadata": { + "image": "forgejo.coulomb.social/coulomb/flex-auth:main-ec21518", + "exit_code": 1, + "output": null, + "stderr_present": true + } +}