diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/README.md b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/README.md new file mode 100644 index 00000000..42ec5d52 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/README.md @@ -0,0 +1,37 @@ +# Access signer rollout preparation R1 + +Existing work: FLEX-WP-0024-T06. Access deployment preparation only; no cluster/GitOps owner assent, founder approval, custody, trust admission or live action is asserted. Source checks pass. All native gates remain open. + +Exact source is Access `84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, rendered from a disposable `git archive`, not the working tree. `render-evidence.json` pins every chart/value/policy/registry input and output. The image receipt at `../image-84c78e0/candidate-receipt.json` independently records ordinary main-push CI402 success, pulled source-label verification and public registry agreement for `forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. Historical run400 failure remains retained separately. + +The canonical local managed package is d52f811/tree d4fbad7, readiness draft, no reef bindings. Its extraction2b/current05a pin remains unchanged. This is a proposed separately reviewed signer successor, not execution through the extraction-only package command and not proof of central/private availability or placement. No owner repository was modified. + +`activation-values.proposed.yaml` overlays exact retained `values/secrets-engine.yaml` using current `charts/flex-auth`. `source-render.diff` shows all changes against the same chart's retained render: immutable image, signer mount/flags, single-writer Recreate, bounded durable outbox and a new PVC. Service, ServiceAccount, RBAC, NetworkPolicy and ingress posture are byte-equivalent as parsed objects. Existing caller enforce/audience flex-auth/SA binding, lifecycle policy/registry paths and resource requests/limits remain equal. Audit delivery stays disabled; no task source is enabled; no Secret is generated. Kid and seed Secret name are proposed coordinates, not current custody or public trust admission. + +Reproduce locally: + +```sh +python3 docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/prepare.py --image-receipt docs/changes/netkingdom-target-2026-10-16/access-native-admission/image-84c78e0/candidate-receipt.json +``` + +`source-deployment-projection.json` is produced with the actual Platform native adapter `deployment_projection` and `encoded` functions, whose module hash is pinned. Its source-rendered SHA256 is in `render-evidence.json`. It includes fixed args, immutable image, SA, UID/GID/fsGroup65532, seed0440 (`288` decimal) directory mount, reviewer projection and PVC mount. Both proposed stopped and ready variants have the same projection. This is not a native stopped-fence receipt: the exact future observed stopped Deployment projection must equal this hash before Platform can use it. Kubernetes defaulting/admission mutation or any drift must be reconciled and reviewed, not silently normalized. + +The chart intentionally rejects outbox replicaCount other than1. `stopped-source-render.yaml` is a proposed maintenance state derived from the one-replica render by changing only Deployment replicas to0; it is not a claim that Helm can render replicas0. The future Access/cluster reviewed release path must install the candidate pod template while stopped without letting Helm or another writer restart it. No apply, scale, Helm command or native dry-run was executed. Source rendering is not server dry-run or native diff. + +## Proposed exact native order and ownership boundaries + +1. Founder and distinct Access, Platform, Secrets, cluster/GitOps owners review the exact successor scope and source/image/config hashes. Capture the actual prechange Helm revision/history and allowlisted nonsecret config. Raw Helm values, Secret objects/annotations/errors, tokens, private key material and credential-bearing logs must not enter shared evidence. Refresh verified node92.205.62.239, kube-system UIDa553c742-0115-43d4-99a4-a5ca56fe0786, exact Deployment UID, namespace, chart/release/controller ownership, current CPU/memory/free disk and namespace resource quota/limit-range disposition. Existing metadata is dated15:20/15:25UTC, not a current headroom claim. +2. Open an independently reviewed exclusive maintenance window. Founder abort owner; Access controls release and protected consumers; cluster owner freezes competing Helm/GitOps sync and other writers under its own authority. Record actual controller identities, prior state and restoration criteria; no controller identity is invented here. Stop affected protected consumer acceptance first. Refuse any HPA target or second signer workload. Install exact reviewed candidate template at replicas0 through the separately approved maintenance path. Wait for observedGeneration to catch up and no Deployment replicas, live/terminating owned pods, signer-SA pods or seed-mount pods. Retain owned ReplicaSets at replicas0 and verify by ownerReference UID, not labels alone. Hold fence through custody/public trust/delivery; recheck immediately before CAS. +3. Platform's separate attended action requires its real admitted CCR0050 read lane, five exact scope-bound independent dispositions, exact founder identity, absolute expiry, current source/image/projection/window/trust hashes and independently authenticated receipts. This artifact supplies none of those approvals. The writer generates its seed in memory; agents do not read/generate it. Ambiguous outcomes preserve fence and unknown receipt; no CAS0 replay or remint. +4. Independently authenticate Platform public receipt and Secrets public admission before reopening signed acceptance. Platform owns delivery manifests/role readback. Access owns seed mount exact Secret/key0440 and startup kid; Secrets owns exact installed bundle/verifier/trust/caller. Candidate64MiB bounded outbox must use the proposed PVC. Prepare source-only PVC request now; creation/binding remains separately authorized. WaitForFirstConsumer means no PV identity can be predicted now. Allow the reviewed binding/start step with consumers still fenced, capture actual claim/PV UIDs and node affinity, and have cluster owner set and verify the exact bound PV reclaim policy Retain before durable acceptance. +5. Under exact reviewed action restore replicaCount1 with Recreate and verify no concurrent writer. Require Ready within120s and total recovery decision within10m. Failures keep consumers fenced. Refresh image/pod UID/SA/startup flags/mounts/UID/GID and exact source-rendered projection equality, then capture native signed lifecycle ALLOW and DENY plus original durable records using the independent Secrets verifier. Require wrong/missing/expired/audience/subject caller refusals, wrong-tenant and task refusal, restart signature/sequence/cursor preservation and separately admitted lock/capacity/unwritable/fsync negative checks. No protected backend effect or audit receiver acceptance is part of this initial proof. + +## Storage and rollback + +The exact PVC request is `flex-auth/flex-auth-secrets-engine-outbox`, `1Gi` (1073741824 bytes), `ReadWriteOnce`, `local-path`, Helm `resource-policy: keep`. Existing receipt showed zero namespace PVCs, local-path WaitForFirstConsumer/Delete. Namespace storage quota and actual free filesystem bytes are unverified and remain native preflight gates. Requested capacity is neither a hard filesystem quota nor backup/node-loss protection. The process source hard ceiling is67108864 bytes (64MiB); storage class behavior does not enforce it. Exact requested storage versus namespace remaining quota and actual node free bytes must be recorded before create; no synthetic quota limit is asserted. + +Helmkeep protects against Helm deletion, not Kubernetes manual deletion or local-path PV Delete. Review actual bound PV name/UID/claimRef UID, capacity, node affinity and Retain mutation; do not guess a PV UID/name or patch an arbitrary claim-selected object. No PVC/PV is created and no Retain mutation is performed here. Backup/restore and independent evidence continuity remain unverified. + +Rollback: fence consumers, stop candidate Deployment, observe all owned/SA/seed-mount pods gone and single-writer lock released, retain PVC/logs/cursor/signatures/receipts. Record candidate failure. Only restore the captured prior Helm revision after format compatibility and exact image/config are independently reviewed. Prechange revision number is currently unknown, so no executable rollback with a guessed revision is supplied. Prior05a restores unsigned evaluation availability only and cannot satisfy the strict signed consumer; do not mount or mutate committed new outbox evidence with an unreviewed predecessor. If compatibility or exclusion is uncertain, stay unavailable with evidence preserved. Never uninstall, delete PVC/PV, reset cursor/log, weaken caller/trust/policy or restore an old seed. Key compromise requires public trust withdrawal and separately reviewed forward-only rotation. After CAS ambiguity retain original unknown outcome and use Platform's fresh reviewed read-only recovery. + +Source preparation adds EUR0 infrastructure and makes no resource allocation. Proposed native addition is one1Gi local-path request on the existing node, current25m CPU/32Mi request and300m CPU/192Mi limits unchanged. Native spend approval and a quantified loss ceiling are absent. Availability interruption, node-local evidence loss and forged lifecycle decisions after signer compromise require founder disposition; no autopilot grant follows from this proposal. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-source-render.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-source-render.yaml new file mode 100644 index 00000000..5c6b53e0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-source-render.yaml @@ -0,0 +1,237 @@ +--- +# Source: flex-auth/templates/networkpolicy.yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + policyTypes: + - Ingress + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: secrets-engine + podSelector: + matchLabels: + app.kubernetes.io/name: secrets-engine + ports: + - port: 8080 + protocol: TCP +--- +# Source: flex-auth/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +automountServiceAccountToken: false +--- +# Source: flex-auth/templates/outbox-pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: flex-auth-secrets-engine-outbox + annotations: + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + accessModes: + - ReadWriteOnce + storageClassName: "local-path" + resources: + requests: + storage: "1Gi" +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flex-auth-secrets-engine-tokenreviewer +subjects: + - kind: ServiceAccount + name: flex-auth-secrets-engine + namespace: flex-auth +--- +# Source: flex-auth/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + selector: + app.kubernetes.io/name: flex-auth-secrets-engine + ports: + - name: http + port: 8080 + targetPort: http +--- +# Source: flex-auth/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + template: + metadata: + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: + automountServiceAccountToken: false + serviceAccountName: flex-auth-secrets-engine + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f + imagePullPolicy: IfNotPresent + args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md + - --envelope-signing-key-file + - "/var/run/secrets/decision-envelope-signing/seed" + - --envelope-signing-kid + - "flex-auth-lifecycle-20261016-r1" + - --outbox-max-bytes + - "67108864" + - --outbox-dir + - "/var/lib/flex-auth/outbox" + - --emission-source + - "flex-auth.secrets-engine" + - --caller-audience + - "flex-auth" + - --caller-auth-mode + - "enforce" + - --caller-kubernetes-url + - "https://10.43.0.1" + - --caller-binding + - "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine" + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: "/var/run/secrets/decision-envelope-signing" + name: flex-auth-envelope-signing + readOnly: true + - mountPath: "/var/lib/flex-auth/outbox" + name: flex-auth-outbox + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: + - name: flex-auth-envelope-signing + secret: + secretName: "flex-auth-secrets-engine-envelope-signing" + defaultMode: 0440 + items: + - key: "seed" + path: seed + - name: flex-auth-outbox + persistentVolumeClaim: + claimName: "flex-auth-secrets-engine-outbox" + - name: flex-auth-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-values.proposed.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-values.proposed.yaml new file mode 100644 index 00000000..96960c73 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/activation-values.proposed.yaml @@ -0,0 +1,20 @@ +image: + repository: forgejo.coulomb.social/coulomb/flex-auth + digest: sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f +replicaCount: 1 +envelopeSigning: + enabled: true + kid: flex-auth-lifecycle-20261016-r1 + secretName: flex-auth-secrets-engine-envelope-signing + secretKey: seed + mountPath: /var/run/secrets/decision-envelope-signing +outbox: + enabled: true + source: flex-auth.secrets-engine + mountPath: /var/lib/flex-auth/outbox + size: 1Gi + maxBytes: 67108864 + storageClassName: local-path + existingClaim: '' +auditDelivery: + enabled: false diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/exclusive-window.proposed.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/exclusive-window.proposed.json new file mode 100644 index 00000000..8fa50a1d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/exclusive-window.proposed.json @@ -0,0 +1,60 @@ +{ + "schema": "access-engine.signer-exclusive-window-proposal.v1", + "proposal_id": "FLEX24-SIGNER-ROLLOUT-20261016-R1", + "status": "proposed-incomplete-native-window-not-held", + "owner_work": "FLEX-WP-0024-T06", + "target": { + "cluster": "railiance01", + "node_ip": "92.205.62.239", + "kube_system_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "namespace": "flex-auth", + "deployment": "flex-auth-secrets-engine", + "observed_predecessor_deployment_uid": "f83a5f5c-7dd7-4aac-bc02-d447e423e6ae", + "service_account": "flex-auth-secrets-engine" + }, + "executor": null, + "abort_owner": "founder", + "access_preparation_owner": "access-engine", + "cluster_gitops_disposition": null, + "founder_disposition": null, + "absolute_window_start": null, + "absolute_window_end": null, + "ready_timeout_seconds": 120, + "recovery_decision_seconds": 600, + "competing_controllers": null, + "actual_prechange_helm_revision": null, + "consumer_stop_receipt": null, + "native_stopped_fence_receipt": null, + "conditions": [ + "Protected consumers fenced before template/stop or custody changes", + "Exact Deployment replicas0; observedGeneration equals generation; no status replicas", + "All owned ReplicaSets spec/status replicas0; no live or terminating owned pods", + "No signer-SA or seed-mount pods anywhere in target namespace; no HPA targeting signer", + "Freeze each actual competing Helm/GitOps/automation writer; record exact identity/prior state/restore criteria", + "Native stopped Deployment projection equals reviewed source projection immediately before custody CAS", + "Remain fenced until independently authenticated public trust/delivery/runtime proof", + "Any uncertain write/readback/identity/volume or readiness outcome keeps fence closed" + ], + "storage": { + "pvc": "flex-auth-secrets-engine-outbox", + "request": "1Gi", + "request_bytes": 1073741824, + "outbox_hard_ceiling_bytes": 67108864, + "storage_class": "local-path", + "helm_keep": true, + "pv_reclaim_required": "Retain", + "pv_name": null, + "pv_uid": null, + "claim_uid": null, + "namespace_storage_quota_remaining_bytes": null, + "node_free_filesystem_bytes": null, + "binding": "WaitForFirstConsumer; exact actual binding and Retain verification required before durable acceptance" + }, + "cost": { + "source_preparation_incremental_eur": 0, + "native_spend_authority": false, + "native_incremental_resources": "one1Gi local-path request; unchanged25m/32Mi workload request", + "risk_loss_ceiling_eur": null + }, + "authority_granted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/manifest.json new file mode 100644 index 00000000..7adb758e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/manifest.json @@ -0,0 +1,20 @@ +{ + "schema": "access-engine.signer-rollout-preparation.v1", + "proposal_id": "FLEX24-SIGNER-ROLLOUT-20261016-R1", + "role": "access-engine-deployment-preparation-only", + "disposition": "source-checks-pass-native-gates-open", + "cluster_gitops_assent": false, + "execution_authorized": false, + "files": { + "README.md": "sha256:09ea9c7c9026fdd0cb5f995a440b5b71be08b9bad1145df1057a58acecc56da8", + "activation-source-render.yaml": "sha256:b7edecb8ec50a6d2db7623a3d962dbece46de81d3809d5f73b3aca20ff76b8c7", + "activation-values.proposed.yaml": "sha256:e76a30967add800ed8b588368757e7a5fa0ce4d8305ef658b5a4a08e377eb5b8", + "exclusive-window.proposed.json": "sha256:98ad9c7a5661ce7b3a47cad061a03c6d55ca200b54c96c3f257b644c0739bd19", + "prepare.py": "sha256:d15db1603808e1881d6244c536588d5dd255c7b02a95fbd313ff75ea51e752b7", + "render-evidence.json": "sha256:07363f3003cede57d992e7edab2d96e7012e01ccf31fc55f50a6a4ed74cd3bcf", + "retained-source-render.yaml": "sha256:f979f7d05d5b0a54e3b8ac7c413aa4d401139f991b43412cdbde3d63249b0dd9", + "source-deployment-projection.json": "sha256:056b6bf842713fb9a328dae901401d6867da47da68bfb8a4904bb0d7b8d9e912", + "source-render.diff": "sha256:77480a093011aba0e75afc49bc32c6d9ec4b4a9131d87b201f0d5cea9fc15888", + "stopped-source-render.yaml": "sha256:be1c431b07477f4d19baf24874a23556c594328b2bb2b5f52af8ccc483d44133" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/prepare.py b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/prepare.py new file mode 100644 index 00000000..f84ac06f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/prepare.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Offline source render only. Image receipt is deliberately a separate input.""" +import argparse +import difflib +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import re +import subprocess +import sys +import tarfile +import tempfile +import yaml + +HERE = Path(__file__).resolve().parent +SOURCE = Path('/home/worsch/access-engine') +REVISION = '84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0' +NATIVE = Path('/home/worsch/railiance-platform/scripts/flex_auth_signer_native.py') +PATHS = ['charts/flex-auth', 'values/secrets-engine.yaml', + 'examples/secrets-engine/policy_package.md', 'examples/secrets-engine/registry_snapshot.json'] + +def sha(data): + return 'sha256:' + hashlib.sha256(data).hexdigest() + +def write_json(name, value): + (HERE / name).write_text(json.dumps(value, indent=2) + '\n') + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--image-receipt', type=Path) + args = parser.parse_args() + receipt = json.loads(args.image_receipt.read_text()) if args.image_receipt else None + # A pending shape render has no immutable candidate and is unusable for admission. + digest = 'sha256:' + '0' * 64 + if receipt: + assert receipt['source_revision'] == REVISION + assert receipt['ordinary_push'] is True and receipt['terminal_status'] == 'success' + digest = receipt['manifest_digest'] + assert receipt['publisher_receipt']['revision'] == REVISION + assert receipt['independent_public_registry_package']['explicit_revision_label'] == REVISION + assert receipt['independent_public_registry_package']['sole_manifest_digest'] == digest + assert re.fullmatch(r'sha256:[0-9a-f]{64}', digest) and digest != 'sha256:' + '0' * 64 + overlay = { + 'image': {'repository': 'forgejo.coulomb.social/coulomb/flex-auth', 'digest': digest}, + 'replicaCount': 1, + 'envelopeSigning': {'enabled': True, 'kid': 'flex-auth-lifecycle-20261016-r1', + 'secretName': 'flex-auth-secrets-engine-envelope-signing', 'secretKey': 'seed', + 'mountPath': '/var/run/secrets/decision-envelope-signing'}, + 'outbox': {'enabled': True, 'source': 'flex-auth.secrets-engine', + 'mountPath': '/var/lib/flex-auth/outbox', 'size': '1Gi', 'maxBytes': 67108864, + 'storageClassName': 'local-path', 'existingClaim': ''}, + 'auditDelivery': {'enabled': False}, + } + (HERE / 'activation-values.proposed.yaml').write_text(yaml.safe_dump(overlay, sort_keys=False)) + archive = subprocess.run(['git', '-C', str(SOURCE), 'archive', REVISION, *PATHS], + capture_output=True, check=True).stdout + with tempfile.TemporaryDirectory(prefix='access-signer-rollout-r1-') as directory: + source = Path(directory) + with tarfile.open(fileobj=io.BytesIO(archive)) as tar: + files = {m.name: sha(tar.extractfile(m).read()) for m in tar.getmembers() if m.isfile()} + tar.extractall(source, filter='data') + base = ['helm', 'template', 'flex-auth-secrets-engine', str(source / 'charts/flex-auth'), + '--namespace', 'flex-auth', '-f', str(source / 'values/secrets-engine.yaml')] + retained = subprocess.run(base, capture_output=True, check=True, text=True).stdout + def render(replicas): + return subprocess.run(base + ['-f', str(HERE / 'activation-values.proposed.yaml'), + '--set', 'replicaCount=' + str(replicas)], capture_output=True, check=True, text=True).stdout + ready = render(1) + # Chart intentionally refuses outbox replicas!=1. Stopping is a distinct + # owner-approved scale action, not an unsafe relaxation of that guard. + stopped_docs = [d for d in yaml.safe_load_all(ready) if d] + next(d for d in stopped_docs if d['kind'] == 'Deployment')['spec']['replicas'] = 0 + stopped = yaml.safe_dump_all(stopped_docs, sort_keys=False) + (HERE / 'retained-source-render.yaml').write_text(retained) + (HERE / 'activation-source-render.yaml').write_text(ready) + (HERE / 'stopped-source-render.yaml').write_text(stopped) + (HERE / 'source-render.diff').write_text(''.join(difflib.unified_diff(retained.splitlines(True), + ready.splitlines(True), fromfile='retained84', tofile='proposed-signer84'))) + # Call the actual committed Platform algorithm, rather than copy its projection. + sys.path.insert(0, str(NATIVE.parent)) + spec = importlib.util.spec_from_file_location('rollout_native_projection', NATIVE) + native = importlib.util.module_from_spec(spec) + spec.loader.exec_module(native) + docs = [d for d in yaml.safe_load_all(ready) if d] + dep = next(d for d in docs if d['kind'] == 'Deployment') + stopped_dep = next(d for d in yaml.safe_load_all(stopped) if d and d['kind'] == 'Deployment') + projection = native.deployment_projection(dep) + assert projection == native.deployment_projection(stopped_dep) + c = dep['spec']['template']['spec']['containers'][0] + old = next(d for d in yaml.safe_load_all(retained) if d and d['kind'] == 'Deployment') + oldc = old['spec']['template']['spec']['containers'][0] + assert c['resources'] == oldc['resources'] + for flag in ['--caller-audience', '--caller-auth-mode', '--caller-binding', '--caller-kubernetes-url', '--policy', '--registry']: + assert c['args'][c['args'].index(flag)+1] == oldc['args'][oldc['args'].index(flag)+1] + assert dep['spec']['strategy'] == {'type': 'Recreate'} + assert all(d['kind'] != 'Secret' for d in docs) + assert all(d['kind'] != 'Ingress' for d in docs) + other = lambda ds: [d for d in ds if d['kind'] not in ('Deployment', 'PersistentVolumeClaim')] + assert other(docs) == other([d for d in yaml.safe_load_all(retained) if d]) + write_json('source-deployment-projection.json', projection) + write_json('render-evidence.json', { + 'schema': 'access-engine.signer-rollout-source-render.v1', + 'source_revision': REVISION, 'source_files': files, + 'package_revision': 'd52f811ed5ec0be27c9c367e71d476125b3228be', + 'package_baseline': 'extraction2b/current05a; unchanged, not this activation successor', + 'image_state': 'verified-ordinary-ci-source-candidate' if receipt else 'pending; all-zero image is noncandidate shape sentinel', + 'image_receipt_sha256': sha(args.image_receipt.read_bytes()) if receipt else None, + 'source_rendered_projection_sha256': native.digest(native.encoded(projection)), + 'deployment_projection_sha256': native.digest(native.encoded(projection)) if receipt else None, + 'projection_algorithm_sha256': sha(NATIVE.read_bytes()), + 'native_stopped_projection_verified': False, + 'native_admission': False, 'execution_authorized': False, + 'source_checks': {'caller_policy_registry_resources_unchanged': True, + 'nondeployment_resources_unchanged_except_new_pvc': True, + 'replicas0_and1_projection_equal': True, 'no_secret_generated': True}, + 'artifacts': {name: sha((HERE / name).read_bytes()) for name in [ + 'activation-values.proposed.yaml', 'retained-source-render.yaml', 'activation-source-render.yaml', + 'stopped-source-render.yaml', 'source-render.diff', 'source-deployment-projection.json']}, + }) + +if __name__ == '__main__': + main() diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/render-evidence.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/render-evidence.json new file mode 100644 index 00000000..6fc50404 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/render-evidence.json @@ -0,0 +1,42 @@ +{ + "schema": "access-engine.signer-rollout-source-render.v1", + "source_revision": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0", + "source_files": { + "charts/flex-auth/Chart.yaml": "sha256:e8e00d968a8ba2783f81312cebe27f0315e29f0b663033886d4104bab6a1b455", + "charts/flex-auth/templates/_helpers.tpl": "sha256:1e4a3ef486202b54d245060d2ee357880ee5b117732b2152bb4f75b489e0dc81", + "charts/flex-auth/templates/deployment.yaml": "sha256:b347614283e65d9e8006a93dfbdfec15360632f6c318761e245213460e158666", + "charts/flex-auth/templates/networkpolicy.yaml": "sha256:53d8ae1851b66c8c9f990fc56bc8ad9076741e2684f39470a7e1583b093d9c14", + "charts/flex-auth/templates/outbox-pvc.yaml": "sha256:284a78b0fd10537490f4f62e58c0cd85eb07b4222e932eb098c1bc5ddda8387d", + "charts/flex-auth/templates/rbac.yaml": "sha256:07033364ec9624f25e0a4ebaac9c29bc25edac93fde2c514fd946596e428e580", + "charts/flex-auth/templates/service.yaml": "sha256:9af858c02268d0492352ebe09efe7b6750711d48624a054b05e85707bf6a4e97", + "charts/flex-auth/templates/serviceaccount.yaml": "sha256:a62caa647b4a55f0092214c6a200bd3a6f34fa97d8989c55a387429a487bd6c8", + "charts/flex-auth/values.yaml": "sha256:0d5c3fa0c52d87ff6350d8f92527814b74bb8b7adadc92ded71856d9c83b1b03", + "examples/secrets-engine/policy_package.md": "sha256:657fa9312c9dcabee059ec31c89ee13396fe9aa54d852edc9e6559a74bbc139a", + "examples/secrets-engine/registry_snapshot.json": "sha256:37fba44ec0e0fd9b1e17a05ad1b7e4f9bb6ec9e0fc0bb3133e8af2ac6f832d7c", + "values/secrets-engine.yaml": "sha256:567189d14467815a77df5ad7e0f22e7184c8e5bb9f814cbc0fbb99102b3cfd55" + }, + "package_revision": "d52f811ed5ec0be27c9c367e71d476125b3228be", + "package_baseline": "extraction2b/current05a; unchanged, not this activation successor", + "image_state": "verified-ordinary-ci-source-candidate", + "image_receipt_sha256": "sha256:af8e032d28776722ccf2bfe0f884577e568d8d0fb3713aa869e1d7d1c77c576f", + "source_rendered_projection_sha256": "sha256:8ce375f065552c551538f3acc7300c427407057eeff1d0b43755bfdf8a46169c", + "deployment_projection_sha256": "sha256:8ce375f065552c551538f3acc7300c427407057eeff1d0b43755bfdf8a46169c", + "projection_algorithm_sha256": "sha256:5a3b3b33bcad45ef2939413bfd3db76e5ee70e3b815d0d64af53147851aa3a0d", + "native_stopped_projection_verified": false, + "native_admission": false, + "execution_authorized": false, + "source_checks": { + "caller_policy_registry_resources_unchanged": true, + "nondeployment_resources_unchanged_except_new_pvc": true, + "replicas0_and1_projection_equal": true, + "no_secret_generated": true + }, + "artifacts": { + "activation-values.proposed.yaml": "sha256:e76a30967add800ed8b588368757e7a5fa0ce4d8305ef658b5a4a08e377eb5b8", + "retained-source-render.yaml": "sha256:f979f7d05d5b0a54e3b8ac7c413aa4d401139f991b43412cdbde3d63249b0dd9", + "activation-source-render.yaml": "sha256:b7edecb8ec50a6d2db7623a3d962dbece46de81d3809d5f73b3aca20ff76b8c7", + "stopped-source-render.yaml": "sha256:be1c431b07477f4d19baf24874a23556c594328b2bb2b5f52af8ccc483d44133", + "source-render.diff": "sha256:77480a093011aba0e75afc49bc32c6d9ec4b4a9131d87b201f0d5cea9fc15888", + "source-deployment-projection.json": "sha256:056b6bf842713fb9a328dae901401d6867da47da68bfb8a4904bb0d7b8d9e912" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/retained-source-render.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/retained-source-render.yaml new file mode 100644 index 00000000..44d6f4e4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/retained-source-render.yaml @@ -0,0 +1,191 @@ +--- +# Source: flex-auth/templates/networkpolicy.yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + policyTypes: + - Ingress + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: secrets-engine + podSelector: + matchLabels: + app.kubernetes.io/name: secrets-engine + ports: + - port: 8080 + protocol: TCP +--- +# Source: flex-auth/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +automountServiceAccountToken: false +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flex-auth-secrets-engine-tokenreviewer +subjects: + - kind: ServiceAccount + name: flex-auth-secrets-engine + namespace: flex-auth +--- +# Source: flex-auth/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + selector: + app.kubernetes.io/name: flex-auth-secrets-engine + ports: + - name: http + port: 8080 + targetPort: http +--- +# Source: flex-auth/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + template: + metadata: + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: + automountServiceAccountToken: false + serviceAccountName: flex-auth-secrets-engine + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd + imagePullPolicy: IfNotPresent + args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md + - --caller-audience + - "flex-auth" + - --caller-auth-mode + - "enforce" + - --caller-kubernetes-url + - "https://10.43.0.1" + - --caller-binding + - "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine" + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: + - name: flex-auth-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-deployment-projection.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-deployment-projection.json new file mode 100644 index 00000000..6882e3a3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-deployment-projection.json @@ -0,0 +1,102 @@ +{ + "service_account": "flex-auth-secrets-engine", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f", + "args": [ + "serve", + "--addr", + "0.0.0.0:8080", + "--registry", + "/opt/flex-auth/examples/secrets-engine/registry_snapshot.json", + "--policy", + "/opt/flex-auth/examples/secrets-engine/policy_package.md", + "--envelope-signing-key-file", + "/var/run/secrets/decision-envelope-signing/seed", + "--envelope-signing-kid", + "flex-auth-lifecycle-20261016-r1", + "--outbox-max-bytes", + "67108864", + "--outbox-dir", + "/var/lib/flex-auth/outbox", + "--emission-source", + "flex-auth.secrets-engine", + "--caller-audience", + "flex-auth", + "--caller-auth-mode", + "enforce", + "--caller-kubernetes-url", + "https://10.43.0.1", + "--caller-binding", + "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine" + ], + "volume_mounts": [ + { + "mountPath": "/var/run/secrets/decision-envelope-signing", + "name": "flex-auth-envelope-signing", + "readOnly": true + }, + { + "mountPath": "/var/lib/flex-auth/outbox", + "name": "flex-auth-outbox" + }, + { + "mountPath": "/var/run/secrets/flex-auth-reviewer", + "name": "flex-auth-reviewer", + "readOnly": true + } + ], + "volumes": [ + { + "name": "flex-auth-envelope-signing", + "secret": { + "secretName": "flex-auth-secrets-engine-envelope-signing", + "defaultMode": 288, + "items": [ + { + "key": "seed", + "path": "seed" + } + ] + } + }, + { + "name": "flex-auth-outbox", + "persistentVolumeClaim": { + "claimName": "flex-auth-secrets-engine-outbox" + } + }, + { + "name": "flex-auth-reviewer", + "projected": { + "defaultMode": 288, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3600, + "path": "token" + } + }, + { + "configMap": { + "name": "kube-root-ca.crt", + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ] + } + } + ] + } + } + ], + "security_context": { + "runAsNonRoot": true, + "runAsUser": 65532, + "runAsGroup": 65532, + "fsGroup": 65532, + "seccompProfile": { + "type": "RuntimeDefault" + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-render.diff b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-render.diff new file mode 100644 index 00000000..cd6f4a94 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/source-render.diff @@ -0,0 +1,89 @@ +--- retained84 ++++ proposed-signer84 +@@ -44,6 +44,25 @@ + app.kubernetes.io/part-of: flex-auth + automountServiceAccountToken: false + --- ++# Source: flex-auth/templates/outbox-pvc.yaml ++apiVersion: v1 ++kind: PersistentVolumeClaim ++metadata: ++ name: flex-auth-secrets-engine-outbox ++ annotations: ++ helm.sh/resource-policy: keep ++ labels: ++ app.kubernetes.io/name: flex-auth-secrets-engine ++ app.kubernetes.io/instance: flex-auth-secrets-engine ++ app.kubernetes.io/part-of: flex-auth ++spec: ++ accessModes: ++ - ReadWriteOnce ++ storageClassName: "local-path" ++ resources: ++ requests: ++ storage: "1Gi" ++--- + # Source: flex-auth/templates/rbac.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole +@@ -106,6 +125,8 @@ + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: ++ strategy: ++ type: Recreate + replicas: 1 + selector: + matchLabels: +@@ -128,7 +149,7 @@ + type: RuntimeDefault + containers: + - name: flex-auth +- image: forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd ++ image: forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f + imagePullPolicy: IfNotPresent + args: + - serve +@@ -138,6 +159,16 @@ + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md ++ - --envelope-signing-key-file ++ - "/var/run/secrets/decision-envelope-signing/seed" ++ - --envelope-signing-kid ++ - "flex-auth-lifecycle-20261016-r1" ++ - --outbox-max-bytes ++ - "67108864" ++ - --outbox-dir ++ - "/var/lib/flex-auth/outbox" ++ - --emission-source ++ - "flex-auth.secrets-engine" + - --caller-audience + - "flex-auth" + - --caller-auth-mode +@@ -173,10 +204,25 @@ + - ALL + readOnlyRootFilesystem: true + volumeMounts: ++ - mountPath: "/var/run/secrets/decision-envelope-signing" ++ name: flex-auth-envelope-signing ++ readOnly: true ++ - mountPath: "/var/lib/flex-auth/outbox" ++ name: flex-auth-outbox + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: ++ - name: flex-auth-envelope-signing ++ secret: ++ secretName: "flex-auth-secrets-engine-envelope-signing" ++ defaultMode: 0440 ++ items: ++ - key: "seed" ++ path: seed ++ - name: flex-auth-outbox ++ persistentVolumeClaim: ++ claimName: "flex-auth-secrets-engine-outbox" + - name: flex-auth-reviewer + projected: + defaultMode: 0440 diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/stopped-source-render.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/stopped-source-render.yaml new file mode 100644 index 00000000..ba172471 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/rollout-r1/stopped-source-render.yaml @@ -0,0 +1,229 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + policyTypes: + - Ingress + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: secrets-engine + podSelector: + matchLabels: + app.kubernetes.io/name: secrets-engine + ports: + - port: 8080 + protocol: TCP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: flex-auth-secrets-engine-outbox + annotations: + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 1Gi +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flex-auth-secrets-engine-tokenreviewer +subjects: +- kind: ServiceAccount + name: flex-auth-secrets-engine + namespace: flex-auth +--- +apiVersion: v1 +kind: Service +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + selector: + app.kubernetes.io/name: flex-auth-secrets-engine + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + strategy: + type: Recreate + replicas: 0 + selector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + template: + metadata: + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: + automountServiceAccountToken: false + serviceAccountName: flex-auth-secrets-engine + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f + imagePullPolicy: IfNotPresent + args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md + - --envelope-signing-key-file + - /var/run/secrets/decision-envelope-signing/seed + - --envelope-signing-kid + - flex-auth-lifecycle-20261016-r1 + - --outbox-max-bytes + - '67108864' + - --outbox-dir + - /var/lib/flex-auth/outbox + - --emission-source + - flex-auth.secrets-engine + - --caller-audience + - flex-auth + - --caller-auth-mode + - enforce + - --caller-kubernetes-url + - https://10.43.0.1 + - --caller-binding + - secrets-engine=system:serviceaccount:secrets-engine:secrets-engine + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /var/run/secrets/decision-envelope-signing + name: flex-auth-envelope-signing + readOnly: true + - mountPath: /var/lib/flex-auth/outbox + name: flex-auth-outbox + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: + - name: flex-auth-envelope-signing + secret: + secretName: flex-auth-secrets-engine-envelope-signing + defaultMode: 288 + items: + - key: seed + path: seed + - name: flex-auth-outbox + persistentVolumeClaim: + claimName: flex-auth-secrets-engine-outbox + - name: flex-auth-reviewer + projected: + defaultMode: 288 + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/runtime-metadata.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/runtime-metadata.json new file mode 100644 index 00000000..68080e84 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/runtime-metadata.json @@ -0,0 +1,104 @@ +{ + "observed_at": "2026-10-16T15:20:06.611593+00:00", + "scope": "metadata-only; no credentials, Secret objects, logs, exec or writes", + "queries": { + "nodes": { + "command": [ + "kubectl", + "get", + "nodes", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.status.addresses}{\" \"}{.status.allocatable}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "239.62.205.92.host.secureserver.net [{\"address\":\"92.205.62.239\",\"type\":\"InternalIP\"},{\"address\":\"2a00:1169:111:6d70::\",\"type\":\"InternalIP\"},{\"address\":\"239.62.205.92.host.secureserver.net\",\"type\":\"Hostname\"}] {\"cpu\":\"4\",\"ephemeral-storage\":\"196555267123\",\"hugepages-1Gi\":\"0\",\"hugepages-2Mi\":\"0\",\"memory\":\"16377032Ki\",\"pods\":\"110\"}\n", + "stderr_present": false + }, + "deployment": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "deployment", + "flex-auth-secrets-engine", + "-o", + "jsonpath={.metadata.name}{\"\\n\"}{.metadata.uid}{\"\\n\"}{.metadata.labels.helm\\.sh/chart}{\"\\n\"}{.metadata.generation}{\" \"}{.status.observedGeneration}{\"\\n\"}{.spec.replicas}{\" \"}{.spec.strategy}{\"\\n\"}{.spec.template.spec.serviceAccountName}{\"\\n\"}{.spec.template.spec.containers[0].image}{\"\\n\"}{.spec.template.spec.containers[0].args}{\"\\n\"}{.spec.template.spec.containers[0].volumeMounts}{\"\\n\"}{.spec.template.spec.volumes}{\"\\n\"}{.spec.template.spec.securityContext}{\"\\n\"}{.spec.template.spec.containers[0].resources}{\"\\n\"}{.status.availableReplicas}{\"\\n\"}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine\nf83a5f5c-7dd7-4aac-bc02-d447e423e6ae\n\n4 4\n1 {\"rollingUpdate\":{\"maxSurge\":\"25%\",\"maxUnavailable\":\"25%\"},\"type\":\"RollingUpdate\"}\nflex-auth-secrets-engine\nforgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd\n[\"serve\",\"--addr\",\"0.0.0.0:8080\",\"--registry\",\"/opt/flex-auth/examples/secrets-engine/registry_snapshot.json\",\"--policy\",\"/opt/flex-auth/examples/secrets-engine/policy_package.md\",\"--caller-auth-mode\",\"enforce\",\"--caller-kubernetes-url\",\"https://10.43.0.1\",\"--caller-binding\",\"secrets-engine=system:serviceaccount:secrets-engine:secrets-engine\"]\n[{\"mountPath\":\"/var/run/secrets/flex-auth-reviewer\",\"name\":\"flex-auth-reviewer\",\"readOnly\":true}]\n[{\"name\":\"flex-auth-reviewer\",\"projected\":{\"defaultMode\":288,\"sources\":[{\"serviceAccountToken\":{\"expirationSeconds\":3600,\"path\":\"token\"}},{\"configMap\":{\"items\":[{\"key\":\"ca.crt\",\"path\":\"ca.crt\"}],\"name\":\"kube-root-ca.crt\"}}]}}]\n{\"fsGroup\":65532,\"runAsGroup\":65532,\"runAsNonRoot\":true,\"runAsUser\":65532,\"seccompProfile\":{\"type\":\"RuntimeDefault\"}}\n{\"limits\":{\"cpu\":\"300m\",\"memory\":\"192Mi\"},\"requests\":{\"cpu\":\"25m\",\"memory\":\"32Mi\"}}\n1\n", + "stderr_present": false + }, + "pods": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "pods", + "-l", + "app.kubernetes.io/name=flex-auth-secrets-engine", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.metadata.uid}{\" \"}{.status.phase}{\" \"}{.spec.nodeName}{\" \"}{.status.containerStatuses[0].imageID}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine-6467b96c6f-ncl5t f9598eac-6db2-41a5-8054-fb35bb5c0861 Running 239.62.205.92.host.secureserver.net forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd\n", + "stderr_present": false + }, + "service": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "service", + "flex-auth-secrets-engine", + "-o", + "jsonpath={.metadata.name}{\" \"}{.spec.clusterIP}{\" \"}{.spec.selector}{\" \"}{.spec.ports}{\"\\n\"}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine 10.43.40.221 {\"app.kubernetes.io/name\":\"flex-auth-secrets-engine\"} [{\"name\":\"http\",\"port\":8080,\"protocol\":\"TCP\",\"targetPort\":\"http\"}]\n", + "stderr_present": false + }, + "pvcs": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "pvc", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.spec.accessModes}{\" \"}{.spec.storageClassName}{\" \"}{.spec.volumeName}{\" \"}{.spec.resources.requests.storage}{\" \"}{.status.capacity}{\" \"}{.status.phase}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "", + "stderr_present": false + }, + "storage_classes": { + "command": [ + "kubectl", + "get", + "storageclass", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.provisioner}{\" \"}{.volumeBindingMode}{\" \"}{.reclaimPolicy}{\" \"}{.allowVolumeExpansion}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "local-path rancher.io/local-path WaitForFirstConsumer Delete \n", + "stderr_present": false + }, + "namespace_serviceaccounts": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "serviceaccount", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.automountServiceAccountToken}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "default \nflex-auth-application-readback false\nflex-auth-informed-decision-lifecycle false\nflex-auth-informed-decision-sitting false\nflex-auth-informed-decision-t03 false\nflex-auth-ops-warden false\nflex-auth-ops-warden-fetch false\nflex-auth-secrets-engine false\nflex-auth-tenant-engine false\nflex-auth-user-engine false\n", + "stderr_present": false + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/README.md b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/README.md new file mode 100644 index 00000000..fd619ab9 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/README.md @@ -0,0 +1,15 @@ +# Stopped outbox Helm state: source proposal R1 + +Access owner preparation for existing FLEX-WP-0024-T06. This isolated proposal changes both chart guards to accept numeric replicaCount exactly0 or1 when outbox is enabled. It rejects fractions, negative numbers,2+, booleans, strings, null and collections. Recreate, RWO, the process lock, bounded outbox and default one replica remain unchanged. No canonical owner files, frozen rollout R1 or Platform P2 files were changed. + +Allowing zero writers is consistent with the single-writer invariant and the required Platform custody fence. It lets the ordinary owner Helm path review/install the exact signer template with replicaCount0, then independently prove that all signer pods are gone before bootstrap. It removes the need for a separate derived stopped manifest or imperative scale/Deployment patch. Zero desired replicas alone proves neither current pod absence nor a held exclusive maintenance window; competing controllers, consumers and all native admission gates remain separately reviewed. + +Helm decodes YAML numeric0 and0.0 identically as float64/text0, likewise1 and1.0. The guard therefore checks mathematical integer value0/1, not a lexical spelling it cannot recover. YAML/JSON integral floats0.0/1.0 are accepted. CLI `--set replicaCount=0.0` yields a string and is rejected, as are explicit `--set-string` numeric strings. The prior coercing `int` check could admit some fractional/string forms as1; the new guard removes that truncation/coercion route. + +Validation: 86 actual Helm renders cover both charts via YAML, JSON and CLI, stopped/ready object equality after changing only replicas, unsafe value refusals and explicit integral numeric semantics. Existing `tests/test_outbox_chart.py` passes, including capacity, delivery and receiver-port boundaries. Patch applies cleanly to canonical84 source in read-only `git apply --check`. The unchanged baseline refused the first stopped render; the isolated patch passes it. No native API was contacted. + +`stopped-source-render.yaml` and `ready-source-render.yaml` were genuinely rendered by Helm against patched current chart source plus frozen rollout R1 values, with explicit replicaCount0/1 respectively. The Platform native projection algorithm gives both the same source-only digest `sha256:8ce375f065552c551538f3acc7300c427407057eeff1d0b43755bfdf8a46169c`, equal to frozen R1. The frozen84 image remains `sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`; this patch changes no binary, Containerfile, policy or registry. A separately reviewed source/chart successor pin is still required: the old84 chart hashes do not authenticate these changed templates. A source successor may reuse that independently verified immutable84 image by explicit reviewed chart/image selection; it cannot claim the image revision label changed to the new chart revision. No rebuilt image is needed to express a chart-only stopped state. + +If adopted, freeze a new rollout revision and update package/scope exact source hashes while preserving original R1/P2. Equal runtime projection does not authenticate new chart source or automatically renew approvals. Actual stopped native projection equality, image/runtime verification, ordinary Helm history, controllers, PVC/Retain, custody and all independent dispositions remain open. + +Isolated source and tests: `/tmp/access-stopped-chart-proposal-1gom08nf`. Apply `implementation.patch` only through the existing owner source integration path after review. No rollout or privileged action is authorized. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/implementation.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/implementation.patch new file mode 100644 index 00000000..f54f7982 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/implementation.patch @@ -0,0 +1,98 @@ +--- a/charts/flex-auth/templates/deployment.yaml ++++ b/charts/flex-auth/templates/deployment.yaml +@@ -28,8 +28,10 @@ + {{- include "flex-auth.labels" . | nindent 4 }} + spec: + {{- if .Values.outbox.enabled }} +- {{- if ne (int .Values.replicaCount) 1 }} +- {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} ++ {{- $replicas := .Values.replicaCount }} ++ {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} ++ {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} ++ {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate +--- a/charts/access-engine/templates/deployment.yaml ++++ b/charts/access-engine/templates/deployment.yaml +@@ -29,8 +29,10 @@ + {{- include "access-engine.labels" . | nindent 4 }} + spec: + {{- if .Values.outbox.enabled }} +- {{- if ne (int .Values.replicaCount) 1 }} +- {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} ++ {{- $replicas := .Values.replicaCount }} ++ {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} ++ {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} ++ {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate +--- /dev/null ++++ b/tests/test_outbox_replicas_chart.py +@@ -0,0 +1,65 @@ ++#!/usr/bin/env python3 ++"""Actual Helm stop/start renders and unsafe replica value rejection; offline only.""" ++import copy ++import json ++from pathlib import Path ++import subprocess ++import tempfile ++import yaml ++ ++ROOT = Path(__file__).resolve().parents[1] ++ ++def render(chart, value, encoding="yaml", success=True): ++ with tempfile.TemporaryDirectory() as directory: ++ command = ["helm", "template", "flex-auth-secrets-engine", str(ROOT / "charts" / chart), ++ "--namespace", "flex-auth", "-f", str(ROOT / "values/secrets-engine.yaml"), ++ "--set", "outbox.enabled=true", "--set", "outbox.source=flex-auth.secrets-engine", ++ "--set", "envelopeSigning.enabled=true", "--set", "envelopeSigning.kid=test-kid", ++ "--set", "envelopeSigning.secretName=proposed-seed", "--set", "envelopeSigning.secretKey=seed"] ++ if encoding == "yaml": ++ overlay = Path(directory) / "values.yaml" ++ overlay.write_text(yaml.safe_dump({"replicaCount": value})) ++ command += ["-f", str(overlay)] ++ elif encoding == "json": ++ command += ["--set-json", "replicaCount=" + json.dumps(value)] ++ elif encoding == "string": ++ command += ["--set-string", "replicaCount=" + str(value)] ++ else: ++ command += ["--set", "replicaCount=" + str(value)] ++ result = subprocess.run(command, capture_output=True, text=True) ++ if not success: ++ assert result.returncode != 0, (chart, encoding, value) ++ assert "replicaCount" in result.stderr, result.stderr ++ return ++ assert result.returncode == 0, result.stderr ++ documents = [d for d in yaml.safe_load_all(result.stdout) if d] ++ deployment = next(d for d in documents if d["kind"] == "Deployment") ++ assert deployment["spec"]["strategy"] == {"type": "Recreate"} ++ assert type(deployment["spec"]["replicas"]) is int ++ assert deployment["spec"]["replicas"] == value ++ return documents ++ ++count = 0 ++for chart in ("flex-auth", "access-engine"): ++ for encoding in ("yaml", "json", "cli"): ++ stopped = render(chart, 0, encoding) ++ ready = render(chart, 1, encoding) ++ normalized = copy.deepcopy(ready) ++ next(d for d in normalized if d["kind"] == "Deployment")["spec"]["replicas"] = 0 ++ assert stopped == normalized, "stop must change replicas only, including identical pod projection/PVC" ++ count += 2 ++ for encoding in ("yaml", "json"): ++ for value in (-1, 2, 1.5, 0.5, True, False, None, "0", "1", [], {}): ++ render(chart, value, encoding, success=False) ++ count += 1 ++ # Helm loses lexical 0 vs0.0 and1 vs1.0 before template evaluation. ++ for value in (0.0, 1.0): ++ render(chart, value, encoding) ++ count += 1 ++ for value in ("-1", "2", "0.5", "1.5", "true", "false", "null", "0.0", "1.0"): ++ render(chart, value, "cli", success=False) ++ count += 1 ++ for value in ("0", "1"): ++ render(chart, value, "string", success=False) ++ count += 1 ++print(f"stopped/single-writer chart checks passed: {count} actual Helm renders") diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/manifest.json new file mode 100644 index 00000000..6c19773f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/manifest.json @@ -0,0 +1,15 @@ +{ + "schema": "access-engine.stopped-chart-source-proposal.v1", + "proposal_id": "FLEX24-STOPPED-CHART-20261016-R1", + "base_revision": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0", + "disposition": "approve-source-proposal-for-independent-review-only", + "execution_authorized": false, + "files": { + "README.md": "sha256:00bb92f649f01c75731418d9e3f2113a71a7ef5c0e6c112db18c34dd9aa6255c", + "implementation.patch": "sha256:4baf8f35028020a5718ffc86d57827cf8d48fb736211ae725964ac62b28086ff", + "projection-evidence.json": "sha256:49d210a08432699cab9c9f718fdb250c6f0f3de582acdd2300946944c7cfef23", + "ready-source-render.yaml": "sha256:b7edecb8ec50a6d2db7623a3d962dbece46de81d3809d5f73b3aca20ff76b8c7", + "stopped-source-render.yaml": "sha256:03b631f0fe17c63494c14619199f69f2a2a9ab245d00f1ec441248868b24c071", + "validation.json": "sha256:7540335f36593d9f8eb432901e1d1a04684a80544bb8bb240b561ed2a5de608c" + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/projection-evidence.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/projection-evidence.json new file mode 100644 index 00000000..9a4339b7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/projection-evidence.json @@ -0,0 +1,8 @@ +{ + "source_revision": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0 plus isolated implementation.patch", + "source_rendered_projection_sha256": "sha256:8ce375f065552c551538f3acc7300c427407057eeff1d0b43755bfdf8a46169c", + "stopped_equals_ready": true, + "unchanged_from_frozen_R1": true, + "native_stopped_fence_verified": false, + "execution_authorized": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/ready-source-render.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/ready-source-render.yaml new file mode 100644 index 00000000..5c6b53e0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/ready-source-render.yaml @@ -0,0 +1,237 @@ +--- +# Source: flex-auth/templates/networkpolicy.yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + policyTypes: + - Ingress + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: secrets-engine + podSelector: + matchLabels: + app.kubernetes.io/name: secrets-engine + ports: + - port: 8080 + protocol: TCP +--- +# Source: flex-auth/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +automountServiceAccountToken: false +--- +# Source: flex-auth/templates/outbox-pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: flex-auth-secrets-engine-outbox + annotations: + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + accessModes: + - ReadWriteOnce + storageClassName: "local-path" + resources: + requests: + storage: "1Gi" +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flex-auth-secrets-engine-tokenreviewer +subjects: + - kind: ServiceAccount + name: flex-auth-secrets-engine + namespace: flex-auth +--- +# Source: flex-auth/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + selector: + app.kubernetes.io/name: flex-auth-secrets-engine + ports: + - name: http + port: 8080 + targetPort: http +--- +# Source: flex-auth/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + template: + metadata: + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: + automountServiceAccountToken: false + serviceAccountName: flex-auth-secrets-engine + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f + imagePullPolicy: IfNotPresent + args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md + - --envelope-signing-key-file + - "/var/run/secrets/decision-envelope-signing/seed" + - --envelope-signing-kid + - "flex-auth-lifecycle-20261016-r1" + - --outbox-max-bytes + - "67108864" + - --outbox-dir + - "/var/lib/flex-auth/outbox" + - --emission-source + - "flex-auth.secrets-engine" + - --caller-audience + - "flex-auth" + - --caller-auth-mode + - "enforce" + - --caller-kubernetes-url + - "https://10.43.0.1" + - --caller-binding + - "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine" + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: "/var/run/secrets/decision-envelope-signing" + name: flex-auth-envelope-signing + readOnly: true + - mountPath: "/var/lib/flex-auth/outbox" + name: flex-auth-outbox + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: + - name: flex-auth-envelope-signing + secret: + secretName: "flex-auth-secrets-engine-envelope-signing" + defaultMode: 0440 + items: + - key: "seed" + path: seed + - name: flex-auth-outbox + persistentVolumeClaim: + claimName: "flex-auth-secrets-engine-outbox" + - name: flex-auth-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/stopped-source-render.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/stopped-source-render.yaml new file mode 100644 index 00000000..3357fd79 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/stopped-source-render.yaml @@ -0,0 +1,237 @@ +--- +# Source: flex-auth/templates/networkpolicy.yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + policyTypes: + - Ingress + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: secrets-engine + podSelector: + matchLabels: + app.kubernetes.io/name: secrets-engine + ports: + - port: 8080 + protocol: TCP +--- +# Source: flex-auth/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +automountServiceAccountToken: false +--- +# Source: flex-auth/templates/outbox-pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: flex-auth-secrets-engine-outbox + annotations: + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + accessModes: + - ReadWriteOnce + storageClassName: "local-path" + resources: + requests: + storage: "1Gi" +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +--- +# Source: flex-auth/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flex-auth-secrets-engine-tokenreviewer + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flex-auth-secrets-engine-tokenreviewer +subjects: + - kind: ServiceAccount + name: flex-auth-secrets-engine + namespace: flex-auth +--- +# Source: flex-auth/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + selector: + app.kubernetes.io/name: flex-auth-secrets-engine + ports: + - name: http + port: 8080 + targetPort: http +--- +# Source: flex-auth/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flex-auth-secrets-engine + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth +spec: + strategy: + type: Recreate + replicas: 0 + selector: + matchLabels: + app.kubernetes.io/name: flex-auth-secrets-engine + template: + metadata: + labels: + app.kubernetes.io/name: flex-auth-secrets-engine + app.kubernetes.io/instance: flex-auth-secrets-engine + app.kubernetes.io/part-of: flex-auth + spec: + automountServiceAccountToken: false + serviceAccountName: flex-auth-secrets-engine + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: forgejo.coulomb.social/coulomb/flex-auth@sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f + imagePullPolicy: IfNotPresent + args: + - serve + - --addr + - 0.0.0.0:8080 + - --registry + - /opt/flex-auth/examples/secrets-engine/registry_snapshot.json + - --policy + - /opt/flex-auth/examples/secrets-engine/policy_package.md + - --envelope-signing-key-file + - "/var/run/secrets/decision-envelope-signing/seed" + - --envelope-signing-kid + - "flex-auth-lifecycle-20261016-r1" + - --outbox-max-bytes + - "67108864" + - --outbox-dir + - "/var/lib/flex-auth/outbox" + - --emission-source + - "flex-auth.secrets-engine" + - --caller-audience + - "flex-auth" + - --caller-auth-mode + - "enforce" + - --caller-kubernetes-url + - "https://10.43.0.1" + - --caller-binding + - "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine" + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 5 + resources: + limits: + cpu: 300m + memory: 192Mi + requests: + cpu: 25m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: "/var/run/secrets/decision-envelope-signing" + name: flex-auth-envelope-signing + readOnly: true + - mountPath: "/var/lib/flex-auth/outbox" + name: flex-auth-outbox + - mountPath: /var/run/secrets/flex-auth-reviewer + name: flex-auth-reviewer + readOnly: true + volumes: + - name: flex-auth-envelope-signing + secret: + secretName: "flex-auth-secrets-engine-envelope-signing" + defaultMode: 0440 + items: + - key: "seed" + path: seed + - name: flex-auth-outbox + persistentVolumeClaim: + claimName: "flex-auth-secrets-engine-outbox" + - name: flex-auth-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + expirationSeconds: 3600 + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/validation.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/validation.json new file mode 100644 index 00000000..5bf13743 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r1/validation.json @@ -0,0 +1,10 @@ +{ + "source_checks": "passed", + "actual_new_helm_renders": 86, + "existing_outbox_chart_suite": "passed: optional outbox, audit delivery and receiver-port checks", + "git_apply_check": "passed against canonical84", + "runtime_changes": false, + "canonical_owner_files_changed": false, + "frozen_rollout_R1_P2_changed": false, + "incremental_source_spend_eur": 0 +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/README.md b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/README.md new file mode 100644 index 00000000..d22bc51c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/README.md @@ -0,0 +1,7 @@ +# Stopped-chart source proposal R2 + +R2 preserves frozen R1 and adds only the bounded source note to existing FLEX-WP-0024-T06. No status, admission or authority field changes. `implementation-r2.patch` is the complete four-file proposal against canonical84; `r1-to-r2.patch` adds only the workplan note after R1. `postimages/` contains exact final four-file bytes, with SHA256s in manifest.json. + +Both deployment guards and the 86-render test remain byte-identical to independently checked R1. Existing outbox/delivery/receiver-port suites and root independent reconstruction passed. The isolated source change is approved for source integration only; native approval and actual stopped-fence proof remain absent. Current immutable source84 image09a83b remains selected explicitly alongside a future new chart/source commit. This chart/test/workplan-only delta does not trigger the existing image workflow or require rebuilding the binary image; new source pinning cannot relabel the unchanged image. + +See ../stopped-chart-r1/README.md for guard semantics, Helm integral numeric decoding and source-only projection correspondence. Neither R1, rollout R1, Platform P2, canonical owner files nor runtime was changed in this preparation. Coordinator must run ordinary owner consistency sync after integration of the existing workplan file. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/implementation-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/implementation-r2.patch new file mode 100644 index 00000000..f81a7183 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/implementation-r2.patch @@ -0,0 +1,127 @@ +--- a/charts/flex-auth/templates/deployment.yaml ++++ b/charts/flex-auth/templates/deployment.yaml +@@ -28,8 +28,10 @@ + {{- include "flex-auth.labels" . | nindent 4 }} + spec: + {{- if .Values.outbox.enabled }} +- {{- if ne (int .Values.replicaCount) 1 }} +- {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} ++ {{- $replicas := .Values.replicaCount }} ++ {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} ++ {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} ++ {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate +--- a/charts/access-engine/templates/deployment.yaml ++++ b/charts/access-engine/templates/deployment.yaml +@@ -29,8 +29,10 @@ + {{- include "access-engine.labels" . | nindent 4 }} + spec: + {{- if .Values.outbox.enabled }} +- {{- if ne (int .Values.replicaCount) 1 }} +- {{- fail "outbox requires replicaCount=1: one writer per source and PVC" }} ++ {{- $replicas := .Values.replicaCount }} ++ {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} ++ {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} ++ {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate +--- /dev/null ++++ b/tests/test_outbox_replicas_chart.py +@@ -0,0 +1,65 @@ ++#!/usr/bin/env python3 ++"""Actual Helm stop/start renders and unsafe replica value rejection; offline only.""" ++import copy ++import json ++from pathlib import Path ++import subprocess ++import tempfile ++import yaml ++ ++ROOT = Path(__file__).resolve().parents[1] ++ ++def render(chart, value, encoding="yaml", success=True): ++ with tempfile.TemporaryDirectory() as directory: ++ command = ["helm", "template", "flex-auth-secrets-engine", str(ROOT / "charts" / chart), ++ "--namespace", "flex-auth", "-f", str(ROOT / "values/secrets-engine.yaml"), ++ "--set", "outbox.enabled=true", "--set", "outbox.source=flex-auth.secrets-engine", ++ "--set", "envelopeSigning.enabled=true", "--set", "envelopeSigning.kid=test-kid", ++ "--set", "envelopeSigning.secretName=proposed-seed", "--set", "envelopeSigning.secretKey=seed"] ++ if encoding == "yaml": ++ overlay = Path(directory) / "values.yaml" ++ overlay.write_text(yaml.safe_dump({"replicaCount": value})) ++ command += ["-f", str(overlay)] ++ elif encoding == "json": ++ command += ["--set-json", "replicaCount=" + json.dumps(value)] ++ elif encoding == "string": ++ command += ["--set-string", "replicaCount=" + str(value)] ++ else: ++ command += ["--set", "replicaCount=" + str(value)] ++ result = subprocess.run(command, capture_output=True, text=True) ++ if not success: ++ assert result.returncode != 0, (chart, encoding, value) ++ assert "replicaCount" in result.stderr, result.stderr ++ return ++ assert result.returncode == 0, result.stderr ++ documents = [d for d in yaml.safe_load_all(result.stdout) if d] ++ deployment = next(d for d in documents if d["kind"] == "Deployment") ++ assert deployment["spec"]["strategy"] == {"type": "Recreate"} ++ assert type(deployment["spec"]["replicas"]) is int ++ assert deployment["spec"]["replicas"] == value ++ return documents ++ ++count = 0 ++for chart in ("flex-auth", "access-engine"): ++ for encoding in ("yaml", "json", "cli"): ++ stopped = render(chart, 0, encoding) ++ ready = render(chart, 1, encoding) ++ normalized = copy.deepcopy(ready) ++ next(d for d in normalized if d["kind"] == "Deployment")["spec"]["replicas"] = 0 ++ assert stopped == normalized, "stop must change replicas only, including identical pod projection/PVC" ++ count += 2 ++ for encoding in ("yaml", "json"): ++ for value in (-1, 2, 1.5, 0.5, True, False, None, "0", "1", [], {}): ++ render(chart, value, encoding, success=False) ++ count += 1 ++ # Helm loses lexical 0 vs0.0 and1 vs1.0 before template evaluation. ++ for value in (0.0, 1.0): ++ render(chart, value, encoding) ++ count += 1 ++ for value in ("-1", "2", "0.5", "1.5", "true", "false", "null", "0.0", "1.0"): ++ render(chart, value, "cli", success=False) ++ count += 1 ++ for value in ("0", "1"): ++ render(chart, value, "string", success=False) ++ count += 1 ++print(f"stopped/single-writer chart checks passed: {count} actual Helm renders") +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -302,3 +302,26 @@ + reef binding, custody, native rollout or consumer trust admission. Future signer + image/config adoption remains separate. T06 remains `progress`; no second work + item duplicates this bounded dependency. ++ ++ ++2026-10-16 bounded stopped-chart source preparation (Custodian ++`access-native-admission/stopped-chart-r2`): both outbox charts admit numeric ++replicaCount exactly 0 (maintenance stop) or 1 (single writer), with Recreate, ++process flock and bounded persistence unchanged. Fractions, strings, booleans, ++negative values and 2+ refuse; Helm decodes YAML/JSON integral 0.0/1.0 as the same ++numeric values as 0/1, so lexical float spelling is not distinguished. Actual ++Helm stopped/ready renders differ only in replicas and retain the same Platform ++source-rendered deployment projection. Owner preparation and independent ++coordinator checks approve this bounded source change only; 86 Helm renders and ++the existing outbox/delivery/receiver-port suite pass. No native stop, bootstrap, ++rollout, custody, public trust, resource or protected-effect approval follows. ++The proposed immutable image remains source revision ++`84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, ordinary CI402 verified digest ++`sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. ++This chart-only successor requires its own exact source/chart pin and reviewed ++runtime scope; it can explicitly select the unchanged verified 84 image without ++rebuilding it or claiming that its revision label names the new chart commit. ++These chart/test/workplan-only paths do not trigger the existing image workflow. ++Frozen rollout R1, Platform P2 and the package extraction2b/current05a remain ++unchanged. T06 stays `progress`; actual stopped-fence verification and all native ++admission gates remain open under the same existing task. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/manifest.json b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/manifest.json new file mode 100644 index 00000000..857eaf16 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/manifest.json @@ -0,0 +1,40 @@ +{ + "schema": "access-engine.stopped-chart-source-proposal.v2", + "proposal_id": "FLEX24-STOPPED-CHART-20261016-R1", + "revision": "R2", + "base_revision": "84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0", + "prior_manifest_sha256": "sha256:53dfbfa1b3d4fd36f0fbe975083a46793b19d8b45d134cbe7fa3ebcc923daceb", + "isolated_source": "/tmp/access-stopped-chart-r2-asip4vy_", + "changed_paths": [ + "charts/flex-auth/templates/deployment.yaml", + "charts/access-engine/templates/deployment.yaml", + "tests/test_outbox_replicas_chart.py", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md" + ], + "preimages_sha256": { + "charts/flex-auth/templates/deployment.yaml": "sha256:b347614283e65d9e8006a93dfbdfec15360632f6c318761e245213460e158666", + "charts/access-engine/templates/deployment.yaml": "sha256:155310e75e3883fc25d9c9dcf79650a8216cf6fd09902421b856cdfeab5e4cc3", + "tests/test_outbox_replicas_chart.py": null, + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:def6f73745d02ea88aa334db3239d2ef22984ebefa8853a5573496a08ed1592c" + }, + "postimages_sha256": { + "charts/flex-auth/templates/deployment.yaml": "sha256:26e29618b9110bdd9d8aa73aae432a95c757a4f46e567ce4fe429c60bd519026", + "charts/access-engine/templates/deployment.yaml": "sha256:04f1fa3c0378f1f28b11438549e0542a85c0604fb19d7ea814eeddf8d99de29b", + "tests/test_outbox_replicas_chart.py": "sha256:dde2b054fa0355cf88034c3482aae45b4116d8ebdb9b0b9b28075960ebc244f0", + "workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:3a3fc05528eb869204811d90d12ef5a3ac3b6d537b5b7a8cccbc15b6483a002f" + }, + "files": { + "README.md": "sha256:4be2961c2a3ee90895c4f7fc816d78991cabb1f6030cb342dbae11e93c461aca", + "implementation-r2.patch": "sha256:14f1a8de20105760340d212b579d669a0e770d20accc433876f024601f206be1", + "postimages/charts/access-engine/templates/deployment.yaml": "sha256:04f1fa3c0378f1f28b11438549e0542a85c0604fb19d7ea814eeddf8d99de29b", + "postimages/charts/flex-auth/templates/deployment.yaml": "sha256:26e29618b9110bdd9d8aa73aae432a95c757a4f46e567ce4fe429c60bd519026", + "postimages/tests/test_outbox_replicas_chart.py": "sha256:dde2b054fa0355cf88034c3482aae45b4116d8ebdb9b0b9b28075960ebc244f0", + "postimages/workplans/FLEX-WP-0024-decision-envelope-authenticity.md": "sha256:3a3fc05528eb869204811d90d12ef5a3ac3b6d537b5b7a8cccbc15b6483a002f", + "r1-to-r2.patch": "sha256:28b22ff1480c9060c4758a29231803b32bf7cff2489fb644b9c2fd0f76c0edd2" + }, + "source_checks": "86 Helm renders and existing outbox/delivery/receiver-port suite passed R1; R2 executable bytes unchanged; root independently reconstructed and passed", + "task_status_changed": false, + "canonical_owner_files_changed": false, + "native_admitted": false, + "execution_authorized": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/access-engine/templates/deployment.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/access-engine/templates/deployment.yaml new file mode 100644 index 00000000..a899ab34 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/access-engine/templates/deployment.yaml @@ -0,0 +1,207 @@ +{{- if .Values.auditDelivery.enabled }} +{{- if ne (first .Values.args) "serve" }} +{{- fail "auditDelivery currently supports serve decision pins only" }} +{{- end }} +{{- if not .Values.outbox.enabled }} +{{- fail "auditDelivery requires enabled durable outbox" }} +{{- end }} +{{- if not (regexMatch "^https?://[^/?#@]+/v1/events$" .Values.auditDelivery.url) }} +{{- fail "auditDelivery.url must be an explicit http(s) /v1/events endpoint" }} +{{- end }} +{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }} +{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }} +{{- end }} +{{- end }} +{{- if .Values.envelopeSigning.enabled }} +{{- if ne (first .Values.args) "serve" }} +{{- fail "envelopeSigning currently supports serve decision pins only" }} +{{- end }} +{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }} +{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }} +{{- end }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "access-engine.name" . }} + labels: + {{- include "access-engine.labels" . | nindent 4 }} +spec: + {{- if .Values.outbox.enabled }} + {{- $replicas := .Values.replicaCount }} + {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} + {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate + {{- end }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "access-engine.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "access-engine.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + serviceAccountName: {{ include "access-engine.name" . }} + {{- end }} + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: access-engine + image: {{ include "access-engine.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + {{- toYaml .Values.args | nindent 12 }} + {{- if .Values.envelopeSigning.enabled }} + - --envelope-signing-key-file + - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }} + - --envelope-signing-kid + - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }} + {{- end }} + {{- if .Values.outbox.enabled }} + {{- $limit := .Values.outbox.maxBytes }} + {{- $numericKind := has (kindOf $limit) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- $asFloat := float64 $limit }} + {{- $validNumber := and $numericKind (eq $asFloat (floor $asFloat)) }} + {{- $validString := and (kindIs "string" $limit) (regexMatch "^[1-9][0-9]*$" (toString $limit)) }} + {{- if or (not (or $validNumber $validString)) (lt $asFloat 1.0) (gt $asFloat 67108864.0) }} + {{- fail "outbox.maxBytes must be an exact integer between1 and67108864" }} + {{- end }} + - --outbox-max-bytes + - {{ printf "%.0f" $asFloat | quote }} + - --outbox-dir + - {{ .Values.outbox.mountPath | quote }} + - --emission-source + - {{ required "outbox.source is required when outbox.enabled=true" .Values.outbox.source | quote }} + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - --audit-url + - {{ .Values.auditDelivery.url | quote }} + - --audit-token-file + - {{ printf "%s/token" .Values.auditDelivery.mountPath | quote }} + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + {{- $binding := "" }} + {{- if not (and .Values.readback .Values.readback.enabled) }} + {{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }} + {{- end }} + - --caller-audience + - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }} + - --caller-reviewer-token-file + - {{ printf "%s/token" .Values.callerAuth.reviewer.mountPath | quote }} + - --caller-ca-file + - {{ printf "%s/ca.crt" .Values.callerAuth.reviewer.mountPath | quote }} + - --caller-auth-mode + - {{ include "access-engine.callerAuth.mode" . | quote }} + - --caller-kubernetes-url + - {{ required "callerAuth.kubernetesURL is required when callerAuth.mode is warn or enforce" .Values.callerAuth.kubernetesURL | quote }} + {{- if and .Values.readback .Values.readback.enabled }} + {{- /* serve-readback admits one readback caller and no /v1/check binding (FLEX-DEC-2026-019). */}} + - --readback-caller + - {{ required "readback.caller is required when readback.enabled=true" .Values.readback.caller | quote }} + {{- else }} + - --caller-binding + - {{ $binding | quote }} + {{- range .Values.callerAuth.extraBindings }} + - --caller-binding + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + livenessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }} + volumeMounts: + {{- if .Values.envelopeSigning.enabled }} + - mountPath: {{ .Values.envelopeSigning.mountPath | quote }} + name: access-engine-envelope-signing + readOnly: true + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - mountPath: {{ .Values.auditDelivery.mountPath | quote }} + name: access-engine-audit + readOnly: true + {{- end }} + {{- if .Values.outbox.enabled }} + - mountPath: {{ .Values.outbox.mountPath | quote }} + name: access-engine-outbox + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + - mountPath: {{ .Values.callerAuth.reviewer.mountPath }} + name: access-engine-reviewer + readOnly: true + {{- end }} + {{- end }} + {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "access-engine.callerAuth.enabled" .) "true") }} + volumes: + {{- if .Values.envelopeSigning.enabled }} + - name: access-engine-envelope-signing + secret: + secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }} + defaultMode: 0440 + items: + - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }} + path: seed + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - name: access-engine-audit + secret: + secretName: {{ required "auditDelivery.secretName must name an admitted existing Secret" .Values.auditDelivery.secretName | quote }} + defaultMode: 0440 + items: + - key: {{ required "auditDelivery.secretKey is required" .Values.auditDelivery.secretKey | quote }} + path: token + {{- end }} + {{- if .Values.outbox.enabled }} + - name: access-engine-outbox + persistentVolumeClaim: + claimName: {{ default (printf "%s-outbox" (include "access-engine.name" .)) .Values.outbox.existingClaim | quote }} + {{- end }} + {{- if eq (include "access-engine.callerAuth.enabled" .) "true" }} + - name: access-engine-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + {{- if .Values.callerAuth.reviewer.audience }} + audience: {{ .Values.callerAuth.reviewer.audience | quote }} + {{- end }} + expirationSeconds: {{ .Values.callerAuth.reviewer.expirationSeconds }} + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + {{- end }} + {{- end }} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/flex-auth/templates/deployment.yaml b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/flex-auth/templates/deployment.yaml new file mode 100644 index 00000000..3afef535 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/charts/flex-auth/templates/deployment.yaml @@ -0,0 +1,202 @@ +{{- if .Values.auditDelivery.enabled }} +{{- if ne (first .Values.args) "serve" }} +{{- fail "auditDelivery currently supports serve decision pins only" }} +{{- end }} +{{- if not .Values.outbox.enabled }} +{{- fail "auditDelivery requires enabled durable outbox" }} +{{- end }} +{{- if not (regexMatch "^https?://[^/?#@]+/v1/events$" .Values.auditDelivery.url) }} +{{- fail "auditDelivery.url must be an explicit http(s) /v1/events endpoint" }} +{{- end }} +{{- if or (not (hasPrefix "/" .Values.auditDelivery.mountPath)) (contains ".." .Values.auditDelivery.mountPath) }} +{{- fail "auditDelivery.mountPath must be an absolute normalized directory" }} +{{- end }} +{{- end }} +{{- if .Values.envelopeSigning.enabled }} +{{- if ne (first .Values.args) "serve" }} +{{- fail "envelopeSigning currently supports serve decision pins only" }} +{{- end }} +{{- if or (not (hasPrefix "/" .Values.envelopeSigning.mountPath)) (contains ".." .Values.envelopeSigning.mountPath) }} +{{- fail "envelopeSigning.mountPath must be an absolute normalized directory" }} +{{- end }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "flex-auth.name" . }} + labels: + {{- include "flex-auth.labels" . | nindent 4 }} +spec: + {{- if .Values.outbox.enabled }} + {{- $replicas := .Values.replicaCount }} + {{- $numericKind := has (kindOf $replicas) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- if or (not $numericKind) (not (or (eq (toString $replicas) "0") (eq (toString $replicas) "1"))) }} + {{- fail "outbox requires numeric integer replicaCount 0 (stopped) or 1 (single writer)" }} + {{- end }} + strategy: + type: Recreate + {{- end }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "flex-auth.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "flex-auth.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }} + serviceAccountName: {{ include "flex-auth.name" . }} + {{- end }} + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: flex-auth + image: {{ include "flex-auth.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + {{- toYaml .Values.args | nindent 12 }} + {{- if .Values.envelopeSigning.enabled }} + - --envelope-signing-key-file + - {{ printf "%s/seed" .Values.envelopeSigning.mountPath | quote }} + - --envelope-signing-kid + - {{ required "envelopeSigning.kid must be independently admitted" .Values.envelopeSigning.kid | quote }} + {{- end }} + {{- if .Values.outbox.enabled }} + {{- $limit := .Values.outbox.maxBytes }} + {{- $numericKind := has (kindOf $limit) (list "int" "int64" "int32" "uint" "uint64" "float64" "float32") }} + {{- $asFloat := float64 $limit }} + {{- $validNumber := and $numericKind (eq $asFloat (floor $asFloat)) }} + {{- $validString := and (kindIs "string" $limit) (regexMatch "^[1-9][0-9]*$" (toString $limit)) }} + {{- if or (not (or $validNumber $validString)) (lt $asFloat 1.0) (gt $asFloat 67108864.0) }} + {{- fail "outbox.maxBytes must be an exact integer between1 and67108864" }} + {{- end }} + - --outbox-max-bytes + - {{ printf "%.0f" $asFloat | quote }} + - --outbox-dir + - {{ .Values.outbox.mountPath | quote }} + - --emission-source + - {{ required "outbox.source is required when outbox.enabled=true" .Values.outbox.source | quote }} + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - --audit-url + - {{ .Values.auditDelivery.url | quote }} + - --audit-token-file + - {{ printf "%s/token" .Values.auditDelivery.mountPath | quote }} + {{- end }} + {{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }} + {{- $binding := "" }} + {{- if not (and .Values.readback .Values.readback.enabled) }} + {{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }} + {{- end }} + - --caller-audience + - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }} + - --caller-auth-mode + - {{ include "flex-auth.callerAuth.mode" . | quote }} + - --caller-kubernetes-url + - {{ required "callerAuth.kubernetesURL is required when callerAuth.mode is warn or enforce" .Values.callerAuth.kubernetesURL | quote }} + {{- if and .Values.readback .Values.readback.enabled }} + {{- /* serve-readback admits one readback caller and no /v1/check binding (FLEX-DEC-2026-019). */}} + - --readback-caller + - {{ required "readback.caller is required when readback.enabled=true" .Values.readback.caller | quote }} + {{- else }} + - --caller-binding + - {{ $binding | quote }} + {{- range .Values.callerAuth.extraBindings }} + - --caller-binding + - {{ . | quote }} + {{- end }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + livenessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 20 + readinessProbe: + httpGet: + path: {{ .Values.health.path }} + port: http + periodSeconds: 5 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }} + volumeMounts: + {{- if .Values.envelopeSigning.enabled }} + - mountPath: {{ .Values.envelopeSigning.mountPath | quote }} + name: flex-auth-envelope-signing + readOnly: true + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - mountPath: {{ .Values.auditDelivery.mountPath | quote }} + name: flex-auth-audit + readOnly: true + {{- end }} + {{- if .Values.outbox.enabled }} + - mountPath: {{ .Values.outbox.mountPath | quote }} + name: flex-auth-outbox + {{- end }} + {{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }} + - mountPath: {{ .Values.callerAuth.reviewer.mountPath }} + name: flex-auth-reviewer + readOnly: true + {{- end }} + {{- end }} + {{- if or .Values.envelopeSigning.enabled .Values.outbox.enabled .Values.auditDelivery.enabled (eq (include "flex-auth.callerAuth.enabled" .) "true") }} + volumes: + {{- if .Values.envelopeSigning.enabled }} + - name: flex-auth-envelope-signing + secret: + secretName: {{ required "envelopeSigning.secretName must name an admitted existing Secret" .Values.envelopeSigning.secretName | quote }} + defaultMode: 0440 + items: + - key: {{ required "envelopeSigning.secretKey is required" .Values.envelopeSigning.secretKey | quote }} + path: seed + {{- end }} + {{- if .Values.auditDelivery.enabled }} + - name: flex-auth-audit + secret: + secretName: {{ required "auditDelivery.secretName must name an admitted existing Secret" .Values.auditDelivery.secretName | quote }} + defaultMode: 0440 + items: + - key: {{ required "auditDelivery.secretKey is required" .Values.auditDelivery.secretKey | quote }} + path: token + {{- end }} + {{- if .Values.outbox.enabled }} + - name: flex-auth-outbox + persistentVolumeClaim: + claimName: {{ default (printf "%s-outbox" (include "flex-auth.name" .)) .Values.outbox.existingClaim | quote }} + {{- end }} + {{- if eq (include "flex-auth.callerAuth.enabled" .) "true" }} + - name: flex-auth-reviewer + projected: + defaultMode: 0440 + sources: + - serviceAccountToken: + {{- if .Values.callerAuth.reviewer.audience }} + audience: {{ .Values.callerAuth.reviewer.audience | quote }} + {{- end }} + expirationSeconds: {{ .Values.callerAuth.reviewer.expirationSeconds }} + path: token + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + {{- end }} + {{- end }} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/tests/test_outbox_replicas_chart.py b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/tests/test_outbox_replicas_chart.py new file mode 100644 index 00000000..aa4a1557 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/tests/test_outbox_replicas_chart.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Actual Helm stop/start renders and unsafe replica value rejection; offline only.""" +import copy +import json +from pathlib import Path +import subprocess +import tempfile +import yaml + +ROOT = Path(__file__).resolve().parents[1] + +def render(chart, value, encoding="yaml", success=True): + with tempfile.TemporaryDirectory() as directory: + command = ["helm", "template", "flex-auth-secrets-engine", str(ROOT / "charts" / chart), + "--namespace", "flex-auth", "-f", str(ROOT / "values/secrets-engine.yaml"), + "--set", "outbox.enabled=true", "--set", "outbox.source=flex-auth.secrets-engine", + "--set", "envelopeSigning.enabled=true", "--set", "envelopeSigning.kid=test-kid", + "--set", "envelopeSigning.secretName=proposed-seed", "--set", "envelopeSigning.secretKey=seed"] + if encoding == "yaml": + overlay = Path(directory) / "values.yaml" + overlay.write_text(yaml.safe_dump({"replicaCount": value})) + command += ["-f", str(overlay)] + elif encoding == "json": + command += ["--set-json", "replicaCount=" + json.dumps(value)] + elif encoding == "string": + command += ["--set-string", "replicaCount=" + str(value)] + else: + command += ["--set", "replicaCount=" + str(value)] + result = subprocess.run(command, capture_output=True, text=True) + if not success: + assert result.returncode != 0, (chart, encoding, value) + assert "replicaCount" in result.stderr, result.stderr + return + assert result.returncode == 0, result.stderr + documents = [d for d in yaml.safe_load_all(result.stdout) if d] + deployment = next(d for d in documents if d["kind"] == "Deployment") + assert deployment["spec"]["strategy"] == {"type": "Recreate"} + assert type(deployment["spec"]["replicas"]) is int + assert deployment["spec"]["replicas"] == value + return documents + +count = 0 +for chart in ("flex-auth", "access-engine"): + for encoding in ("yaml", "json", "cli"): + stopped = render(chart, 0, encoding) + ready = render(chart, 1, encoding) + normalized = copy.deepcopy(ready) + next(d for d in normalized if d["kind"] == "Deployment")["spec"]["replicas"] = 0 + assert stopped == normalized, "stop must change replicas only, including identical pod projection/PVC" + count += 2 + for encoding in ("yaml", "json"): + for value in (-1, 2, 1.5, 0.5, True, False, None, "0", "1", [], {}): + render(chart, value, encoding, success=False) + count += 1 + # Helm loses lexical 0 vs0.0 and1 vs1.0 before template evaluation. + for value in (0.0, 1.0): + render(chart, value, encoding) + count += 1 + for value in ("-1", "2", "0.5", "1.5", "true", "false", "null", "0.0", "1.0"): + render(chart, value, "cli", success=False) + count += 1 + for value in ("0", "1"): + render(chart, value, "string", success=False) + count += 1 +print(f"stopped/single-writer chart checks passed: {count} actual Helm renders") diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/workplans/FLEX-WP-0024-decision-envelope-authenticity.md new file mode 100644 index 00000000..f7373499 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/postimages/workplans/FLEX-WP-0024-decision-envelope-authenticity.md @@ -0,0 +1,327 @@ +--- +id: FLEX-WP-0024 +type: workplan +title: "Sign the decision envelope: the response channel is unauthenticated" +domain: infotech +repo: flex-auth +status: active +owner: claude +topic_slug: netkingdom +planning_priority: P1 +planning_order: 240 +depends_on_workplans: + - FLEX-WP-0021 +related_workplans: + - FLEX-WP-0019 + - FLEX-WP-0023 +created: "2026-09-06" +updated: "2026-10-16" +state_hub_workstream_id: "90577acd-6910-548d-a13e-1dbfdfb8ed27" +--- + +# FLEX-WP-0024 — Sign the decision envelope: the response channel is unauthenticated + +Opened by `FLEX-DEC-2026-010`, from a question `secrets-engine` asked rather +than answering locally: **how is the response channel authenticated?** They +declined to choose a transport control for flex-auth's service on the grounds +that it is not a consumer's call. Correct, and it is why this is a flex-auth +workplan and not a mitigation in their repo. + +The stance today: pins serve plain HTTP, `flex-auth.decision-record.v1` carries +no signature, and a responder that knows the package id and version — both +published — can return a well-formed `effect: allow` that passes every check a +consumer performs. + +## The two things not to get backwards + +**The digests do not authenticate the responder and cannot.** Every input to +them is either sent by the caller or published, so a forger reproduces +`request_digest`, `policy_package_digest`, and `registry_snapshot_digest` +exactly. Publishing more digests makes a forged envelope look *more* +authenticated, not less. + +**Fail-closed does not cover this.** It protects against a PDP that is absent, +not against one that lies. An unreachable PDP denies; a lying PDP allows. + +## 1. Publish the stance and stop publishing bare Service names + +```task +id: FLEX-WP-0024-T01 +status: done +priority: high +state_hub_task_id: "b8fad80d-5c44-5e26-a632-5096e0c0f3c5" +``` + +Owner: `flex-auth`. + +**Done 2026-09-06.** `docs/decision-record-contract.md` states that digest +recomputation is not responder verification, because the contract is the only +document a consumer is guaranteed to read. +`docs/operator-caller-access-path.md` carries the reproduced DNS probe and the +channel analysis. The trailing-dot FQDN replaced the bare Service name in +`examples/secrets-engine/README.md`, `SCOPE.md`, and the `FLEX-WP-0021-T05` +handover note — a bare `*.svc.cluster.local` name resolves through +`search ad.binect.de` to one unrelated public host, and a name for a service +that does not exist resolves to the same address. + +## 2. Choose the signature shape and key custody + +```task +id: FLEX-WP-0024-T02 +status: done +priority: high +state_hub_task_id: "5482f3cd-36cb-55e0-8c52-0ca2340ed4d7" +``` + +Owner: `flex-auth`; key custody is **not** ours to invent. + +- Detached signature over the canonical envelope, preferred over transport-only + mTLS for two reasons: it survives the transport, so a decision audited later + is still attributable, where mTLS authenticates a connection that no longer + exists; and it composes with `FLEX-DEC-2026-009`, since a record that names + its authenticated caller *and* carries flex-auth's signature is an audit + artifact where either alone is a claim. +- Decide the canonical form to sign. The existing canonical-JSON rules from + `docs/canonical-request-digest.md` are the obvious basis, but the signed + material is the **envelope**, not the request, and it must exclude the + signature field itself. Write it down before implementing: an implicit + canonicalisation rule is what `FLEX-DEC-2026-007` had to correct. +- **Do not mint a key in this repo.** Route custody via + `warden route find "decision envelope signing key"` and OpenBao, per the + estate credential-routing rule. flex-auth decides the shape; it does not own + key material. +- State key rotation and how a consumer discovers the current public key, since + a consumer that cannot rotate trust has a pinned key that will one day be + wrong. + +Gate: the canonical form, the algorithm, and the custody path are written down +and reviewed before any signing code exists. + +**Done 2026-09-14.** Written in `docs/decision-envelope-signature.md`: detached +Ed25519 over `json.Marshal` of the envelope with `signature` omitted; unsigned +is `{mode: unsigned}`; production key lives in OpenBao, not this repo +(proposed catalog id `flex-auth-decision-envelope-signing-key`). Test-only seed +is documented and is not the custody path. + +## 3. Implement signing and verification + +```task +id: FLEX-WP-0024-T03 +status: done +priority: high +state_hub_task_id: "041612ea-1be4-5997-8c32-49f8bcc50855" +``` + +Owner: `flex-auth`. + +- Add the signature to `schemas/decision_envelope.schema.json` as additive, and + state in the contract that `request_digest` is unaffected so no consumer + re-pins — the same note that was needed for v2 and for + `approval_binding_digest`. +- Emit unsigned when no key is configured, and say so in the envelope rather + than omitting the field silently. Absence stated, not implied — the + `pdp_digest` precedent. +- Ship a verification example and a fixture pair in + `examples/secrets-engine/replay/`: a genuine signed envelope and one whose + payload was altered after signing, so a consumer's verifier is tested against + a real failure rather than only a real success. **A verifier that has never + seen an invalid signature is untested**, which is the lesson from every + fixture suite in `FLEX-DEC-2026-008`. + +Gate: an altered envelope fails verification, an unaltered one passes, and a +consumer can perform both from the fixtures alone. + +**Done 2026-09-14.** Additive `signature` on the schema and envelope. Engine +emits `{mode: unsigned}` when no key is configured. Fixture pair: +`examples/secrets-engine/replay/decision_rotate_signed.json` verifies, +`decision_rotate_signed_tampered.json` does not, same `request_digest`. Public +key in `keys.json`. Tests in `internal/sign`. + +## 4. Report the gap to gate-house + +```task +id: FLEX-WP-0024-T04 +status: done +priority: medium +state_hub_task_id: "82d6b75e-e2c6-5e3d-a897-fbdfb7c9094e" +``` + +Owner: `flex-auth`. + +§6.4 obligates the PEP to act on a decision and §9.3 splits degraded mode by +owner, but **nothing in the standard requires a PDP's response to be +authenticable**. So a fully conformant PEP can trust a forged allow while +meeting every published obligation. That is a gap in the standard, not only in +this implementation, and it belongs in the outstanding v0.8 assent round +alongside the `FLEX-DEC-2026-009` finding. + +## T04 closed — 2026-09-07 + +**Done.** Reported as `FLEX-DEC-2026-011` F1, ranked blocking for +calling the §6.4 PEP obligations complete and explicitly not blocking anyone's +adoption of v0.8 — the condition already exists under v0.7 and is not created by +this version. §16 already carries the same observation one layer up, about the +standard's own publication path having no digest or freeze discipline; F1 is +that observation applied to the artifact the standard regulates. + +## 5. Wire owner signing into the decision pin and retained deployment charts + +```task +id: FLEX-WP-0024-T05 +status: done +priority: high +state_hub_task_id: "cfe4d275-9f02-5090-8391-06eea59d45c5" +``` + +2026-10-16 source follow-through: NetKingdom target Secrets Engine R2 owner review +found that the completed T03 signature package was used only by fixtures. Add +explicit mounted seed/kid configuration for `serve`, sign the final envelope before +recording, and expose inert existing-Secret mounts in both retained/new charts. +The API/sign verifier sources remain byte-identical. Local source validation and +review are in the Custodian `docs/changes/netkingdom-target-2026-10-16/access-verifier-review/`. +This is source preparation only, not signer admission or deployed acceptance. + +## 6. Admit custody, consumer trust, exact packages and native signed decisions + +```task +id: FLEX-WP-0024-T06 +status: progress +priority: high +state_hub_task_id: "7f785b52-bf62-5884-8d34-670d70bc9023" +``` + +Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin +admission, exact image and existing-Secret values rollout, owner-bound caller/tunnel, +and Secrets Engine verifier installation. Follow `docs/decision-envelope-signature.md` +for overlap/removal semantics. Retain signed producer record/consumer acceptance, +forged/altered/unsigned/wrong-pin/expired denial and rotation-removal receipts; +then update actual deployed status. No signer or runtime trust is admitted by the +source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns +consumer admission/enforcement. The old fixture completion never established this +gate, so the workplan is reopened rather than claiming it already passed. + + +2026-10-16 owner native preparation: T06 is `progress` because Access still owns +signer workload declaration, artifact/provenance and source/provider-join preparation; +no unqualified cross-owner wait is claimed. Read-only actual release metadata and +bounded proposal are in the Custodian +`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`. +The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but +is draft/nonresolvable; no production kid/public fingerprint, Secret projection or +custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080 +instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed +consumer native verification and must not form a circular blocking dependency. + +The first proposed native trial only signs the unchanged lifecycle package on the +existing retained release; `execute-ccr` remains denied. Current service-pilot +`runtime_admission := false` remains mandatory: the source ServicePilotInputs is +not yet wired into HTTP, and public service_actor_binding never authenticates the +caller. Later activation needs a complete reviewed provider join, exact policy and +operation admission, artifact digests, owner execution/rollback proposal and native +receipts. Preparation/local synthetic success grants none of those permissions. + + +2026-10-16 source-only native preparation adds the existing Secrets PDP ServiceAccount +to `tenancy.yaml` with evidence in `docs/native-signer-workload.json`, makes the +retained `flex-auth` audience explicit, pins CI archive fetch to the full source +revision, and adds offline native signer receipt verification using the existing +Secrets verifier. No key, trust, image publication or live activation is admitted. +The managed workload's missing rapp declaration remains explicit migration debt; +this source binding does not fabricate a package declaration or raise its grade. + + +2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual +HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls +refuse across every policy, including the old separately named positive fixture; +that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged. +Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are +source evidence only. B2 depends on independently reviewed Approval fact contract +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. + +2026-10-16 native admission prerequisite: fresh metadata confirms no namespace +PVC exists, and current outbox had neither OS writer exclusion nor a byte/startup +bound. Prepared existing-outbox hardening on exact canonical R3 base2b064ee: +nonblocking Linux flock held through Close/process lifetime before recovery, +64MiB source journal ceiling,2MiB record/atomic-batch bounds, streaming startup, +4KiB cursor, all-effect commit refusal and fenced unconfirmed rollback. Actual +process contention/crash release, capacity/prefix/restart and ENOSPC/fsync failure +proofs are part of the owner package. No new service, live mount, deletion or +custody grant; exact published successor, new PVC/PV retention/managed-package +owner disposition and independent consumer admission remain prerequisites. + +2026-10-16 owner package extraction record: canonical local package +`rapp-flex-auth-secrets-engine` now exists at commit +`d52f811ed5ec0be27c9c367e71d476125b3228be`, tree +`d4fbad71bdc8ef870603526ccd1f53195d0322af`; authoritative declaration +`declarations/rapp.yaml` SHA256 `0b722431bc5162dd86f72b15f12424e149aca8f2a8790adf2c57e8511ea73b4c`. +Repo Manager exact local resolution under `/home/worsch` returns +`rapp-flex-auth-secrets-engine` / workload `flex-auth-secrets-engine` / +deployable `flex-auth-secrets-engine`, ownership `access-engine`, readiness +`draft`. The retained lifecycle PDP is the only rollback boundary claimed; +tenant/user/canary and retired `rapp-secrets-engine` remain excluded. Normative +focused family validation passes; existing unrelated global rapp-forgejo error +and reef projection warning remain visible. Package extraction keeps source +`2b064eee510640224274ccdc7cefceb1ae13162e` and current image +`sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd` +unchanged. Exact local source resolution is no central/private read-placement, +reef binding, custody, native rollout or consumer trust admission. Future signer +image/config adoption remains separate. T06 remains `progress`; no second work +item duplicates this bounded dependency. + + +2026-10-16 bounded stopped-chart source preparation (Custodian +`access-native-admission/stopped-chart-r2`): both outbox charts admit numeric +replicaCount exactly 0 (maintenance stop) or 1 (single writer), with Recreate, +process flock and bounded persistence unchanged. Fractions, strings, booleans, +negative values and 2+ refuse; Helm decodes YAML/JSON integral 0.0/1.0 as the same +numeric values as 0/1, so lexical float spelling is not distinguished. Actual +Helm stopped/ready renders differ only in replicas and retain the same Platform +source-rendered deployment projection. Owner preparation and independent +coordinator checks approve this bounded source change only; 86 Helm renders and +the existing outbox/delivery/receiver-port suite pass. No native stop, bootstrap, +rollout, custody, public trust, resource or protected-effect approval follows. +The proposed immutable image remains source revision +`84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, ordinary CI402 verified digest +`sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. +This chart-only successor requires its own exact source/chart pin and reviewed +runtime scope; it can explicitly select the unchanged verified 84 image without +rebuilding it or claiming that its revision label names the new chart commit. +These chart/test/workplan-only paths do not trigger the existing image workflow. +Frozen rollout R1, Platform P2 and the package extraction2b/current05a remain +unchanged. T06 stays `progress`; actual stopped-fence verification and all native +admission gates remain open under the same existing task. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/r1-to-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/r1-to-r2.patch new file mode 100644 index 00000000..ad65ac34 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-admission/stopped-chart-r2/r1-to-r2.patch @@ -0,0 +1,29 @@ +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -302,3 +302,26 @@ + reef binding, custody, native rollout or consumer trust admission. Future signer + image/config adoption remains separate. T06 remains `progress`; no second work + item duplicates this bounded dependency. ++ ++ ++2026-10-16 bounded stopped-chart source preparation (Custodian ++`access-native-admission/stopped-chart-r2`): both outbox charts admit numeric ++replicaCount exactly 0 (maintenance stop) or 1 (single writer), with Recreate, ++process flock and bounded persistence unchanged. Fractions, strings, booleans, ++negative values and 2+ refuse; Helm decodes YAML/JSON integral 0.0/1.0 as the same ++numeric values as 0/1, so lexical float spelling is not distinguished. Actual ++Helm stopped/ready renders differ only in replicas and retain the same Platform ++source-rendered deployment projection. Owner preparation and independent ++coordinator checks approve this bounded source change only; 86 Helm renders and ++the existing outbox/delivery/receiver-port suite pass. No native stop, bootstrap, ++rollout, custody, public trust, resource or protected-effect approval follows. ++The proposed immutable image remains source revision ++`84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0`, ordinary CI402 verified digest ++`sha256:09a83b273740133518cc7e7915fb211381e06545d347d2dd1974042525ac981f`. ++This chart-only successor requires its own exact source/chart pin and reviewed ++runtime scope; it can explicitly select the unchanged verified 84 image without ++rebuilding it or claiming that its revision label names the new chart commit. ++These chart/test/workplan-only paths do not trigger the existing image workflow. ++Frozen rollout R1, Platform P2 and the package extraction2b/current05a remain ++unchanged. T06 stays `progress`; actual stopped-fence verification and all native ++admission gates remain open under the same existing task. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/brief.preexisting.bytes b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/brief.preexisting.bytes new file mode 100644 index 00000000..5e12ebd1 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/brief.preexisting.bytes @@ -0,0 +1,67 @@ + +# Custodian Brief — access-engine + +**Domain:** infotech +**Last synced:** 2026-10-16 14:30 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Sign the decision envelope: the response channel is unauthenticated +Progress: 5/6 done | workplan_id: `90577acd-6910-548d-a13e-1dbfdfb8ed27` + +**Open tasks:** +- ► 6. Admit custody, consumer trust, exact packages and native signed decisions `7f785b52` + +### The decision record has a declared emission guarantee and nothing that delivers it +Progress: 5/9 done | workplan_id: `84f5d9fe-b4c9-584a-b964-efe3e48af095` + +**Open tasks:** +- ! 2. Register flex-auth as an audit-core sender `89661908` + *(wait: GH-WP-0004-T12 refused the load-bearing failure-path exception (GH-DEC-2026-024). AUDIT-IN-0006 sender rows and governed token delivery remain pending.)* +- ! 4. Close G2 `423b3090` + *(wait: G2 needs T08 native heartbeat/drain and silence-finding evidence plus T06 reconciliation; local tests cannot close the delivery gap.)* +- ! 6. Reconciliation, profile check, and storage `bf92a951` + *(wait: Receiver counts use accepted_at, while local counts use commit time; audit-core must supply compatible window semantics or durable acceptance timestamps before a valid comparator can be implemented.)* +- ! 8. Admit and exercise native heartbeat/drain delivery `b3b52b30` + *(wait: Needs admitted sender/token mounts, founder approval of exact image/PVC/network rollout, and audit-core observer evidence; no production delivery is enabled.)* + +### Runtime rename: flex-auth to access-engine, run side by side, consumers switch one at a time +Progress: 0/8 done | workplan_id: `af0f8d77-af04-569b-8e79-6c47eb8f1087` + +**Open tasks:** +- ! Decide the target names and supersede the retain table `4f9a796d` +- ! Make the PDP accept both names `487525e5` +- ! Rename the source `ff7fd369` +- ! Stand up the access-engine namespace next to flex-auth `13b93d7a` +- ! Consumers switch over, one at a time `f0470e5c` +- ► Inventory every consumer reference and open handoffs `8f597899` +- · Retire the flex-auth runtime `e46c9c8e` +- … and 1 more open tasks + +### Repository identity migration from flex-auth to access-engine +Progress: 10/11 done | workplan_id: `99a661a8-b36c-5c1c-b78b-1e8930bcd0a9` + +**Open tasks:** +- ! 11. Soak, hand off residuals, and clean up the old checkout `46d8318b` + *(wait: Cleanup approved conditionally (e071b34e): runs once the listed soak evidence, SBOM-IN-0001 receipt and residual handoffs exist.)* + +### 2026-10-03 secret-read evaluator for ops-warden fetch checks +Progress: 1/2 done | workplan_id: `60941288-d1f8-58fe-ad38-d508b7d5eac2` + +**Open tasks:** +- ! Deploy the pin (founder go required) `aa00b29f` + *(wait: Install accepted retroactively (877c268b); steps 6-7 follow WARDEN-WP-0039-T03 token receipts and fetch URL.)* + +## Inbox Hygiene + +**Stale unread:** 5 message(s) older than 3 day(s) — triage at session start. +**Missing thread_id:** 2 unread message(s) lack supersession chains. + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integrate.py b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integrate.py new file mode 100644 index 00000000..87a9391d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integrate.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Integrate the approved owner record; preserve the unrelated staged brief.""" +import hashlib +import json +import subprocess +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = Path('/home/worsch/access-engine') +WP = 'workplans/FLEX-WP-0024-decision-envelope-authenticity.md' +BRIEF = '.custodian-brief.md' +def git(*args): + return subprocess.check_output(['git', '-C', str(REPO), *args]) +def sha(data): + return hashlib.sha256(data).hexdigest() +def save(name, data): + (HERE / name).write_text(json.dumps(data, indent=2) + '\n') + +meta = json.loads((HERE / 'own-record-metadata.json').read_text()) +assert sha((REPO / WP).read_bytes()) == meta['source_sha256'], 'Workplan preimage changed' +assert sha((HERE / 'own-record.patch').read_bytes()) == meta['patch_sha256'] +assert git('diff', '--cached', '--name-only').decode().splitlines() == [BRIEF], 'Unexpected staged paths' +brief_index = git('ls-files', '--stage', '--', BRIEF).decode().strip() +brief_worktree = (REPO / BRIEF).read_bytes() +head_before = git('rev-parse', 'HEAD').decode().strip() +records_before = sha((REPO / 'WORK-RECORDS.md').read_bytes()) +save('integration-preimage.json', {'head': head_before, 'workplan_sha256':meta['source_sha256'], + 'brief_index':brief_index, 'brief_worktree_sha256':sha(brief_worktree), + 'work_records_sha256':records_before, 'staged_paths':[BRIEF]}) +(HERE / 'brief.preexisting.bytes').write_bytes(brief_worktree) +git('apply', '--check', str(HERE / 'own-record.patch')) +git('apply', str(HERE / 'own-record.patch')) +assert sha((REPO / WP).read_bytes()) == meta['proposed_result_sha256'] +assert git('ls-files', '--stage', '--', BRIEF).decode().strip() == brief_index +assert (REPO / BRIEF).read_bytes() == brief_worktree +commit_output = git('commit', '--only', '-m', 'docs: record Access owner native execution plan review', '--', WP).decode() +owner_commit = git('rev-parse', 'HEAD').decode().strip() +assert git('diff-tree', '--no-commit-id', '--name-only', '-r', owner_commit).decode().splitlines() == [WP] +assert git('ls-files', '--stage', '--', BRIEF).decode().strip() == brief_index +assert (REPO / BRIEF).read_bytes() == brief_worktree +head_brief = git('ls-tree', 'HEAD', '--', BRIEF).decode().split() +git('update-index', '--cacheinfo', head_brief[0], head_brief[2], BRIEF) +assert git('diff', '--cached', '--name-only').decode().strip() == '' +try: + result = subprocess.run(['statehub', 'fix-consistency', '--repo', 'access-engine', + '--repo-path', str(REPO), '--no-writeback', '--json'], + capture_output=True, text=True) + (HERE / 'sync.stdout.json').write_text(result.stdout) + (HERE / 'sync.stderr.txt').write_text(result.stderr) + after_brief = (REPO / BRIEF).read_bytes() + after_index = git('ls-files', '--stage', '--', BRIEF).decode().strip() + # Restore only the captured unrelated brief; race-check both before writing. + assert (REPO / BRIEF).read_bytes() == after_brief + assert git('ls-files', '--stage', '--', BRIEF).decode().strip() == after_index + mode, blob, stage_path = brief_index.split(maxsplit=2) + assert stage_path == '0\t' + BRIEF + (REPO / BRIEF).write_bytes(brief_worktree) + git('update-index', '--cacheinfo', mode, blob, BRIEF) + assert (REPO / BRIEF).read_bytes() == brief_worktree + assert git('ls-files', '--stage', '--', BRIEF).decode().strip() == brief_index + save('integration-receipt.json', {'owner_commit':owner_commit, + 'owner_commit_paths':[WP], 'sync_return_code':result.returncode, + 'sync_stdout_sha256':sha(result.stdout.encode()), + 'sync_stderr_sha256':sha(result.stderr.encode()), + 'head_after_sync':git('rev-parse', 'HEAD').decode().strip(), + 'upstream':git('rev-parse', '@{upstream}').decode().strip(), + 'brief_index_exactly_restored':True, 'brief_worktree_exactly_restored':True, + 'work_records_before_sha256':records_before, + 'work_records_after_sha256':sha((REPO / 'WORK-RECORDS.md').read_bytes()), + 'workplan_after_sha256':sha((REPO / WP).read_bytes()), + 'status':git('status','--short').decode(), 'manual_push':False, + 'no_writeback_scope':'Disables C15/C23 status repairs only; brief generation/auto-commit and normal publication still run.'}) + print(json.dumps({'owner_commit':owner_commit,'sync_return_code':result.returncode, + 'brief_restored':True,'manual_push':False})) +finally: + # No broad checkout/reset and no suppression of a failed publication. + pass diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integration-preimage.json b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integration-preimage.json new file mode 100644 index 00000000..134361ef --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/integration-preimage.json @@ -0,0 +1,10 @@ +{ + "head": "dc042022f7c30671cf0a7605cf43ef6d96d04242", + "workplan_sha256": "3a3fc05528eb869204811d90d12ef5a3ac3b6d537b5b7a8cccbc15b6483a002f", + "brief_index": "100644 5e12ebd1d7606f88c606a5cc23b2c24d1afa8a3a 0\t.custodian-brief.md", + "brief_worktree_sha256": "009d2b150ad8f519818187298527da687946dc019bc35c72821170e7dd1942f8", + "work_records_sha256": "f713204e5c524f22e4a831fa480bf0e2018e206cd48f55fbf9d5472bfca104a5", + "staged_paths": [ + ".custodian-brief.md" + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/manifest.json b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/manifest.json new file mode 100644 index 00000000..76351eb5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/manifest.json @@ -0,0 +1,22 @@ +{ + "schema": "access-engine.native-plan-review-artifacts.v1", + "work_item": "FLEX-WP-0024-T06", + "files": { + "access-native-plan-review.json": "5f49b9d703b3b0c050be8b45a97aab07b0b3af13e9ce56139f886e88a0dc740d", + "brief.preexisting.bytes": "009d2b150ad8f519818187298527da687946dc019bc35c72821170e7dd1942f8", + "execution-supplement.md": "2622cf68d33f5e5f4ede68655c250f70a8132ed03cb4b7c33bdbeec86100e25d", + "formatting-scanner-receipt.json": "443eeba3afc887384903dcefaa560c328eca2d57b470adc55832b4cc22b67a42", + "formatting-successor.json": "3769435ed8a8bc16893642a2e13cbb22feafa0b0f5fbdde74e835413618d69e3", + "integrate.py": "23619d448334e790d98f5972cefe9f51dc2dce98fecb359a266dfc6ed990fd44", + "integration-preimage.json": "3d96b83644ac41f082801a867329320ef1779fd622fb3a4ad96b4d6e72784af8", + "integration-receipt.json": "c019d3594131872d15c52e3de1620a796b0b03fd4612eed782ce12a374566a10", + "own-record-metadata.json": "758db57e818c1dcf312d3e25ceaccd09c848091f500eacb78f12db9f1a080c7b", + "own-record.patch": "d3eacdc45194913a2861503f0fc81332cfb414c5bece49ae8adf2dbc698eaabe", + "progress-receipt.json": "c1e2828d1dc169b164510c0c6c9ca9134876b2d44361e642e20da0dbef628cd7", + "sync.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "sync.stdout.json": "1b0b0b7b02fd25fb21ceb7c1a30c70632221db1c73183c868db765e9915dc7e8", + "verification.json": "05e88b9c4362a9655be7ac19533fa3079558e7d2b57c446b90729a40cc88766f", + "verify.py": "ebeddcbdeb81d990999ac444f7f1c2e0d786a672b03234fc232092a6ea75835d" + }, + "execution_authorized": false +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record-metadata.json b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record-metadata.json new file mode 100644 index 00000000..ea779ac5 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record-metadata.json @@ -0,0 +1,14 @@ +{ + "schema": "access-engine.owner-record-proposed-patch.v1", + "source_path": "/home/worsch/access-engine/workplans/FLEX-WP-0024-decision-envelope-authenticity.md", + "source_sha256": "3a3fc05528eb869204811d90d12ef5a3ac3b6d537b5b7a8cccbc15b6483a002f", + "patch_sha256": "d3eacdc45194913a2861503f0fc81332cfb414c5bece49ae8adf2dbc698eaabe", + "proposed_result_sha256": "cc94a493c23c16a3baa718b75ef5d48a596c6386c11534d50fd4839a6f0448f2", + "task_id": "FLEX-WP-0024-T06", + "status_before": "progress", + "status_after": "progress", + "integrated": true, + "instruction": "Report to coordinator before integration; after authorized integration run statehub fix-consistency and log progress.", + "canonical_commit": "d328a2601095686f74376d87791bff01e2a3a2a3", + "integration_receipt": "integration-receipt.json" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record.patch new file mode 100644 index 00000000..98761aea --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/own-record.patch @@ -0,0 +1,24 @@ +--- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md ++++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +@@ -325,3 +325,21 @@ + Frozen rollout R1, Platform P2 and the package extraction2b/current05a remain + unchanged. T06 stays `progress`; actual stopped-fence verification and all native + admission gates remain open under the same existing task. ++ ++ ++2026-10-16 Access owner exact native plan review (Custodian ++`access-native-plan-review-next/`): approved bounded Access workload/Helm/outbox/ ++stopped-only rollback plan for frozen rollout R2 manifest5be7a7f and Platform P4 ++manifest9eec861, chartdc0420/image84c78e OCI09a83b/projection8ce375f. Offline ++verification checks every R2 manifest/source digest and reproduces both exact ++Helm render bytes. The mandatory execution supplement names actual repo/Helm/CI ++paths and excludes unrelated legacy rollback recipes, records the owner/founder ++manual-writer acknowledgement, supplies a verified stopped-revision rollback ++shape, and separates120-second stop/readiness gates from atomic rollback and ++the600-second recovery decision. No production source change was needed. ++Existing read-only preflight admits all7exact resources but still observes ++unsigned05a/revision4running, no installed stopped fence or held window. T06 ++stays progress: actual founder entity/expiry/window/executor and five final ++external scope dispositions, custody/trust/caller, actual stopped Helm revision, ++PVC/PV Retain and native producer/consumer/restart/failure receipts remain open. ++No live mutation, credential access, new key, authority or protected effect follows. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/progress-receipt.json b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/progress-receipt.json new file mode 100644 index 00000000..0cb2bb31 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/progress-receipt.json @@ -0,0 +1,14 @@ +{ + "workplan_id": "90577acd-6910-548d-a13e-1dbfdfb8ed27", + "id": "79336b2d-260a-46b9-a74f-e2e6aaab1fb7", + "topic_id": null, + "task_id": "7f785b52-bf62-5884-8d34-670d70bc9023", + "decision_id": null, + "event_type": "note", + "summary": "Access owner bounded native plan review integrated at d328a2601095686f74376d87791bff01e2a3a2a3 under FLEX-WP-0024-T06. Exact frozen R2/P4 and source/Helm render checks pass; execution supplement records writer acknowledgement, stopped-only rollback and 120s/600s gates. Normal fix-consistency --no-writeback published commit and updated T06 description: 0 failures, 0 automation errors, 10 existing warnings. Preexisting staged brief index/worktree and dirty WORK-RECORDS bytes preserved exactly. No native authority, custody/trust/caller admission or cluster mutation. Evidence: Custodian access-native-plan-review-next/.", + "detail": null, + "author": "codex", + "session_id": null, + "created_at": "2026-10-16T19:16:32.489704Z", + "workstream_id": "90577acd-6910-548d-a13e-1dbfdfb8ed27" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/sync.stderr.txt b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/sync.stderr.txt new file mode 100644 index 00000000..e69de29b diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/sync.stdout.json b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/sync.stdout.json new file mode 100644 index 00000000..de10ea14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/sync.stdout.json @@ -0,0 +1,121 @@ +{ + "repo_slug": "access-engine", + "repo_path": "/home/worsch/access-engine", + "source_revision": "d328a2601095686f74376d87791bff01e2a3a2a3", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'FLEX-WP-ADHOC-2026-10-03-T02' in workplan 'FLEX-WP-ADHOC-2026-10-03' \u2014 every depends_on target is terminal (WARDEN-WP-0039-T03)", + "file_path": "workplans/ADHOC-2026-10-03.md#FLEX-WP-ADHOC-2026-10-03-T02", + "db_id": "", + "file_value": "WARDEN-WP-0039-T03", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'FLEX-WP-ADHOC-2026-10-03-T02' in workplan 'FLEX-WP-ADHOC-2026-10-03' \u2014 decision 877c268b-2089-42f1-a811-d98f549c1cb2 is resolved", + "file_path": "workplans/ADHOC-2026-10-03.md#FLEX-WP-ADHOC-2026-10-03-T02", + "db_id": "", + "file_value": "877c268b-2089-42f1-a811-d98f549c1cb2", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-20", + "message": "Workplan dependency target 'NK-WP-0024' is not linked to State Hub", + "file_path": "workplans/FLEX-WP-0009-user-engine-production-policy-service.md", + "db_id": "", + "file_value": "NK-WP-0024", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-40", + "message": "blocker satisfied, task still wait: 'FLEX-WP-0020-T11' in workplan 'FLEX-WP-0020' \u2014 decision e071b34e-c4d4-49d0-824d-2c58bb0ae496 is resolved", + "file_path": "workplans/FLEX-WP-0020-repository-identity-migration.md#FLEX-WP-0020-T11", + "db_id": "", + "file_value": "e071b34e-c4d4-49d0-824d-2c58bb0ae496", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'FLEX-WP-0024-T06': file content differs from DB (file wins)", + "file_path": "workplans/FLEX-WP-0024-decision-envelope-authenticity.md#FLEX-WP-0024-T06", + "db_id": "7f785b52-bf62-5884-8d34-670d70bc9023", + "file_value": "Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin\nadmission, exact image and existing-S", + "db_value": "Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin\nadmission, exact image and existing-S", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-37", + "message": "Residual workplan 'flex-wp-0031' has no origin/origin_ref provenance (not invented)", + "file_path": "workplans/FLEX-WP-0031-decision-record-emission.md", + "db_id": "84f5d9fe-b4c9-584a-b964-efe3e48af095", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Human wait task 'FLEX-WP-0036-T01' in workplan 'FLEX-WP-0036' has needs_human: true without decision_id \u2014 unrouted human gate", + "file_path": "workplans/FLEX-WP-0036-runtime-rename-access-engine.md#FLEX-WP-0036-T01", + "db_id": "", + "file_value": "needs_human: true", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Human wait task 'FLEX-WP-0036-T04' in workplan 'FLEX-WP-0036' has needs_human: true without decision_id \u2014 unrouted human gate", + "file_path": "workplans/FLEX-WP-0036-runtime-rename-access-engine.md#FLEX-WP-0036-T04", + "db_id": "", + "file_value": "needs_human: true", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "5 unread inbox message(s) older than 3 day(s): ops-warden:9eab1dc9, net-kingdom:31561723, secrets-engine:fd77c280, the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "2 unread message(s) lack thread_id for supersession tracking: the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 38/38", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-22 fixed: task 7f785b52\u2026 description updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 10, + "info": 0 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/verify.py b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/verify.py new file mode 100644 index 00000000..5bf3fc59 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-plan-review-next/verify.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Offline exact-source owner review verification. No cluster/credential access.""" +import copy +import hashlib +import io +import json +from pathlib import Path +import subprocess +import tarfile +import tempfile +import yaml + +HERE = Path(__file__).resolve().parent +WAVE = HERE.parent +ACCESS = Path('/home/worsch/access-engine') +CHART = 'dc042022f7c30671cf0a7605cf43ef6d96d04242' +IMAGE = '84c78e072c8f3daf2bc3b90f8efc8fbeb16c3cf0' +def digest(data): + return hashlib.sha256(data).hexdigest() +def gitbytes(revision, path): + return subprocess.check_output(['git', '-C', str(ACCESS), 'show', f'{revision}:{path}']) +def write(name, data): + (HERE / name).write_text(json.dumps(data, indent=2) + '\n') + +pins = {} +for rel in ['access-native-admission/rollout-r2/manifest.json', + 'platform-signer-custody/native-adapter-r2/native-packet-p4/manifest.json', + 'native-preflight-r1/live-preflight.json', + 'native-preflight-r1/cluster-native-plan-review.json']: + pins[rel] = digest((WAVE / rel).read_bytes()) +assert pins['access-native-admission/rollout-r2/manifest.json'] == '5be7a7f438624dcb35b7dbc96e7c9e5a8e2d5d1b1966cb4338aa5aecbe3a8b59' +assert pins['platform-signer-custody/native-adapter-r2/native-packet-p4/manifest.json'] == '9eec861838a8e9138f2c821cff3014998fbe74bb01447642bf50d44e8b3b7cb3' +rollout = WAVE / 'access-native-admission/rollout-r2' +manifest = json.loads((rollout / 'manifest.json').read_text()) +for path, expected in manifest['files'].items(): + assert 'sha256:' + digest((rollout / path).read_bytes()) == expected, path +evidence = json.loads((rollout / 'render-evidence.json').read_text()) +for path, expected in evidence['source_files'].items(): + assert 'sha256:' + digest(gitbytes(CHART, path)) == expected, path + if path.startswith('examples/'): + assert gitbytes(CHART, path) == gitbytes(IMAGE, path) + +with tempfile.TemporaryDirectory(prefix='access-plan-review-') as temporary: + root = Path(temporary) + archive = subprocess.check_output(['git', '-C', str(ACCESS), 'archive', CHART]) + with tarfile.open(fileobj=io.BytesIO(archive)) as tar: + tar.extractall(root, filter='data') + states = [] + for phase in ['stopped', 'activation']: + command = ['helm', 'template', 'flex-auth-secrets-engine', str(root / 'charts/flex-auth'), + '--namespace', 'flex-auth', '-f', str(root / 'values/secrets-engine.yaml'), + '-f', str(rollout / f'{phase}-values.proposed.yaml')] + rendered = subprocess.check_output(command) + assert rendered == (rollout / f'{phase}-source-render.yaml').read_bytes() + states.append([x for x in yaml.safe_load_all(rendered) if x]) + stopped, ready = states + normalized = copy.deepcopy(ready) + next(x for x in normalized if x['kind'] == 'Deployment')['spec']['replicas'] = 0 + assert normalized == stopped + deployment = next(x for x in stopped if x['kind'] == 'Deployment') + assert deployment['spec']['strategy'] == {'type': 'Recreate'} + assert deployment['spec']['replicas'] == 0 + assert not any(x['kind'] == 'Secret' for x in stopped) + pvc = next(x for x in stopped if x['kind'] == 'PersistentVolumeClaim') + assert pvc['metadata']['annotations']['helm.sh/resource-policy'] == 'keep' + assert pvc['spec']['resources']['requests']['storage'] == '1Gi' + +paths = ['AGENTS.md', 'INTENT.md', 'SCOPE.md', '.forgejo/workflows/image.yaml', + '.forgejo/workflows/ci-smoke.yaml', '.github/workflows/ci.yml', 'Makefile', + 'deploy/README.md', 'railiance/app.toml', 'tools/publish-runtime-images.sh', + 'internal/emission/outbox.go', 'internal/emission/lock_linux.go', + 'internal/emission/lock_other.go', 'internal/emission/delivery.go', + 'internal/decision/engine.go', 'cmd/access-engine/main.go'] +source_pins = {p: digest(gitbytes(CHART, p)) for p in paths} +source_pins['workplans/FLEX-WP-0024-decision-envelope-authenticity.md'] = digest( + (ACCESS / 'workplans/FLEX-WP-0024-decision-envelope-authenticity.md').read_bytes()) +image_source = {p: digest(gitbytes(IMAGE, p)) for p in paths if p.startswith(('internal/', 'cmd/'))} +for p, sha in image_source.items(): + assert source_pins[p] == sha, p +for p in ['.forgejo/workflows/image.yaml', '.forgejo/workflows/ci-smoke.yaml', '.github/workflows/ci.yml']: + data = gitbytes(CHART, p).decode() + assert 'helm upgrade' not in data and 'kubectl apply' not in data + +preflight = json.loads((WAVE / 'native-preflight-r1/live-preflight.json').read_text()) +observations = preflight['observations'] +writer_observations = {k: observations[k] for k in ['hpa', 'jobs', 'cronjobs', 'daemonsets', 'statefulsets']} +assert all(x['ok'] and not x['rows'] for x in writer_observations.values()) +write('verification.json', { + 'schema': 'access-engine.native-plan-offline-verification.v1', + 'frozen_input_sha256': pins, 'chart_revision': CHART, 'image_revision': IMAGE, + 'reviewed_source_sha256': {p: 'sha256:' + value for p, value in source_pins.items()}, + 'image_runtime_source_sha256': {p: 'sha256:' + value for p, value in image_source.items()}, + 'frozen_manifest_file_hashes_verified': True, 'chart_source_hashes_verified': True, + 'actual_exact_helm_renders_equal_frozen_bytes': True, + 'stopped_ready_only_replicas_differ': True, 'runtime_sources_identical_chart_and_image': True, + 'reviewed_repo_ci_has_no_helm_upgrade_or_kubectl_apply': True, + 'dated_namespace_competing_controller_observations': writer_observations, + 'observation_time': preflight['observed_at'], 'live_execution': False, + 'no_credential_or_kubeconfig_access': True}) +print('Verified frozen R2/P4 hashes, exact source files and two byte-identical Helm renders; no native access.') diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/collect-readonly.py b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/collect-readonly.py new file mode 100644 index 00000000..ba6bc9dd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/collect-readonly.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Allowlisted metadata only: never query Secret objects, logs, env or exec.""" +import datetime,json,subprocess +from pathlib import Path +out=Path(__file__).resolve().parent +queries={ +'nodes':['kubectl','get','nodes','-o','jsonpath={range .items[*]}{.metadata.name}{" "}{.status.addresses}{"\\n"}{end}'], +'deployment':['kubectl','-n','flex-auth','get','deployment','flex-auth-secrets-engine','-o','jsonpath={.metadata.name}{"\\n"}{.metadata.labels}{"\\n"}{.metadata.annotations.meta\\.helm\\.sh/release-name}{"\\n"}{.metadata.annotations.meta\\.helm\\.sh/release-namespace}{"\\n"}{.spec.template.spec.containers[0].image}{"\\n"}{.spec.template.spec.containers[0].args}{"\\n"}{.spec.template.spec.serviceAccountName}{"\\n"}{.spec.template.spec.containers[0].volumeMounts}{"\\n"}{.metadata.generation}{" "}{.status.observedGeneration}{" "}{.status.availableReplicas}{"\\n"}'], +'pods':['kubectl','-n','flex-auth','get','pods','-l','app.kubernetes.io/name=flex-auth-secrets-engine','-o','jsonpath={range .items[*]}{.metadata.name}{" "}{.status.phase}{" "}{.spec.nodeName}{" "}{.status.containerStatuses[0].imageID}{"\\n"}{end}'], +'service':['kubectl','-n','flex-auth','get','service','flex-auth-secrets-engine','-o','jsonpath={.metadata.name}{" "}{.spec.clusterIP}{" "}{.spec.selector}{" "}{.spec.ports}{"\\n"}'], +'helm_release':['helm','list','-n','flex-auth','--filter','^flex-auth-secrets-engine$','-o','json'], +'custody_route':['warden','route','show','flex-auth-decision-envelope-signing-key','--json']} +results={} +for name,args in queries.items(): + r=subprocess.run(args,capture_output=True,text=True,timeout=20) + # Never expose raw unexpected CLI errors. + results[name]={'command':args,'exit_code':r.returncode,'output':r.stdout if r.returncode==0 else None,'stderr_present':bool(r.stderr)} +(out/'live-metadata-r1.json').write_text(json.dumps({'schema':'access-engine.readonly-native-metadata.v1','observed_at_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'boundary':'metadata reads only; no Secret objects, credential requests, tokens, exec, logs or writes','queries':results},indent=2)+'\n') +print({name:record['exit_code'] for name,record in results.items()}) diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json new file mode 100644 index 00000000..ebac1864 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json @@ -0,0 +1,92 @@ +{ + "schema": "access-engine.readonly-native-metadata.v1", + "observed_at_utc": "2026-10-16T07:24:16.644831+00:00", + "boundary": "metadata reads only; no Secret objects, credential requests, tokens, exec, logs or writes", + "queries": { + "nodes": { + "command": [ + "kubectl", + "get", + "nodes", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.status.addresses}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "239.62.205.92.host.secureserver.net [{\"address\":\"92.205.62.239\",\"type\":\"InternalIP\"},{\"address\":\"2a00:1169:111:6d70::\",\"type\":\"InternalIP\"},{\"address\":\"239.62.205.92.host.secureserver.net\",\"type\":\"Hostname\"}]\n", + "stderr_present": false + }, + "deployment": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "deployment", + "flex-auth-secrets-engine", + "-o", + "jsonpath={.metadata.name}{\"\\n\"}{.metadata.labels}{\"\\n\"}{.metadata.annotations.meta\\.helm\\.sh/release-name}{\"\\n\"}{.metadata.annotations.meta\\.helm\\.sh/release-namespace}{\"\\n\"}{.spec.template.spec.containers[0].image}{\"\\n\"}{.spec.template.spec.containers[0].args}{\"\\n\"}{.spec.template.spec.serviceAccountName}{\"\\n\"}{.spec.template.spec.containers[0].volumeMounts}{\"\\n\"}{.metadata.generation}{\" \"}{.status.observedGeneration}{\" \"}{.status.availableReplicas}{\"\\n\"}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine\n{\"app.kubernetes.io/instance\":\"flex-auth-secrets-engine\",\"app.kubernetes.io/managed-by\":\"Helm\",\"app.kubernetes.io/name\":\"flex-auth-secrets-engine\",\"app.kubernetes.io/part-of\":\"flex-auth\"}\nflex-auth-secrets-engine\nflex-auth\nforgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd\n[\"serve\",\"--addr\",\"0.0.0.0:8080\",\"--registry\",\"/opt/flex-auth/examples/secrets-engine/registry_snapshot.json\",\"--policy\",\"/opt/flex-auth/examples/secrets-engine/policy_package.md\",\"--caller-auth-mode\",\"enforce\",\"--caller-kubernetes-url\",\"https://10.43.0.1\",\"--caller-binding\",\"secrets-engine=system:serviceaccount:secrets-engine:secrets-engine\"]\nflex-auth-secrets-engine\n[{\"mountPath\":\"/var/run/secrets/flex-auth-reviewer\",\"name\":\"flex-auth-reviewer\",\"readOnly\":true}]\n4 4 1\n", + "stderr_present": false + }, + "pods": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "pods", + "-l", + "app.kubernetes.io/name=flex-auth-secrets-engine", + "-o", + "jsonpath={range .items[*]}{.metadata.name}{\" \"}{.status.phase}{\" \"}{.spec.nodeName}{\" \"}{.status.containerStatuses[0].imageID}{\"\\n\"}{end}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine-6467b96c6f-ncl5t Running 239.62.205.92.host.secureserver.net forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd\n", + "stderr_present": false + }, + "service": { + "command": [ + "kubectl", + "-n", + "flex-auth", + "get", + "service", + "flex-auth-secrets-engine", + "-o", + "jsonpath={.metadata.name}{\" \"}{.spec.clusterIP}{\" \"}{.spec.selector}{\" \"}{.spec.ports}{\"\\n\"}" + ], + "exit_code": 0, + "output": "flex-auth-secrets-engine 10.43.40.221 {\"app.kubernetes.io/name\":\"flex-auth-secrets-engine\"} [{\"name\":\"http\",\"port\":8080,\"protocol\":\"TCP\",\"targetPort\":\"http\"}]\n", + "stderr_present": false + }, + "helm_release": { + "command": [ + "helm", + "list", + "-n", + "flex-auth", + "--filter", + "^flex-auth-secrets-engine$", + "-o", + "json" + ], + "exit_code": 0, + "output": "[{\"name\":\"flex-auth-secrets-engine\",\"namespace\":\"flex-auth\",\"revision\":\"4\",\"updated\":\"2026-09-14 00:46:30.759520357 +0200 CEST\",\"status\":\"deployed\",\"chart\":\"flex-auth-0.2.0\",\"app_version\":\"0.1.0\"}]\n", + "stderr_present": false + }, + "custody_route": { + "command": [ + "warden", + "route", + "show", + "flex-auth-decision-envelope-signing-key", + "--json" + ], + "exit_code": 0, + "output": "{\n \"id\": \"flex-auth-decision-envelope-signing-key\",\n \"title\": \"Ed25519 key that signs flex-auth decision envelopes\",\n \"owner_repo\": \"railiance-platform\",\n \"subsystem\": \"OpenBao + flex-auth\",\n \"warden_executes\": false,\n \"warden_role\": \"route\",\n \"exec_capable\": false,\n \"resolvable\": false,\n \"wiki_ref\": \"wiki/playbooks/flex-auth-decision-envelope-signing-key.md#worker-checklist\",\n \"canon_ref\": \"flex-auth/docs/decision-envelope-signature.md\",\n \"reviewed\": \"2026-09-14\",\n \"status\": \"draft\",\n \"risk\": \"high\",\n \"high_risk\": true,\n \"workload_ref\": {\n \"applicability\": \"applicable\",\n \"rapp_id\": null,\n \"name\": null,\n \"deployable\": null,\n \"declaration_ref\": null,\n \"reason\": null,\n \"unknown_reason\": \"flex-auth has not published an authoritative workload identity declaration for envelope signing.\",\n \"resolution\": \"unknown\"\n },\n \"has_rotation\": false,\n \"delegation\": {\n \"mode\": \"interim\",\n \"intended_owner\": \"railiance-platform\",\n \"blocked_on\": \"Shape is chosen in flex-auth (FLEX-WP-0024-T02). OpenBao KV path, Kubernetes auth role, and paste_once_provision of the private seed are not built. Testdata key in flex-auth is not this lane.\",\n \"reviewed\": \"2026-09-14\",\n \"verified\": \"unverified\",\n \"is_verified\": false,\n \"implicit\": false\n },\n \"need_keywords\": [\n \"flex-auth\",\n \"decision\",\n \"envelope\",\n \"signing\",\n \"signature\",\n \"ed25519\",\n \"responder\",\n \"authenticity\",\n \"FLEX-WP-0024\"\n ],\n \"next_action\": \"next action on `railiance-platform` \\u2014 see `wiki/playbooks/flex-auth-decision-envelope-signing-key.md#worker-checklist`\"\n}\n", + "stderr_present": false + } + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/native-admission-proposal-r1.json b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/native-admission-proposal-r1.json new file mode 100644 index 00000000..dc96eca3 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/native-admission-proposal-r1.json @@ -0,0 +1,179 @@ +{ + "schema": "access-engine.native-admission-preparation.v1", + "proposal_id": "FLEX-WP-0024-native-signer-20261016", + "revision": "R1-preparation", + "status": "owner preparation complete; not executable until named missing inputs are supplied and exact revision reviewed", + "authority": "Read-only source/metadata preparation authorized by coordinator. No trust admission, credentials, key generation, live changes, policy enablement or foreign approval. Supervision summary grants no authority; this scope remains supervised.", + "scope": { + "beneficiary": "FLEX-WP-0024-T06", + "coordination_driver": "CUST-WP-0080", + "initial_target": "Existing flex-auth/flex-auth-secrets-engine lifecycle decision pin and Secrets Engine consumer only", + "purpose": "Authenticate the existing decision producer and prove admitted consumer verification; preserve execute-ccr DENY until separately reviewed source joins and exact operation admission exist", + "explicit_pilot": "Existing secrets-engine.task-execution.service-pilot source, SECRETS-WP-0013/RPF-WP-0035-T12; no new broker or grant" + }, + "current_live": { + "evidence": "live-metadata-r1.json", + "cluster_node_ip": "92.205.62.239", + "namespace": "flex-auth", + "release": "flex-auth-secrets-engine", + "release_revision": 4, + "chart": "flex-auth-0.2.0", + "deployment": "flex-auth-secrets-engine", + "pod": "flex-auth-secrets-engine-6467b96c6f-ncl5t", + "image": "forgejo.coulomb.social/coulomb/flex-auth@sha256:05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", + "source_association": { + "historical_owner_record": "values/secrets-engine.yaml associates that digest with dd8dd517438b876fdadf27770e3f7e7f55ea69cf", + "limit": "Live image metadata proves digest, not source provenance. No fresh CI/provenance receipt was inspected; do not infer current signer source is deployed." + }, + "registry": "/opt/flex-auth/examples/secrets-engine/registry_snapshot.json", + "policy": "/opt/flex-auth/examples/secrets-engine/policy_package.md", + "service": "flex-auth-secrets-engine.flex-auth.svc.cluster.local.:8080", + "service_cluster_ip": "10.43.40.221", + "caller_auth_mode": "enforce", + "caller_kubernetes_url": "https://10.43.0.1", + "caller_binding": "secrets-engine=system:serviceaccount:secrets-engine:secrets-engine", + "reviewer_service_account": "flex-auth/flex-auth-secrets-engine", + "signer_args_or_mounts_present": false, + "observed_available_replicas": 1 + }, + "source_delivery": { + "revision": "ec21518e25df2a58444f19ff3413ce0b83383d3c", + "signing_integration": "ec21518e25df2a58444f19ff3413ce0b83383d3c", + "chart": "charts/flex-auth retained runtime, updated at exact source revision", + "policy_pins_offline_owner_function": { + "examples/secrets-engine-service-task-execution/policy_package.md": { + "digest": "sha256:b034dc8ad208889113ce475d33df1589f5b84cb817bbb361d10638e23e394a6e", + "id": "secrets-engine.task-execution.service-pilot", + "valid": true, + "version": "v1" + }, + "examples/secrets-engine/policy_package.md": { + "digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4", + "id": "secrets-engine.catalog-lane.lifecycle", + "valid": true, + "version": "v2" + } + }, + "evaluator": "flex-auth/local", + "mode": "standalone", + "image_build": "Owner Forgejo CI legacy package publishing path .forgejo/workflows/image.yaml and tools/publish-runtime-images.sh; no workstation image build", + "candidate_image_digest": null, + "compatibility": "Containerfile ENTRYPOINT access-engine and retains both /opt/access-engine/examples and /opt/flex-auth/examples paths; avoid combining signer rollout with namespace/identity rename", + "public_package_rename": "Not needed; existing admitted legacy OCI package remains sufficient" + }, + "custody_and_independent_trust": { + "exact_route": "warden route show flex-auth-decision-envelope-signing-key --json", + "route_state": "draft, nonresolvable; exact entry exists even though fuzzy find exits1 with adjacent matches", + "owner": "railiance-platform/OpenBao", + "ops_warden_executes": false, + "proposed_source_path": "platform/workloads/flex-auth/envelope-signing", + "proposed_fields": [ + "ED25519_PRIVATE_KEY", + "ED25519_PUBLIC_KEY", + "kid" + ], + "private_encoding_required_by_integrated_source": "Unpadded strict base64url 32-byte seed file; mount0440 accepted, group-write/world bits refused; key read once at startup", + "missing": [ + "Authoritative signer workload identity declaration", + "Platform custody lane, KV/auth role and owner-reviewed projection/Secret mechanism", + "Owner-selected production kid and public key/fingerprint with independently authenticated custody receipt", + "Named existing Secret/key and exact read-only projection path", + "Public admission receipt for each consumer and intended policy package/version/digest", + "Native rotation/revocation/retained audit verification plan" + ], + "public_key": null, + "kid": null, + "fingerprint": null, + "custody_receipt": null, + "bootstrap_rule": "Receive reviewed public material and policy pins independently from custody/owner. Do not trust responder /v1/keys or infer authenticity from policy digest; /v1/keys is not implemented. Do not use fixture keys.", + "consumer_format": "secrets-engine.pdp-trust.v1: exact evaluator/mode/policy_package/policy_version/policy_package_digest and kid->public key map; operator-controlled absolute regular file, no group/world write. Installed reviewed Go helper required." + }, + "enforced_caller_and_remaining_join": { + "existing_contract": "TokenReview authenticates bearer against configured Kubernetes API/CA and maps returned principal exactly to resource.system. Caller provenance is signed. The consumer token is not the PDP reviewer token.", + "audience_caution": "Live args omit explicit audience (legacy binary defaults flex-auth). Current source has bounded flex-auth/access-engine audience compatibility; ServicePilotInputs currently requires record.Audience exactly flex-auth. Preserve explicit flex-auth on first pilot and test alternate spelling refusal before any broader admission; do not infer compatibility grants subject authority.", + "new_proposed_mapping": { + "version": "proposed-service-task-caller/v1", + "transport_principal": "system:serviceaccount:secrets-engine:secrets-engine", + "subject_id": "secrets-engine", + "subject_type": "service", + "tenant": "tenant:platform" + }, + "public_binding_rule": "service_actor_binding is public canonical request metadata, not authentication. ServicePilotInputs.Prepare must independently compare actual reviewed transport, fixed subject/type/tenant and exact binding; reject extras and unverified/delegation flags. Deadline must be positive and no later than reviewed transport expiry.", + "current_gap": "ServicePilotInputs is only source input preparation and tests; cmd/access-engine /v1/check routes Authorize(resource.system) directly to Engine.Check(request). No Prepare call or trusted service input reaches engine. Existing system mapping alone cannot authenticate arbitrary request subjects.", + "other_required_joins": [ + "Authenticate served Approval origin and verified human approver as separate principal", + "Join exact approval-free/request/submitted digests to actual compiled execution descriptor and unchanged canonical request", + "Retain trusted provenance outside caller-forgeable request flags; no field named verified/owner_admission can enable policy", + "Downstream consumer/Platform independently join exact consumed approval/decision/request, journal admission, full source/state/target/body/deadline/use constraints" + ], + "future_api_rule": "Integrate through existing owner decision route and provider adapters; no second PDP or policy evaluator in consumer" + }, + "closed_policy_gates": { + "legacy_lifecycle": "secrets-engine.catalog-lane.lifecycle v2 supports twelve original lifecycle actions, denies execute-ccr unknown_action; do not relabel execute as apply", + "service_pilot": "secrets-engine.task-execution.service-pilot v1 literal runtime_admission := false; normal result service_execution_integration_unadmitted", + "human_variant": "Existing separate human task package remains unchanged and unadmitted", + "local_positive_evidence": "Independent local stack used a separately named synthetic policy. It does not establish service-pilot runtime permission.", + "packaging_caution": "Engine loads one policy package per process. Swapping existing lifecycle pin to isolated task-only package would remove legacy actions. No such swap is authorized. Later packaging choice must preserve existing lifecycle behavior or use owner-reviewed isolated current-pilot deployment; do not quietly broaden or combine packages." + }, + "minimal_next_changes": [ + { + "stage": "source-only preparation available now", + "owner": "Access Engine", + "change": "Publish authoritative signer workload identity/reference through existing estate declaration; define exact native tests and explicit retained-runtime audience; attach custody lane dependency to current coordination driver. No credential required." + }, + { + "stage": "signer-only native trial after missing inputs and exact admission", + "owner": "Access Engine + Platform + Secrets", + "change": "CI-built exact ec21518e successor image on same retained flex-auth-secrets-engine release; activate existing envelopeSigning block with admitted kid/Secret/key read-only mount. Keep lifecycle package, caller-auth enforce, exact binding and task DENY. Install reviewed consumer bundle and independent trust. Pin source/image/helper/trust digests and use owner-bound literal loopback tunnel." + }, + { + "stage": "later bounded service execution source successor", + "owner": "Access Engine with Approval/Secrets/Platform owner reviews", + "change": "Wire actual authenticated service input and independently authenticated provider joins into existing decision route, characterize real negative cases, and replace structural false gate only with a reviewed complete eligibility path. No enable flag or request boolean. Current pilot false gate remains until this separate implementation and admission completes." + } + ], + "required_tests_before_native_acceptance": [ + "CI source-to-OCI digest provenance; rendered manifest/image/args/policy unchanged apart from explicit signer and candidate image; CPU headroom, exact client diff/server dry-run under owner deployment path before any apply.", + "Native reviewer CA/audience/RBAC provenance and projected token rotation; correct service passes, foreign/missing/expired token, wrong resource.system/subject/type/tenant, unknown transport expiry, public binding spoof/extra fields and deadline beyond transport expiry refuse.", + "Actual admitted producer returns signed allow for existing allowed lifecycle action and signed deny for forbidden action; exact consumer key/policy pins, request digest and lifetime validation; forged/altered/wrong-kid/unsigned/wrong-policy/expired refuse.", + "Current service-pilot still returns signed DENY before consume/effect; caller flags cannot flip it. Legacy lifecycle actions unchanged; no approval consumed in mere signer probe.", + "Returned signed decision equals retained original source record; require native durable recording/custody path explicitly (live deployment currently has no --log/outbox flags). Local unit/local JSONL parity is not native independent audit custody.", + "Admit successor public key first; prove successor verification; roll producer seed/kid; keep old public key only through maximum outstanding allow TTL plus bounded uncertainty; remove and prove old signature refusal. Emergency revoked keys remove immediately.", + "Exact installed Secrets wheel/helper provenance, target architecture, private path/mode admission and second clean install; no request-time compile or bundled test signer.", + "For later task activation separately: actual served claim/verified human approver provenance, exact request/descriptor and consumption triplet, one-shot journal, partial crash/unknown recovery, six fixed backend calls, use/time ceilings/readback/revoke/cleanup with independent audit receipts." + ], + "rollback_and_abort": { + "abort_owner": "Access Engine deployment owner for PDP rollout; Platform for signer custody; Secrets owner for consumer enforcement. Named human action identity must be sealed in executable revision.", + "abort_conditions": [ + "Any missing independent public admission or candidate artifact digest", + "Headroom insufficient, source/policy drift, unexpected caller/pin acceptance, unsigned response accepted, unexpected task allow, custody or record uncertainty" + ], + "signer_rollout_failure": "Stop consumer protected operations; restore exact prior producer deployment revision only as availability/diagnostic rollback. Enforcing consumer remains fail-closed against unsigned responses; never enable unsigned fallback or reuse revoked key.", + "policy_join_failure": "Retain/restore original hard DENY current service-pilot. Any unknown protected effect requires owner reconciliation; do not auto-retry.", + "key_rotation": "Retain original failure/refinement and key-custody recovery receipts; no rollback to revoked/compromised predecessor." + }, + "missing_execution_inputs": { + "candidate_ci_image_digest": null, + "existing_custody_secret_name": null, + "secret_key_field": null, + "admitted_kid_and_public_fingerprint": null, + "public_custody_admission_receipt": null, + "consumer_exact_installed_bundle_digest": null, + "native_durable_recording_and_audit_path": null, + "exact_owner_execution_revision_approval": null, + "native_headroom_diff_dryrun_receipts": null + }, + "cost_risk": { + "preparation_incremental_spend_eur": 0, + "basis": "Read-only existing metadata and local source validation only; no paid resource provision or live mutation.", + "future_activation_cost_bound_eur": null, + "future_activation_risk_bound_eur": null, + "required_before_privilege": "Supervising owner must estimate and admit rollout/resource/custody risk for exact executable revision. Missing estimates do not mean free or authorized." + }, + "work_record_correction": { + "existing_task": "FLEX-WP-0024-T06", + "recommended_status": "progress", + "reason": "Source owner still has actionable preparation, declaration and packaging work. A broad unqualified wait on custody plus consumer acceptance hides owner work and risks a dependency cycle with consumer T03. Native execution is genuinely blocked by draft custody route and missing public admission; record those as explicit stage gates without inventing a Platform task or approval.", + "coordination": "CUST-WP-0080 already coordinates Access, Secrets, Platform and Approval. Root should attach/retain named custody commitment through the existing project driver; no new workplan, worker wake or foreign task patch here." + } +} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/source-policy-pins.json b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/source-policy-pins.json new file mode 100644 index 00000000..5e7e89ae --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/source-policy-pins.json @@ -0,0 +1 @@ +{"examples/secrets-engine-service-task-execution/policy_package.md":{"digest":"sha256:b034dc8ad208889113ce475d33df1589f5b84cb817bbb361d10638e23e394a6e","id":"secrets-engine.task-execution.service-pilot","valid":true,"version":"v1"},"examples/secrets-engine/policy_package.md":{"digest":"sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4","id":"secrets-engine.catalog-lane.lifecycle","valid":true,"version":"v2"}} diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/workplan-native-preparation-r1.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/workplan-native-preparation-r1.patch new file mode 100644 index 00000000..220f9b14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/workplan-native-preparation-r1.patch @@ -0,0 +1,36 @@ +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 +@@ -184,7 +184,7 @@ + + ```task + id: FLEX-WP-0024-T06 +-status: wait ++status: progress + priority: high + ``` + +@@ -197,3 +197,23 @@ + source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns + consumer admission/enforcement. The old fixture completion never established this + gate, so the workplan is reopened rather than claiming it already passed. ++ ++ ++2026-10-16 owner native preparation: T06 is `progress` because Access still owns ++signer workload declaration, artifact/provenance and source/provider-join preparation; ++no unqualified cross-owner wait is claimed. Read-only actual release metadata and ++bounded proposal are in the Custodian ++`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`. ++The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but ++is draft/nonresolvable; no production kid/public fingerprint, Secret projection or ++custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080 ++instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed ++consumer native verification and must not form a circular blocking dependency. ++ ++The first proposed native trial only signs the unchanged lifecycle package on the ++existing retained release; `execute-ccr` remains denied. Current service-pilot ++`runtime_admission := false` remains mandatory: the source ServicePilotInputs is ++not yet wired into HTTP, and public service_actor_binding never authenticates the ++caller. Later activation needs a complete reviewed provider join, exact policy and ++operation admission, artifact digests, owner execution/rollback proposal and native ++receipts. Preparation/local synthetic success grants none of those permissions. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-over-native-prep-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-over-native-prep-r2.patch new file mode 100644 index 00000000..2991395c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-over-native-prep-r2.patch @@ -0,0 +1,766 @@ +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 +@@ -22,6 +22,7 @@ + "github.com/netkingdom/access-engine/internal/emission" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/taskexecution" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -397,7 +398,11 @@ + return fail(stderr, err) + } + +- mux := newServeMuxWithCallerAuth(engine, authenticator) ++ serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap()) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -448,6 +453,10 @@ + } + + func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux { ++ return newServeMuxWithServicePilotInputs(engine, authenticator, nil) ++} ++ ++func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -458,6 +467,25 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } ++ if engine.IsServicePilot() { ++ if serviceInputs == nil { ++ http.Error(w, "service input unavailable", http.StatusServiceUnavailable) ++ return ++ } ++ request, err := taskexecution.DecodeServiceRequest(r.Body) ++ if err != nil || request.Purpose != "" { ++ http.Error(w, "invalid service task request", http.StatusBadRequest) ++ return ++ } ++ input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC()) ++ if err != nil { ++ http.Error(w, "service task input refused", http.StatusForbidden) ++ return ++ } ++ decision, err := engine.CheckServicePilot(r.Context(), input) ++ writeHTTP(w, decision, err) ++ return ++ } + var request api.CheckRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) +@@ -478,6 +506,10 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { ++ if engine.IsServicePilot() { ++ http.Error(w, "service task batch unavailable", http.StatusForbidden) ++ return ++ } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return +@@ -500,6 +532,22 @@ + writeHTTP(w, decisions, err) + }) + return mux ++} ++ ++// 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() { ++ return nil, nil ++ } ++ if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { ++ return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping") ++ } ++ reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) ++ if err != nil { ++ return nil, err ++ } ++ return taskexecution.NewServicePilotInputs(reviewer) + } + + func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) { +@@ -615,7 +663,7 @@ + + func writeHTTP(w http.ResponseWriter, value any, err error) { + w.Header().Set("content-type", "application/json") +- if errors.Is(err, decisioncore.ErrRecordNotCommitted) { ++ if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) { + // Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024). + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return +diff --git a/cmd/access-engine/service_pilot_http_test.go b/cmd/access-engine/service_pilot_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/service_pilot_http_test.go +@@ -0,0 +1,376 @@ ++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" ++ "sync/atomic" ++ "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" ++) ++ ++func pilotRequest(now time.Time) api.CheckRequest { ++ return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}} ++} ++func pilotEngine(t *testing.T) *decision.Engine { ++ t.Helper() ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ return engine ++} ++func pilotToken(kind string, now time.Time) string { ++ payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()} ++ if kind == "expired" { ++ payload["exp"] = now.Add(-time.Second).Unix() ++ } ++ if kind == "no-expiry" { ++ delete(payload, "exp") ++ } ++ raw, _ := json.Marshal(payload) ++ return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token" ++} ++func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) { ++ now := time.Now().UTC() ++ var reviews atomic.Int64 ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reviews.Add(1) ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ t.Error("unexpected reviewer route/auth") ++ w.WriteHeader(403) ++ return ++ } ++ var body struct { ++ Spec struct { ++ Token string `json:"token"` ++ Audiences []string `json:"audiences"` ++ } `json:"spec"` ++ } ++ if json.NewDecoder(r.Body).Decode(&body) != nil { ++ w.WriteHeader(400) ++ return ++ } ++ if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) { ++ t.Error("unexpected bounded runtime audience request") ++ } ++ pieces := strings.Split(body.Spec.Token, ".") ++ raw, _ := base64.RawURLEncoding.DecodeString(pieces[1]) ++ var claims map[string]any ++ json.Unmarshal(raw, &claims) ++ principal := taskexecution.ServicePilotPrincipal ++ audiences := []string{"flex-auth"} ++ authenticated := true ++ switch claims["case"] { ++ case "foreign": ++ principal = "system:serviceaccount:other:other" ++ case "alternate-audience": ++ audiences = []string{"access-engine"} ++ case "wrong-audience": ++ audiences = []string{"openbao"} ++ case "unauthenticated": ++ authenticated = false ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}}) ++ })) ++ defer reviewer.Close() ++ folder := t.TempDir() ++ ca := filepath.Join(folder, "ca.crt") ++ reviewToken := filepath.Join(folder, "reviewer-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600) ++ engine := pilotEngine(t) ++ private := ed25519.NewKeyFromSeed(make([]byte, 32)) ++ if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil { ++ t.Fatal("signer config") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl")) ++ engine.SetDecisionLog(log) ++ bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal} ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs)) ++ defer server.Close() ++ post := func(path string, request api.CheckRequest, token string) (int, []byte) { ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw)) ++ if token != "" { ++ req.Header.Set("Authorization", "Bearer "+token) ++ } ++ 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 ++ } ++ original := pilotRequest(now) ++ status, body := post("/v1/check", original, pilotToken("valid", now)) ++ if status != 200 { ++ t.Fatalf("status=%d", status) ++ } ++ var envelope api.DecisionEnvelope ++ if json.Unmarshal(body, &envelope) != nil { ++ t.Fatal("decode") ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" { ++ t.Fatal("B1 changed policy gate") ++ } ++ if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("actual response signature") ++ } ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original submitted request/context changed") ++ } ++ if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" { ++ t.Fatal("caller provenance") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) { ++ t.Fatal("recorded signed response differs") ++ } ++ if reviews.Load() != 1 { ++ t.Fatal("request was reviewed more than once") ++ } ++ for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} { ++ t.Run(kind, func(t *testing.T) { ++ status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now)) ++ if status == 200 { ++ t.Fatal("invalid source caller accepted") ++ } ++ }) ++ } ++ for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) { ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano) ++ }} { ++ request := pilotRequest(now) ++ mutate(&request) ++ if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 { ++ t.Fatal("invalid public request accepted") ++ } ++ } ++ if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 { ++ t.Fatal("missing caller accepted") ++ } ++ if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 { ++ t.Fatal("batch bypass") ++ } ++ records, _ = log.ReadAll() ++ if len(records) != 1 { ++ t.Fatal("refused request entered decision record") ++ } ++ ++ // Exercise the actual production serve flag/configuration path in a disposable ++ // subprocess of this compiled test binary. Its environment contains only the ++ // synthetic argument handoff; no ambient backend credentials are inherited. ++ seedFile := filepath.Join(folder, "synthetic-seed") ++ os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440) ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-records.jsonl") ++ cliArgs := []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", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"} ++ argsJSON, _ := json.Marshal(cliArgs) ++ 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 child.Start() != nil { ++ t.Fatal("start disposable CLI") ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ resp, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ resp.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("disposable production serve path not ready") ++ } ++ raw, _ := json.Marshal(original) ++ cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ cliResponse, err := http.DefaultClient.Do(cliRequest) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer cliResponse.Body.Close() ++ var cliEnvelope api.DecisionEnvelope ++ if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil { ++ t.Fatal("production serve path refused supported source input") ++ } ++ if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("production CLI did not emit signed DENY") ++ } ++ if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("production CLI changed original request/context") ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("production CLI response/record differ") ++ } ++ for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} { ++ if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("non-enforcing task mode accepted") ++ } ++ } ++ if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("task pin renamed audience accepted") ++ } ++ missing := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body)) ++ newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req) ++ if missing.Code != 503 { ++ t.Fatal("unwired generic handler accepted task pin") ++ } ++} ++func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) { ++ engine := pilotEngine(t) ++ request := pilotRequest(time.Now()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic Check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil { ++ t.Fatal("generic Batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil { ++ t.Fatal("generic List bypass") ++ } ++ if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil { ++ t.Fatal("zero input trusted") ++ } ++ folder := t.TempDir() ++ path := filepath.Join(folder, "request.json") ++ raw, _ := json.Marshal(request) ++ os.WriteFile(path, raw, 0600) ++ var stdout, stderr bytes.Buffer ++ code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr) ++ if code == 0 || stdout.Len() != 0 { ++ t.Fatal("generic CLI emitted task verdict") ++ } ++} ++ ++// The earlier separately named positive fixture policy is not an exception: ++// generic task actions cannot bypass the typed path on any loaded package. ++func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ p.Metadata.ID = "synthetic-legacy-stack-positive-only" ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ request := pilotRequest(time.Now()) ++ result, err := p.Evaluate(context.Background(), request) ++ if err != nil || result.Effect != api.DecisionEffectAllow { ++ t.Fatal("synthetic precondition did not allow") ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("separately named generic task allow escaped") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task batch escaped") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task list escaped") ++ } ++ raw, _ := json.Marshal(request) ++ recorder := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw)) ++ newServeMux(engine).ServeHTTP(recorder, req) ++ if recorder.Code != 503 { ++ t.Fatal("separate synthetic generic HTTP task route did not refuse") ++ } ++} ++ ++func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) { ++ now := time.Now().UTC() ++ adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ for _, accidentalAllow := range []bool{false, true} { ++ t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl")) ++ engine.SetDecisionLog(log) ++ if _, err := engine.CheckServicePilot(context.Background(), input); err == nil { ++ t.Fatal("B1 released unsupported result") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 0 { ++ t.Fatal("withheld result entered recorder") ++ } ++ }) ++ } ++} ++ ++func TestServicePilotChildCLI(t *testing.T) { ++ raw := os.Getenv("ACCESS_B1_SERVER_ARGS") ++ if raw == "" { ++ return ++ } ++ var args []string ++ if json.Unmarshal([]byte(raw), &args) != nil { ++ os.Exit(2) ++ } ++ os.Exit(run(args, os.Stdout, os.Stderr)) ++} +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -20,6 +20,7 @@ + "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" + ) + +@@ -153,9 +154,44 @@ + return envelope, nil + } + ++// IsServicePilot selects the existing exact source package, never a request flag. ++func (e *Engine) IsServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion ++} ++ ++// ErrServicePilotUnavailable withholds any result that is not this B1 slice's ++// signed DENY. Provider joins and executable eligibility remain unimplemented. ++var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable") ++ ++// CheckServicePilot records only a signed DENY after authenticating the exact ++// immutable service input. It does not validate Approval facts or grant an act. ++func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) { ++ if !e.IsServicePilot() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ request, err := input.RequestAt(e.now()) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ + // 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" { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } + decision, err := e.decide(ctx, request) + if err != nil { + return api.DecisionEnvelope{}, err +@@ -169,6 +205,9 @@ + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) + for _, resource := range request.Resources { + decision, err := e.decide(ctx, api.CheckRequest{ +@@ -193,6 +232,9 @@ + + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) + decisions := make([]api.DecisionEnvelope, 0, len(candidates)) + for _, resource := range candidates { +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 +@@ -1,20 +1,27 @@ + package taskexecution + + import ( ++ "bytes" + "context" + "encoding/json" + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "io" + "time" + ) + + var ErrServiceInput = errors.New("service task execution input refused") + ++const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" ++const ServicePilotPackageVersion = "v1" ++ + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" + +-// ServicePilotInputs is source preparation only. It installs the explicit ++// ServicePilotInputs installs the exact B1 service input mapping for signed DENY. ++// It does not validate Approval facts or establish executable eligibility. ++// 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 } +@@ -46,6 +53,44 @@ + return r, nil + } + func (s ServiceInput) Caller() callerauth.Record { return s.caller } ++ ++// RequestAt rechecks transport and operation lifetime at the runtime use site. ++// Request() remains a detached source snapshot, never an execution capability. ++func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) { ++ r, err := s.Request() ++ if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ contract, ok := r.Context["execution_contract"].(map[string]any) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ value, ok := contract["operation_deadline"].(string) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ deadline, err := time.Parse(time.RFC3339Nano, value) ++ if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return r, nil ++} ++ ++// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded ++// task-only route. It never changes producer canonicalization or legacy input. ++func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ var request api.CheckRequest ++ if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil { ++ return request, ErrServiceInput ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} + + // DeclaredServiceBinding is public immutable metadata to bind at approval issue. + // It is not authentication evidence; Prepare independently verifies its principal. +diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go +--- a/internal/taskexecution/service_input_test.go ++++ b/internal/taskexecution/service_input_test.go +@@ -1,7 +1,9 @@ + package taskexecution + + import ( ++ "bytes" + "context" ++ "encoding/json" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "testing" +@@ -79,3 +81,35 @@ + t.Fatal("missing owner reviewer accepted") + } + } ++ ++func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) { ++ now := time.Now().UTC() ++ r := serviceRequest(t, now) ++ reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} ++ adapter, _ := NewServicePilotInputs(reviewer) ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := input.RequestAt(now); err != nil { ++ t.Fatal(err) ++ } ++ for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} { ++ if _, err := input.RequestAt(when); err == nil { ++ t.Fatal("stale/invalid runtime input accepted") ++ } ++ } ++ raw, _ := json.Marshal(r) ++ decoded, err := DecodeServiceRequest(bytes.NewReader(raw)) ++ if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) { ++ t.Fatal("valid decode changed canonical submitted input") ++ } ++ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context" ++ "bytes" ++ "encoding/json":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} ++ for _, body := range bad { ++ if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil { ++ t.Fatal("malformed/oversized task request accepted") ++ } ++ } ++} +diff --git a/internal/taskexecution/identity.go b/internal/taskexecution/identity.go +--- a/internal/taskexecution/identity.go ++++ b/internal/taskexecution/identity.go +@@ -1,5 +1,6 @@ +-// Package taskexecution contains proposed source-only identity input validation. +-// It is not registered with HTTP evaluation and grants no executable admission. ++// Package taskexecution contains task input validation, not policy evaluation. ++// The human component remains source-only. The B1 service HTTP path can emit ++// only signed DENY and grants no executable admission. + package taskexecution + + import ( +diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md +--- a/docs/service-task-execution-contract.md ++++ b/docs/service-task-execution-contract.md +@@ -51,9 +51,32 @@ + + Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication. + Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping +-admission must be proved before runtime use. The adapter is not wired into HTTP +-routes, decision engine evaluation or a native executor; source package remains +-hard-closed. A forged public service_actor_binding alone remains insufficient. ++admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only ++when the exact installed service-pilot v1 package is selected. Startup requires ++enforced retained `flex-auth` audience and the one exact Secrets mapping. The ++actual reviewer authenticates once; the detached ServiceInput is rechecked for ++caller/operation expiry at use. The single existing decision engine records only ++a signed DENY on this path; unsigned output or an accidental policy allow is ++withheld before recording. No native executor or Approval fact join is installed. ++A forged public service_actor_binding alone remains insufficient. ++ ++Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate ++`execute-ccr` under any package; the dedicated typed path is required. The exact ++service pin also refuses generic calls and batch requests. This deliberately ++invalidates the older separately named synthetic positive stack fixture on the ++new source. Its positive proof remains historical at Access `ec21518e`, not proof ++for B1. Do not add a fixture exception; a positive successor must use reviewed ++B2/B3 provider joins. The literal service policy false gate is unchanged. ++ ++B1 retains the exact original request context, carried claim, submitted digest ++and producer-owned canonicalization. Its bounded strict JSON decoder refuses ++duplicates, trailing/unknown typed fields and oversized input. The caller's ++Approval blob remains unverified and cannot enable anything. B2 requires the ++Approval owner's fact contract/vector review; B3 additionally requires exact ++server-owned native-act representation and the complete trusted join. Fresh ++provider observations stay out of band: no refreshed context or claim replacement ++may relax strict Secrets request correspondence. Access never invokes a ++credential-carrying Python subprocess, consumes approval or performs the effect. + + ## Exact operation and independent gates + +@@ -129,7 +152,8 @@ + admit grant/baseline/estate or perform an effect. Those are explicit implementation + requirements; a matching predicate and reviewed TokenReview input are insufficient. + The service adapter carries authenticated transport evidence outside the canonical +-request but is not connected to HTTP evaluation/provider validation. A caller's ++request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and ++executable eligibility remain absent. A caller's + service_actor_binding remains a public declaration independently compared by the + adapter, never a substitute for runtime provenance or exact operation authority. + +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 +@@ -226,3 +226,14 @@ + Secrets verifier. No key, trust, image publication or live activation is admitted. + The managed workload's missing rapp declaration remains explicit migration debt; + this source binding does not fabricate a package declaration or raise its grade. ++ ++ ++2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual ++HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls ++refuse across every policy, including the old separately named positive fixture; ++that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged. ++Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are ++source evidence only. B2 depends on independently reviewed Approval fact contract ++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. diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r1-to-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r1-to-r2.patch new file mode 100644 index 00000000..574e7e46 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r1-to-r2.patch @@ -0,0 +1,14 @@ +diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go +--- a/internal/taskexecution/service_input_test.go ++++ b/internal/taskexecution/service_input_test.go +@@ -104,9 +104,7 @@ + if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) { + t.Fatal("valid decode changed canonical submitted input") + } +- bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context" +- "bytes" +- "encoding/json":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} ++ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} + for _, body := range bad { + if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil { + t.Fatal("malformed/oversized task request accepted") diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r2-over-native-prep-r2.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r2-over-native-prep-r2.patch new file mode 100644 index 00000000..6fd6ced0 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/b1-r2-over-native-prep-r2.patch @@ -0,0 +1,764 @@ +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 +@@ -226,3 +226,14 @@ + Secrets verifier. No key, trust, image publication or live activation is admitted. + The managed workload's missing rapp declaration remains explicit migration debt; + this source binding does not fabricate a package declaration or raise its grade. ++ ++ ++2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual ++HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls ++refuse across every policy, including the old separately named positive fixture; ++that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged. ++Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are ++source evidence only. B2 depends on independently reviewed Approval fact contract ++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. +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 +@@ -22,6 +22,7 @@ + "github.com/netkingdom/access-engine/internal/emission" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/taskexecution" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -397,7 +398,11 @@ + return fail(stderr, err) + } + +- mux := newServeMuxWithCallerAuth(engine, authenticator) ++ serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap()) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -448,6 +453,10 @@ + } + + func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux { ++ return newServeMuxWithServicePilotInputs(engine, authenticator, nil) ++} ++ ++func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -458,6 +467,25 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } ++ if engine.IsServicePilot() { ++ if serviceInputs == nil { ++ http.Error(w, "service input unavailable", http.StatusServiceUnavailable) ++ return ++ } ++ request, err := taskexecution.DecodeServiceRequest(r.Body) ++ if err != nil || request.Purpose != "" { ++ http.Error(w, "invalid service task request", http.StatusBadRequest) ++ return ++ } ++ input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC()) ++ if err != nil { ++ http.Error(w, "service task input refused", http.StatusForbidden) ++ return ++ } ++ decision, err := engine.CheckServicePilot(r.Context(), input) ++ writeHTTP(w, decision, err) ++ return ++ } + var request api.CheckRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) +@@ -478,6 +506,10 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { ++ if engine.IsServicePilot() { ++ http.Error(w, "service task batch unavailable", http.StatusForbidden) ++ return ++ } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return +@@ -500,6 +532,22 @@ + writeHTTP(w, decisions, err) + }) + return mux ++} ++ ++// 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() { ++ return nil, nil ++ } ++ if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { ++ return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping") ++ } ++ reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) ++ if err != nil { ++ return nil, err ++ } ++ return taskexecution.NewServicePilotInputs(reviewer) + } + + func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) { +@@ -615,7 +663,7 @@ + + func writeHTTP(w http.ResponseWriter, value any, err error) { + w.Header().Set("content-type", "application/json") +- if errors.Is(err, decisioncore.ErrRecordNotCommitted) { ++ if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) { + // Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024). + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return +diff --git a/cmd/access-engine/service_pilot_http_test.go b/cmd/access-engine/service_pilot_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/service_pilot_http_test.go +@@ -0,0 +1,376 @@ ++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" ++ "sync/atomic" ++ "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" ++) ++ ++func pilotRequest(now time.Time) api.CheckRequest { ++ return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}} ++} ++func pilotEngine(t *testing.T) *decision.Engine { ++ t.Helper() ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ return engine ++} ++func pilotToken(kind string, now time.Time) string { ++ payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()} ++ if kind == "expired" { ++ payload["exp"] = now.Add(-time.Second).Unix() ++ } ++ if kind == "no-expiry" { ++ delete(payload, "exp") ++ } ++ raw, _ := json.Marshal(payload) ++ return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token" ++} ++func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) { ++ now := time.Now().UTC() ++ var reviews atomic.Int64 ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reviews.Add(1) ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ t.Error("unexpected reviewer route/auth") ++ w.WriteHeader(403) ++ return ++ } ++ var body struct { ++ Spec struct { ++ Token string `json:"token"` ++ Audiences []string `json:"audiences"` ++ } `json:"spec"` ++ } ++ if json.NewDecoder(r.Body).Decode(&body) != nil { ++ w.WriteHeader(400) ++ return ++ } ++ if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) { ++ t.Error("unexpected bounded runtime audience request") ++ } ++ pieces := strings.Split(body.Spec.Token, ".") ++ raw, _ := base64.RawURLEncoding.DecodeString(pieces[1]) ++ var claims map[string]any ++ json.Unmarshal(raw, &claims) ++ principal := taskexecution.ServicePilotPrincipal ++ audiences := []string{"flex-auth"} ++ authenticated := true ++ switch claims["case"] { ++ case "foreign": ++ principal = "system:serviceaccount:other:other" ++ case "alternate-audience": ++ audiences = []string{"access-engine"} ++ case "wrong-audience": ++ audiences = []string{"openbao"} ++ case "unauthenticated": ++ authenticated = false ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}}) ++ })) ++ defer reviewer.Close() ++ folder := t.TempDir() ++ ca := filepath.Join(folder, "ca.crt") ++ reviewToken := filepath.Join(folder, "reviewer-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600) ++ engine := pilotEngine(t) ++ private := ed25519.NewKeyFromSeed(make([]byte, 32)) ++ if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil { ++ t.Fatal("signer config") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl")) ++ engine.SetDecisionLog(log) ++ bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal} ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs)) ++ defer server.Close() ++ post := func(path string, request api.CheckRequest, token string) (int, []byte) { ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw)) ++ if token != "" { ++ req.Header.Set("Authorization", "Bearer "+token) ++ } ++ 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 ++ } ++ original := pilotRequest(now) ++ status, body := post("/v1/check", original, pilotToken("valid", now)) ++ if status != 200 { ++ t.Fatalf("status=%d", status) ++ } ++ var envelope api.DecisionEnvelope ++ if json.Unmarshal(body, &envelope) != nil { ++ t.Fatal("decode") ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" { ++ t.Fatal("B1 changed policy gate") ++ } ++ if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("actual response signature") ++ } ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original submitted request/context changed") ++ } ++ if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" { ++ t.Fatal("caller provenance") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) { ++ t.Fatal("recorded signed response differs") ++ } ++ if reviews.Load() != 1 { ++ t.Fatal("request was reviewed more than once") ++ } ++ for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} { ++ t.Run(kind, func(t *testing.T) { ++ status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now)) ++ if status == 200 { ++ t.Fatal("invalid source caller accepted") ++ } ++ }) ++ } ++ for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) { ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano) ++ }} { ++ request := pilotRequest(now) ++ mutate(&request) ++ if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 { ++ t.Fatal("invalid public request accepted") ++ } ++ } ++ if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 { ++ t.Fatal("missing caller accepted") ++ } ++ if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 { ++ t.Fatal("batch bypass") ++ } ++ records, _ = log.ReadAll() ++ if len(records) != 1 { ++ t.Fatal("refused request entered decision record") ++ } ++ ++ // Exercise the actual production serve flag/configuration path in a disposable ++ // subprocess of this compiled test binary. Its environment contains only the ++ // synthetic argument handoff; no ambient backend credentials are inherited. ++ seedFile := filepath.Join(folder, "synthetic-seed") ++ os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440) ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-records.jsonl") ++ cliArgs := []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", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"} ++ argsJSON, _ := json.Marshal(cliArgs) ++ 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 child.Start() != nil { ++ t.Fatal("start disposable CLI") ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ resp, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ resp.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("disposable production serve path not ready") ++ } ++ raw, _ := json.Marshal(original) ++ cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ cliResponse, err := http.DefaultClient.Do(cliRequest) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer cliResponse.Body.Close() ++ var cliEnvelope api.DecisionEnvelope ++ if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil { ++ t.Fatal("production serve path refused supported source input") ++ } ++ if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("production CLI did not emit signed DENY") ++ } ++ if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("production CLI changed original request/context") ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("production CLI response/record differ") ++ } ++ for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} { ++ if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("non-enforcing task mode accepted") ++ } ++ } ++ if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("task pin renamed audience accepted") ++ } ++ missing := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body)) ++ newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req) ++ if missing.Code != 503 { ++ t.Fatal("unwired generic handler accepted task pin") ++ } ++} ++func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) { ++ engine := pilotEngine(t) ++ request := pilotRequest(time.Now()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic Check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil { ++ t.Fatal("generic Batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil { ++ t.Fatal("generic List bypass") ++ } ++ if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil { ++ t.Fatal("zero input trusted") ++ } ++ folder := t.TempDir() ++ path := filepath.Join(folder, "request.json") ++ raw, _ := json.Marshal(request) ++ os.WriteFile(path, raw, 0600) ++ var stdout, stderr bytes.Buffer ++ code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr) ++ if code == 0 || stdout.Len() != 0 { ++ t.Fatal("generic CLI emitted task verdict") ++ } ++} ++ ++// The earlier separately named positive fixture policy is not an exception: ++// generic task actions cannot bypass the typed path on any loaded package. ++func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ p.Metadata.ID = "synthetic-legacy-stack-positive-only" ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ request := pilotRequest(time.Now()) ++ result, err := p.Evaluate(context.Background(), request) ++ if err != nil || result.Effect != api.DecisionEffectAllow { ++ t.Fatal("synthetic precondition did not allow") ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("separately named generic task allow escaped") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task batch escaped") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task list escaped") ++ } ++ raw, _ := json.Marshal(request) ++ recorder := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw)) ++ newServeMux(engine).ServeHTTP(recorder, req) ++ if recorder.Code != 503 { ++ t.Fatal("separate synthetic generic HTTP task route did not refuse") ++ } ++} ++ ++func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) { ++ now := time.Now().UTC() ++ adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ for _, accidentalAllow := range []bool{false, true} { ++ t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl")) ++ engine.SetDecisionLog(log) ++ if _, err := engine.CheckServicePilot(context.Background(), input); err == nil { ++ t.Fatal("B1 released unsupported result") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 0 { ++ t.Fatal("withheld result entered recorder") ++ } ++ }) ++ } ++} ++ ++func TestServicePilotChildCLI(t *testing.T) { ++ raw := os.Getenv("ACCESS_B1_SERVER_ARGS") ++ if raw == "" { ++ return ++ } ++ var args []string ++ if json.Unmarshal([]byte(raw), &args) != nil { ++ os.Exit(2) ++ } ++ os.Exit(run(args, os.Stdout, os.Stderr)) ++} +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -20,6 +20,7 @@ + "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" + ) + +@@ -153,9 +154,44 @@ + return envelope, nil + } + ++// IsServicePilot selects the existing exact source package, never a request flag. ++func (e *Engine) IsServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion ++} ++ ++// ErrServicePilotUnavailable withholds any result that is not this B1 slice's ++// signed DENY. Provider joins and executable eligibility remain unimplemented. ++var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable") ++ ++// CheckServicePilot records only a signed DENY after authenticating the exact ++// immutable service input. It does not validate Approval facts or grant an act. ++func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) { ++ if !e.IsServicePilot() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ request, err := input.RequestAt(e.now()) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ + // 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" { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } + decision, err := e.decide(ctx, request) + if err != nil { + return api.DecisionEnvelope{}, err +@@ -169,6 +205,9 @@ + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) + for _, resource := range request.Resources { + decision, err := e.decide(ctx, api.CheckRequest{ +@@ -193,6 +232,9 @@ + + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) + decisions := make([]api.DecisionEnvelope, 0, len(candidates)) + for _, resource := range candidates { +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 +@@ -1,20 +1,27 @@ + package taskexecution + + import ( ++ "bytes" + "context" + "encoding/json" + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "io" + "time" + ) + + var ErrServiceInput = errors.New("service task execution input refused") + ++const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" ++const ServicePilotPackageVersion = "v1" ++ + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" + +-// ServicePilotInputs is source preparation only. It installs the explicit ++// ServicePilotInputs installs the exact B1 service input mapping for signed DENY. ++// It does not validate Approval facts or establish executable eligibility. ++// 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 } +@@ -46,6 +53,44 @@ + return r, nil + } + func (s ServiceInput) Caller() callerauth.Record { return s.caller } ++ ++// RequestAt rechecks transport and operation lifetime at the runtime use site. ++// Request() remains a detached source snapshot, never an execution capability. ++func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) { ++ r, err := s.Request() ++ if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ contract, ok := r.Context["execution_contract"].(map[string]any) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ value, ok := contract["operation_deadline"].(string) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ deadline, err := time.Parse(time.RFC3339Nano, value) ++ if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return r, nil ++} ++ ++// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded ++// task-only route. It never changes producer canonicalization or legacy input. ++func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ var request api.CheckRequest ++ if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil { ++ return request, ErrServiceInput ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} + + // DeclaredServiceBinding is public immutable metadata to bind at approval issue. + // It is not authentication evidence; Prepare independently verifies its principal. +diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go +--- a/internal/taskexecution/service_input_test.go ++++ b/internal/taskexecution/service_input_test.go +@@ -1,7 +1,9 @@ + package taskexecution + + import ( ++ "bytes" + "context" ++ "encoding/json" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "testing" +@@ -79,3 +81,33 @@ + t.Fatal("missing owner reviewer accepted") + } + } ++ ++func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) { ++ now := time.Now().UTC() ++ r := serviceRequest(t, now) ++ reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} ++ adapter, _ := NewServicePilotInputs(reviewer) ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := input.RequestAt(now); err != nil { ++ t.Fatal(err) ++ } ++ for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} { ++ if _, err := input.RequestAt(when); err == nil { ++ t.Fatal("stale/invalid runtime input accepted") ++ } ++ } ++ raw, _ := json.Marshal(r) ++ decoded, err := DecodeServiceRequest(bytes.NewReader(raw)) ++ if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) { ++ t.Fatal("valid decode changed canonical submitted input") ++ } ++ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} ++ for _, body := range bad { ++ if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil { ++ t.Fatal("malformed/oversized task request accepted") ++ } ++ } ++} +diff --git a/internal/taskexecution/identity.go b/internal/taskexecution/identity.go +--- a/internal/taskexecution/identity.go ++++ b/internal/taskexecution/identity.go +@@ -1,5 +1,6 @@ +-// Package taskexecution contains proposed source-only identity input validation. +-// It is not registered with HTTP evaluation and grants no executable admission. ++// Package taskexecution contains task input validation, not policy evaluation. ++// The human component remains source-only. The B1 service HTTP path can emit ++// only signed DENY and grants no executable admission. + package taskexecution + + import ( +diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md +--- a/docs/service-task-execution-contract.md ++++ b/docs/service-task-execution-contract.md +@@ -51,9 +51,32 @@ + + Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication. + Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping +-admission must be proved before runtime use. The adapter is not wired into HTTP +-routes, decision engine evaluation or a native executor; source package remains +-hard-closed. A forged public service_actor_binding alone remains insufficient. ++admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only ++when the exact installed service-pilot v1 package is selected. Startup requires ++enforced retained `flex-auth` audience and the one exact Secrets mapping. The ++actual reviewer authenticates once; the detached ServiceInput is rechecked for ++caller/operation expiry at use. The single existing decision engine records only ++a signed DENY on this path; unsigned output or an accidental policy allow is ++withheld before recording. No native executor or Approval fact join is installed. ++A forged public service_actor_binding alone remains insufficient. ++ ++Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate ++`execute-ccr` under any package; the dedicated typed path is required. The exact ++service pin also refuses generic calls and batch requests. This deliberately ++invalidates the older separately named synthetic positive stack fixture on the ++new source. Its positive proof remains historical at Access `ec21518e`, not proof ++for B1. Do not add a fixture exception; a positive successor must use reviewed ++B2/B3 provider joins. The literal service policy false gate is unchanged. ++ ++B1 retains the exact original request context, carried claim, submitted digest ++and producer-owned canonicalization. Its bounded strict JSON decoder refuses ++duplicates, trailing/unknown typed fields and oversized input. The caller's ++Approval blob remains unverified and cannot enable anything. B2 requires the ++Approval owner's fact contract/vector review; B3 additionally requires exact ++server-owned native-act representation and the complete trusted join. Fresh ++provider observations stay out of band: no refreshed context or claim replacement ++may relax strict Secrets request correspondence. Access never invokes a ++credential-carrying Python subprocess, consumes approval or performs the effect. + + ## Exact operation and independent gates + +@@ -129,7 +152,8 @@ + admit grant/baseline/estate or perform an effect. Those are explicit implementation + requirements; a matching predicate and reviewed TokenReview input are insufficient. + The service adapter carries authenticated transport evidence outside the canonical +-request but is not connected to HTTP evaluation/provider validation. A caller's ++request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and ++executable eligibility remain absent. A caller's + service_actor_binding remains a public declaration independently compared by the + adapter, never a substitute for runtime provenance or exact operation authority. + diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-full.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-full.patch new file mode 100644 index 00000000..5152a351 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-full.patch @@ -0,0 +1,1201 @@ +diff --git a/tenancy.yaml b/tenancy.yaml +--- a/tenancy.yaml ++++ b/tenancy.yaml +@@ -11,8 +11,16 @@ + workload_identity: + name: flex-auth + kind: platform-service +- responsible_repo: flex-auth ++ responsible_repo: access-engine + identity_bindings: ++ # Existing retained Secrets PDP pin; identity source, not key admission. ++ - scheme: kubernetes-service-account ++ authority: railiance01 ++ subject: system:serviceaccount:flex-auth:flex-auth-secrets-engine ++ principal_type: service ++ environment: prod ++ evidence: ++ - docs/native-signer-workload.json + - scheme: kubernetes-service-account + authority: railiance01 + subject: system:serviceaccount:flex-auth:flex-auth-ops-warden +diff --git a/values/secrets-engine.yaml b/values/secrets-engine.yaml +--- a/values/secrets-engine.yaml ++++ b/values/secrets-engine.yaml +@@ -13,6 +13,8 @@ + - /opt/flex-auth/examples/secrets-engine/policy_package.md + callerAuth: + mode: enforce ++ # Retained runtime audience is explicit; signer preparation does not rename it. ++ audience: flex-auth + kubernetesURL: https://10.43.0.1 + binding: secrets-engine=system:serviceaccount:secrets-engine:secrets-engine + consumer: +diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml +--- a/charts/flex-auth/templates/deployment.yaml ++++ b/charts/flex-auth/templates/deployment.yaml +@@ -83,6 +83,8 @@ + {{- if not (and .Values.readback .Values.readback.enabled) }} + {{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }} + {{- end }} ++ - --caller-audience ++ - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }} + - --caller-auth-mode + - {{ include "flex-auth.callerAuth.mode" . | quote }} + - --caller-kubernetes-url +diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml +--- a/.forgejo/workflows/image.yaml ++++ b/.forgejo/workflows/image.yaml +@@ -51,7 +51,7 @@ + SHORT="${REF:0:7}" + mkdir -p buildctx "${HOME}/bin" + wget -qO /tmp/repo.tar.gz \ +- "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" ++ "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz" + tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1 + wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ + | tar xz --strip-components=1 -C "${HOME}/bin" docker/docker +diff --git a/tools/verify_native_signer_receipt.py b/tools/verify_native_signer_receipt.py +new file mode 100644 +--- /dev/null ++++ b/tools/verify_native_signer_receipt.py +@@ -0,0 +1,135 @@ ++#!/usr/bin/env python3 ++"""Verify captured native decision receipts with the installed Secrets verifier. ++ ++Offline only: no token input, network request, key generation, build or effect. ++Use an independently reviewed admission file; success never admits that file. ++""" ++from __future__ import annotations ++import argparse ++import copy ++from datetime import datetime, timezone ++import hashlib ++import json ++from pathlib import Path ++import re ++ ++# These are the consumer's existing verifier/binding functions, not another PDP ++# or signature/canonicalization implementation. Install the reviewed bundle first. ++from secrets_engine.authorization import request_digest, validate_decision_envelope ++from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json ++from secrets_engine.errors import DecisionError ++ ++FIELDS = {'schema','trust_file','verifier','verifier_sha256','allow_request','allow_envelope','allow_record','deny_request','deny_envelope','deny_record','expected_caller'} ++ ++def read(path: str) -> bytes: ++ target = Path(path) ++ if not target.is_absolute(): ++ raise ValueError('absolute input path required') ++ with target.open("rb") as stream: ++ data = stream.read(524289) ++ if len(data) > 524288: ++ raise ValueError('receipt input too large') ++ return data ++ ++def same_json_types(left, right) -> bool: ++ """JSON value correspondence only; never signature canonicalization.""" ++ if type(left) is not type(right): ++ return False ++ if isinstance(left, dict): ++ return left.keys() == right.keys() and all(same_json_types(value,right[key]) for key,value in left.items()) ++ if isinstance(left, list): ++ return len(left) == len(right) and all(same_json_types(a,b) for a,b in zip(left,right)) ++ return left == right ++ ++ ++def stamp(value: str) -> datetime: ++ parsed = datetime.fromisoformat(value.replace('Z','+00:00')) ++ if parsed.tzinfo is None: ++ raise ValueError('timezone required') ++ return parsed.astimezone(timezone.utc) ++ ++def verify(config: dict, *, now: datetime | None = None) -> dict: ++ if not isinstance(config,dict) or set(config) != FIELDS or config['schema'] != 'access-engine.signer-receipt-check.v1': ++ raise ValueError('unsupported admission fields') ++ current = now or datetime.now(timezone.utc) ++ caller = config['expected_caller'] ++ if caller != {'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}: ++ raise ValueError('retained native caller admission required') ++ executable = Path(config['verifier']) ++ if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']: ++ raise ValueError('installed verifier digest mismatch') ++ trust_bytes = read(config['trust_file']) ++ trust = load_trust(Path(config['trust_file'])) ++ if not same_json_types(trust,strict_json(trust_bytes)): ++ raise ValueError('trust input changed during verification') ++ if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'): ++ raise ValueError('signer-only lifecycle package required') ++ checks = {} ++ envelopes = {} ++ inputs = {} ++ requests = {} ++ for label in ('allow','deny'): ++ blobs = {suffix:read(config[label+'_'+suffix]) for suffix in ('request','envelope','record')} ++ body = blobs['envelope'] ++ # Strict decode before helper; authenticated typed return alone is acted on. ++ strict_json(body.decode('utf-8')) ++ envelope = authenticate_response(body,trust=trust,verifier=executable) ++ request = strict_json(blobs['request']) ++ record = strict_json(blobs['record']) ++ if not same_json_types(record,envelope): ++ raise ValueError('captured original source record differs from authenticated response') ++ provenance = envelope['provenance']['caller'] ++ if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current: ++ raise ValueError('native caller provenance missing or expired') ++ if label == 'allow': ++ validate_decision_envelope(envelope,request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=current) ++ else: ++ if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request): ++ raise ValueError('deny receipt does not bind exact negative request') ++ envelopes[label] = envelope ++ requests[label] = request ++ for suffix in ('request','envelope','record'): ++ inputs[label+'_'+suffix] = hashlib.sha256(blobs[suffix]).hexdigest() ++ checks[label+'_authenticated_bound_and_record_matched'] = True ++ # These are local refusals against the captured real producer envelope. They ++ # do not claim a rejected caller was exercised on the native HTTP endpoint. ++ for label in ('altered','unsigned','unknown_kid','wrong_policy_pin'): ++ bad = copy.deepcopy(envelopes['allow']); pins = copy.deepcopy(trust) ++ if label == 'altered': bad['binding']['action'] = 'forged-action' ++ elif label == 'unsigned': bad['signature'] = {'mode':'unsigned'} ++ elif label == 'unknown_kid': bad['signature']['kid'] = 'unadmitted-key' ++ else: pins['policy_package_digest'] = 'sha256:'+'0'*64 ++ try: ++ authenticate_response(json.dumps(bad).encode(),trust=pins,verifier=executable) ++ except DecisionError: ++ checks[label+'_refused'] = True ++ else: ++ raise ValueError('negative authentication case accepted') ++ try: ++ expiry=stamp(envelopes['allow']['lifetime']['expires_at']) ++ request=requests['allow'] ++ validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry) ++ except DecisionError: ++ checks['expiry_boundary_refused'] = True ++ else: ++ raise ValueError('expired allow accepted') ++ return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(trust_bytes).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'} ++ ++def main() -> int: ++ parser=argparse.ArgumentParser(description=__doc__) ++ parser.add_argument('--admission',type=Path,required=True) ++ parser.add_argument('--output',type=Path,required=True) ++ args=parser.parse_args() ++ try: ++ config=strict_json(read(str(args.admission))) ++ result=verify(config) ++ args.output.write_text(json.dumps(result,indent=2)+'\n') ++ except (OSError,ValueError,TypeError,KeyError,RecursionError,DecisionError): ++ # Never dump source objects, unexpected key material or verifier stderr. ++ print('signer receipt verification refused') ++ return 1 ++ print('captured signer receipt checks passed') ++ return 0 ++ ++if __name__ == '__main__': ++ raise SystemExit(main()) +diff --git a/tests/test_native_signer_receipt.py b/tests/test_native_signer_receipt.py +new file mode 100644 +--- /dev/null ++++ b/tests/test_native_signer_receipt.py +@@ -0,0 +1,100 @@ ++"""Offline tool regression using the producer's existing synthetic signer only.""" ++import copy ++from datetime import datetime,timedelta,timezone ++import hashlib ++import importlib.util ++import json ++import os ++from pathlib import Path ++import subprocess ++import tempfile ++import unittest ++from unittest import mock ++ ++ROOT=Path(__file__).resolve().parents[1] ++@unittest.skipUnless(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT'), 'requires reviewed Secrets consumer source/bundle for integration checks') ++class NativeSignerReceiptTests(unittest.TestCase): ++ @classmethod ++ def setUpClass(cls): ++ global MODULE ++ spec=importlib.util.spec_from_file_location('signer_receipt',ROOT/'tools/verify_native_signer_receipt.py') ++ MODULE=importlib.util.module_from_spec(spec);spec.loader.exec_module(MODULE) ++ consumer=Path(os.environ['SECRETS_ENGINE_SOURCE_ROOT']) ++ cls.scratch=tempfile.TemporaryDirectory();cls.base=Path(cls.scratch.name) ++ env={**os.environ,'GOFLAGS':'-buildvcs=false'} ++ cls.verifier=cls.base/'verify';cls.signer=cls.base/'synthetic-sign' ++ for binary,package in [(cls.verifier,'.'),(cls.signer,'./testsupport/signer')]: ++ subprocess.run(['go','build','-p','2','-o',str(binary),package],cwd=consumer/'verifier',env=env,check=True) ++ cls.now=datetime.now(timezone.utc) ++ env=json.loads((consumer/'tests/fixtures/pdp-signature/decision_rotate_signed.json').read_text()) ++ env['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth','not_after':(cls.now+timedelta(minutes=20)).isoformat()} ++ env['lifetime']['not_before']=(cls.now-timedelta(seconds=1)).isoformat();env['lifetime']['expires_at']=(cls.now+timedelta(minutes=15)).isoformat() ++ def sign(envelope): ++ return json.loads(subprocess.check_output([str(cls.signer)],input=json.dumps(envelope).encode())) ++ env['diagnostics']['target']={'phase':{'uses':1}} ++ cls.allow=sign(env) ++ req=json.loads((ROOT/'examples/secrets-engine/check_request_allow_rotate.json').read_text()) ++ (cls.base/'allow-request.json').write_text(json.dumps(req)) ++ denied=copy.deepcopy(req);denied['action']='execute-ccr' ++ cls.deny=copy.deepcopy(env);cls.deny['effect']='deny';cls.deny['binding']['action']='execute-ccr';cls.deny['binding']['submitted_request_digest']=MODULE.request_digest(denied);cls.deny=sign(cls.deny) ++ (cls.base/'deny-request.json').write_text(json.dumps(denied)) ++ provenance=cls.allow['provenance'];cls.trust={key:provenance[key] for key in ('evaluator','mode','policy_package','policy_version','policy_package_digest')} ++ cls.trust.update(schema='secrets-engine.pdp-trust.v1',keys={'testdata-ed25519':'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'}) ++ @classmethod ++ def tearDownClass(cls):cls.scratch.cleanup() ++ def setUp(self): ++ for label,envelope in [('allow',self.allow),('deny',self.deny)]: ++ for kind in ('envelope','record'):(self.base/f'{label}-{kind}.json').write_text(json.dumps(envelope)) ++ (self.base/'trust.json').write_text(json.dumps(self.trust)) ++ self.config={'schema':'access-engine.signer-receipt-check.v1','trust_file':str(self.base/'trust.json'),'verifier':str(self.verifier),'verifier_sha256':hashlib.sha256(self.verifier.read_bytes()).hexdigest(),'expected_caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}} ++ for label in ('allow','deny'): ++ for kind in ('request','envelope','record'):self.config[f'{label}_{kind}']=str(self.base/f'{label}-{kind}.json') ++ def test_genuine_signed_receipt_and_refusal_matrix(self): ++ result=MODULE.verify(self.config,now=self.now) ++ self.assertTrue(all(result['checks'].values()));self.assertEqual(len(result['checks']),7) ++ def test_changed_source_record_refuses(self): ++ record=copy.deepcopy(self.allow);record['effect']='deny';(self.base/'allow-record.json').write_text(json.dumps(record)) ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_nested_record_number_type_change_refuses(self): ++ for replacement in (True,1.0): ++ with self.subTest(replacement_type=type(replacement).__name__): ++ record=copy.deepcopy(self.allow) ++ record['diagnostics']['target']['phase']['uses']=replacement ++ (self.base/'allow-record.json').write_text(json.dumps(record)) ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ ++ def test_receipt_hashes_use_the_once_read_verified_bytes(self): ++ actual_read=MODULE.read ++ seen={} ++ original={str(self.base/f'{label}-{suffix}.json'):actual_read(str(self.base/f'{label}-{suffix}.json')) for label in ('allow','deny') for suffix in ('request','envelope','record')} ++ def once(path): ++ if path in original: ++ seen[path]=seen.get(path,0)+1 ++ self.assertEqual(seen[path],1,'receipt was reopened after verification') ++ return actual_read(path) ++ with mock.patch.object(MODULE,'read',side_effect=once): ++ result=MODULE.verify(self.config,now=self.now) ++ self.assertEqual(len(seen),6) ++ for label in ('allow','deny'): ++ for suffix in ('request','envelope','record'): ++ self.assertEqual(result['input_sha256'][label+'_'+suffix],hashlib.sha256(original[str(self.base/f'{label}-{suffix}.json')]).hexdigest()) ++ ++ def test_oversized_file_read_is_bounded(self): ++ stream=mock.MagicMock();stream.__enter__.return_value=stream;stream.read.return_value=b'x'*524289 ++ with mock.patch.object(Path,'open',return_value=stream): ++ with self.assertRaises(ValueError):MODULE.read('/absolute/oversized.json') ++ stream.read.assert_called_once_with(524289) ++ ++ def test_unadmitted_helper_digest_refuses(self): ++ self.config['verifier_sha256']='0'*64 ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_disabled_caller_admission_refuses(self): ++ self.config['expected_caller']['mode']='disabled' ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_expired_captured_allow_refuses(self): ++ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now+timedelta(minutes=16)) ++ def test_wrong_independent_policy_pin_refuses(self): ++ wrong=copy.deepcopy(self.trust);wrong['policy_package_digest']='sha256:'+'0'*64;(self.base/'trust.json').write_text(json.dumps(wrong)) ++ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now) ++ ++if __name__=='__main__':unittest.main() +diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json +new file mode 100644 +--- /dev/null ++++ b/docs/native-signer-workload.json +@@ -0,0 +1,68 @@ ++{ ++ "schema": "access-engine.signer-workload-reference.v1", ++ "owner_repo": "access-engine", ++ "existing_workload": "flex-auth", ++ "authoritative_owner_declaration": "tenancy.yaml#/workload_identity", ++ "exact_signer_identity_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "declaration_scope": "Existing retained Secrets lifecycle PDP pin; declaration of identity source only. Not a new managed workload, credential grant, zone promotion or live admission.", ++ "observed_runtime": { ++ "cluster": "railiance01", ++ "node_internal_ip": "92.205.62.239", ++ "namespace": "flex-auth", ++ "release": "flex-auth-secrets-engine", ++ "deployment": "flex-auth-secrets-engine", ++ "reviewer_service_account": "flex-auth-secrets-engine", ++ "source_evidence": "the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json", ++ "evidence_sha256": "bafce991e02083ac30c8d6ce15f037dc22087bbb5dc8185c472149b46bca47c2" ++ }, ++ "identity_roles": { ++ "signer_workload": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "requesting_consumer": "system:serviceaccount:secrets-engine:secrets-engine", ++ "retained_caller_audience": "flex-auth", ++ "rule": "Signer custody binds the PDP workload, not the consumer or a human approver. Existing TokenReview reviewer projection does not grant signing-key custody." ++ }, ++ "custody_reference": { ++ "catalog_id": "flex-auth-decision-envelope-signing-key", ++ "owner_repo": "railiance-platform", ++ "current_state": "draft/nonresolvable", ++ "proposed_source_path": "platform/workloads/flex-auth/envelope-signing", ++ "proposed_private_field": "ED25519_PRIVATE_KEY", ++ "required_encoding": "unpadded base64url 32-byte Ed25519 seed; mount0440 allowed, group-write and world permissions refused", ++ "public_fields": [ ++ "ED25519_PUBLIC_KEY", ++ "kid" ++ ], ++ "proposed_catalog_workload_reference": { ++ "name": "flex-auth", ++ "declaration_ref": "access-engine/tenancy.yaml#/workload_identity", ++ "exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding" ++ }, ++ "key_created_or_admitted": false ++ }, ++ "managed_package_limit": { ++ "rapp_declaration_ref": null, ++ "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.", ++ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule." ++ }, ++ "unchanged_limits": [ ++ "tenancy and zones grades are unchanged", ++ "service-pilot literal false gate unchanged", ++ "human variant unchanged", ++ "legacy twelve lifecycle actions remain the selected policy package", ++ "no signer Secret name, kid, public fingerprint, custody receipt or rollout permission is supplied" ++ ], ++ "consumer_admission": "Secrets owner independently admits public keys/pins and exact installed verifier; this file never bootstraps trust from PDP discovery.", ++ "native_tool": { ++ "path": "tools/verify_native_signer_receipt.py", ++ "requires": "Installed reviewed Secrets Engine package/helper and independent public admission; source request, authenticated response and original source record for one allow and one deny.", ++ "operation": "Offline captured receipt checks only, no network/token input/key build or protected effect. Current clock validation; no CLI backdating option.", ++ "result_limits": "Does not prove record durability, real rejected HTTP caller, audit receiver custody, rotation, or authorization to run effect." ++ }, ++ "ci": { ++ "trigger": "Existing ordinary push to main when .forgejo/workflows/image.yaml, Containerfile, cmd/**, internal/**, pkg/**, examples/**, go.mod/go.sum or tools/publish-runtime-images.sh changes; workflow_dispatch also exists. This patch changes the workflow, so an authorized push would trigger it.", ++ "scope": "Existing container-build runner and already admitted forgejo.coulomb.social/coulomb/flex-auth package only in legacy mode; paired access-engine package publication requires separate existing dispatch authority and is excluded.", ++ "provenance_preparation": "Download archive/${REF}.tar.gz using full validated GITHUB_SHA; existing publisher labels org.opencontainers.image.revision and reads back registry manifest digest.", ++ "not_invoked": "No push, workflow dispatch, Docker build, image publication or paid resource allocation in this source preparation." ++ } ++} +diff --git a/examples/secrets-engine/signer-receipt-admission.template.json b/examples/secrets-engine/signer-receipt-admission.template.json +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine/signer-receipt-admission.template.json +@@ -0,0 +1,17 @@ ++{ ++ "schema": "access-engine.signer-receipt-check.v1", ++ "trust_file": "/absolute/owner-admitted/public-trust.json", ++ "verifier": "/absolute/installed/secrets-engine-pdp-verify", ++ "verifier_sha256": "REPLACE_WITH_INDEPENDENTLY_REVIEWED_INSTALLED_HELPER_SHA256", ++ "allow_request": "/absolute/sanitized-native-receipts/allow-request.json", ++ "allow_envelope": "/absolute/sanitized-native-receipts/allow-envelope.json", ++ "allow_record": "/absolute/sanitized-native-receipts/allow-source-record.json", ++ "deny_request": "/absolute/sanitized-native-receipts/deny-request.json", ++ "deny_envelope": "/absolute/sanitized-native-receipts/deny-envelope.json", ++ "deny_record": "/absolute/sanitized-native-receipts/deny-source-record.json", ++ "expected_caller": { ++ "mode": "enforce", ++ "principal": "system:serviceaccount:secrets-engine:secrets-engine", ++ "audience": "flex-auth" ++ } ++} +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 +@@ -184,7 +184,7 @@ + + ```task + id: FLEX-WP-0024-T06 +-status: wait ++status: progress + priority: high + ``` + +@@ -197,3 +197,43 @@ + source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns + consumer admission/enforcement. The old fixture completion never established this + gate, so the workplan is reopened rather than claiming it already passed. ++ ++ ++2026-10-16 owner native preparation: T06 is `progress` because Access still owns ++signer workload declaration, artifact/provenance and source/provider-join preparation; ++no unqualified cross-owner wait is claimed. Read-only actual release metadata and ++bounded proposal are in the Custodian ++`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`. ++The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but ++is draft/nonresolvable; no production kid/public fingerprint, Secret projection or ++custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080 ++instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed ++consumer native verification and must not form a circular blocking dependency. ++ ++The first proposed native trial only signs the unchanged lifecycle package on the ++existing retained release; `execute-ccr` remains denied. Current service-pilot ++`runtime_admission := false` remains mandatory: the source ServicePilotInputs is ++not yet wired into HTTP, and public service_actor_binding never authenticates the ++caller. Later activation needs a complete reviewed provider join, exact policy and ++operation admission, artifact digests, owner execution/rollback proposal and native ++receipts. Preparation/local synthetic success grants none of those permissions. ++ ++ ++2026-10-16 source-only native preparation adds the existing Secrets PDP ServiceAccount ++to `tenancy.yaml` with evidence in `docs/native-signer-workload.json`, makes the ++retained `flex-auth` audience explicit, pins CI archive fetch to the full source ++revision, and adds offline native signer receipt verification using the existing ++Secrets verifier. No key, trust, image publication or live activation is admitted. ++The managed workload's missing rapp declaration remains explicit migration debt; ++this source binding does not fabricate a package declaration or raise its grade. ++ ++ ++2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual ++HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls ++refuse across every policy, including the old separately named positive fixture; ++that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged. ++Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are ++source evidence only. B2 depends on independently reviewed Approval fact contract ++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. +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 +@@ -22,6 +22,7 @@ + "github.com/netkingdom/access-engine/internal/emission" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/taskexecution" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -397,7 +398,11 @@ + return fail(stderr, err) + } + +- mux := newServeMuxWithCallerAuth(engine, authenticator) ++ serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap()) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -448,6 +453,10 @@ + } + + func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux { ++ return newServeMuxWithServicePilotInputs(engine, authenticator, nil) ++} ++ ++func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -458,6 +467,25 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } ++ if engine.IsServicePilot() { ++ if serviceInputs == nil { ++ http.Error(w, "service input unavailable", http.StatusServiceUnavailable) ++ return ++ } ++ request, err := taskexecution.DecodeServiceRequest(r.Body) ++ if err != nil || request.Purpose != "" { ++ http.Error(w, "invalid service task request", http.StatusBadRequest) ++ return ++ } ++ input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC()) ++ if err != nil { ++ http.Error(w, "service task input refused", http.StatusForbidden) ++ return ++ } ++ decision, err := engine.CheckServicePilot(r.Context(), input) ++ writeHTTP(w, decision, err) ++ return ++ } + var request api.CheckRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) +@@ -478,6 +506,10 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { ++ if engine.IsServicePilot() { ++ http.Error(w, "service task batch unavailable", http.StatusForbidden) ++ return ++ } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return +@@ -500,6 +532,22 @@ + writeHTTP(w, decisions, err) + }) + return mux ++} ++ ++// 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() { ++ return nil, nil ++ } ++ if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { ++ return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping") ++ } ++ reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) ++ if err != nil { ++ return nil, err ++ } ++ return taskexecution.NewServicePilotInputs(reviewer) + } + + func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) { +@@ -615,7 +663,7 @@ + + func writeHTTP(w http.ResponseWriter, value any, err error) { + w.Header().Set("content-type", "application/json") +- if errors.Is(err, decisioncore.ErrRecordNotCommitted) { ++ if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) { + // Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024). + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return +diff --git a/cmd/access-engine/service_pilot_http_test.go b/cmd/access-engine/service_pilot_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/service_pilot_http_test.go +@@ -0,0 +1,376 @@ ++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" ++ "sync/atomic" ++ "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" ++) ++ ++func pilotRequest(now time.Time) api.CheckRequest { ++ return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}} ++} ++func pilotEngine(t *testing.T) *decision.Engine { ++ t.Helper() ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ return engine ++} ++func pilotToken(kind string, now time.Time) string { ++ payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()} ++ if kind == "expired" { ++ payload["exp"] = now.Add(-time.Second).Unix() ++ } ++ if kind == "no-expiry" { ++ delete(payload, "exp") ++ } ++ raw, _ := json.Marshal(payload) ++ return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token" ++} ++func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) { ++ now := time.Now().UTC() ++ var reviews atomic.Int64 ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reviews.Add(1) ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ t.Error("unexpected reviewer route/auth") ++ w.WriteHeader(403) ++ return ++ } ++ var body struct { ++ Spec struct { ++ Token string `json:"token"` ++ Audiences []string `json:"audiences"` ++ } `json:"spec"` ++ } ++ if json.NewDecoder(r.Body).Decode(&body) != nil { ++ w.WriteHeader(400) ++ return ++ } ++ if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) { ++ t.Error("unexpected bounded runtime audience request") ++ } ++ pieces := strings.Split(body.Spec.Token, ".") ++ raw, _ := base64.RawURLEncoding.DecodeString(pieces[1]) ++ var claims map[string]any ++ json.Unmarshal(raw, &claims) ++ principal := taskexecution.ServicePilotPrincipal ++ audiences := []string{"flex-auth"} ++ authenticated := true ++ switch claims["case"] { ++ case "foreign": ++ principal = "system:serviceaccount:other:other" ++ case "alternate-audience": ++ audiences = []string{"access-engine"} ++ case "wrong-audience": ++ audiences = []string{"openbao"} ++ case "unauthenticated": ++ authenticated = false ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}}) ++ })) ++ defer reviewer.Close() ++ folder := t.TempDir() ++ ca := filepath.Join(folder, "ca.crt") ++ reviewToken := filepath.Join(folder, "reviewer-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600) ++ engine := pilotEngine(t) ++ private := ed25519.NewKeyFromSeed(make([]byte, 32)) ++ if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil { ++ t.Fatal("signer config") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl")) ++ engine.SetDecisionLog(log) ++ bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal} ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs)) ++ defer server.Close() ++ post := func(path string, request api.CheckRequest, token string) (int, []byte) { ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw)) ++ if token != "" { ++ req.Header.Set("Authorization", "Bearer "+token) ++ } ++ 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 ++ } ++ original := pilotRequest(now) ++ status, body := post("/v1/check", original, pilotToken("valid", now)) ++ if status != 200 { ++ t.Fatalf("status=%d", status) ++ } ++ var envelope api.DecisionEnvelope ++ if json.Unmarshal(body, &envelope) != nil { ++ t.Fatal("decode") ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" { ++ t.Fatal("B1 changed policy gate") ++ } ++ if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("actual response signature") ++ } ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original submitted request/context changed") ++ } ++ if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" { ++ t.Fatal("caller provenance") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) { ++ t.Fatal("recorded signed response differs") ++ } ++ if reviews.Load() != 1 { ++ t.Fatal("request was reviewed more than once") ++ } ++ for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} { ++ t.Run(kind, func(t *testing.T) { ++ status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now)) ++ if status == 200 { ++ t.Fatal("invalid source caller accepted") ++ } ++ }) ++ } ++ for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) { ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano) ++ }} { ++ request := pilotRequest(now) ++ mutate(&request) ++ if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 { ++ t.Fatal("invalid public request accepted") ++ } ++ } ++ if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 { ++ t.Fatal("missing caller accepted") ++ } ++ if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 { ++ t.Fatal("batch bypass") ++ } ++ records, _ = log.ReadAll() ++ if len(records) != 1 { ++ t.Fatal("refused request entered decision record") ++ } ++ ++ // Exercise the actual production serve flag/configuration path in a disposable ++ // subprocess of this compiled test binary. Its environment contains only the ++ // synthetic argument handoff; no ambient backend credentials are inherited. ++ seedFile := filepath.Join(folder, "synthetic-seed") ++ os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440) ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-records.jsonl") ++ cliArgs := []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", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"} ++ argsJSON, _ := json.Marshal(cliArgs) ++ 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 child.Start() != nil { ++ t.Fatal("start disposable CLI") ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ resp, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ resp.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("disposable production serve path not ready") ++ } ++ raw, _ := json.Marshal(original) ++ cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ cliResponse, err := http.DefaultClient.Do(cliRequest) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer cliResponse.Body.Close() ++ var cliEnvelope api.DecisionEnvelope ++ if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil { ++ t.Fatal("production serve path refused supported source input") ++ } ++ if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("production CLI did not emit signed DENY") ++ } ++ if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("production CLI changed original request/context") ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("production CLI response/record differ") ++ } ++ for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} { ++ if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("non-enforcing task mode accepted") ++ } ++ } ++ if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("task pin renamed audience accepted") ++ } ++ missing := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body)) ++ newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req) ++ if missing.Code != 503 { ++ t.Fatal("unwired generic handler accepted task pin") ++ } ++} ++func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) { ++ engine := pilotEngine(t) ++ request := pilotRequest(time.Now()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic Check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil { ++ t.Fatal("generic Batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil { ++ t.Fatal("generic List bypass") ++ } ++ if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil { ++ t.Fatal("zero input trusted") ++ } ++ folder := t.TempDir() ++ path := filepath.Join(folder, "request.json") ++ raw, _ := json.Marshal(request) ++ os.WriteFile(path, raw, 0600) ++ var stdout, stderr bytes.Buffer ++ code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr) ++ if code == 0 || stdout.Len() != 0 { ++ t.Fatal("generic CLI emitted task verdict") ++ } ++} ++ ++// The earlier separately named positive fixture policy is not an exception: ++// generic task actions cannot bypass the typed path on any loaded package. ++func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ p.Metadata.ID = "synthetic-legacy-stack-positive-only" ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ request := pilotRequest(time.Now()) ++ result, err := p.Evaluate(context.Background(), request) ++ if err != nil || result.Effect != api.DecisionEffectAllow { ++ t.Fatal("synthetic precondition did not allow") ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("separately named generic task allow escaped") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task batch escaped") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task list escaped") ++ } ++ raw, _ := json.Marshal(request) ++ recorder := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw)) ++ newServeMux(engine).ServeHTTP(recorder, req) ++ if recorder.Code != 503 { ++ t.Fatal("separate synthetic generic HTTP task route did not refuse") ++ } ++} ++ ++func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) { ++ now := time.Now().UTC() ++ adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ for _, accidentalAllow := range []bool{false, true} { ++ t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl")) ++ engine.SetDecisionLog(log) ++ if _, err := engine.CheckServicePilot(context.Background(), input); err == nil { ++ t.Fatal("B1 released unsupported result") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 0 { ++ t.Fatal("withheld result entered recorder") ++ } ++ }) ++ } ++} ++ ++func TestServicePilotChildCLI(t *testing.T) { ++ raw := os.Getenv("ACCESS_B1_SERVER_ARGS") ++ if raw == "" { ++ return ++ } ++ var args []string ++ if json.Unmarshal([]byte(raw), &args) != nil { ++ os.Exit(2) ++ } ++ os.Exit(run(args, os.Stdout, os.Stderr)) ++} +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -20,6 +20,7 @@ + "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" + ) + +@@ -153,9 +154,44 @@ + return envelope, nil + } + ++// IsServicePilot selects the existing exact source package, never a request flag. ++func (e *Engine) IsServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion ++} ++ ++// ErrServicePilotUnavailable withholds any result that is not this B1 slice's ++// signed DENY. Provider joins and executable eligibility remain unimplemented. ++var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable") ++ ++// CheckServicePilot records only a signed DENY after authenticating the exact ++// immutable service input. It does not validate Approval facts or grant an act. ++func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) { ++ if !e.IsServicePilot() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ request, err := input.RequestAt(e.now()) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ + // 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" { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } + decision, err := e.decide(ctx, request) + if err != nil { + return api.DecisionEnvelope{}, err +@@ -169,6 +205,9 @@ + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) + for _, resource := range request.Resources { + decision, err := e.decide(ctx, api.CheckRequest{ +@@ -193,6 +232,9 @@ + + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) + decisions := make([]api.DecisionEnvelope, 0, len(candidates)) + for _, resource := range candidates { +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 +@@ -1,20 +1,27 @@ + package taskexecution + + import ( ++ "bytes" + "context" + "encoding/json" + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "io" + "time" + ) + + var ErrServiceInput = errors.New("service task execution input refused") + ++const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" ++const ServicePilotPackageVersion = "v1" ++ + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" + +-// ServicePilotInputs is source preparation only. It installs the explicit ++// ServicePilotInputs installs the exact B1 service input mapping for signed DENY. ++// It does not validate Approval facts or establish executable eligibility. ++// 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 } +@@ -46,6 +53,44 @@ + return r, nil + } + func (s ServiceInput) Caller() callerauth.Record { return s.caller } ++ ++// RequestAt rechecks transport and operation lifetime at the runtime use site. ++// Request() remains a detached source snapshot, never an execution capability. ++func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) { ++ r, err := s.Request() ++ if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ contract, ok := r.Context["execution_contract"].(map[string]any) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ value, ok := contract["operation_deadline"].(string) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ deadline, err := time.Parse(time.RFC3339Nano, value) ++ if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return r, nil ++} ++ ++// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded ++// task-only route. It never changes producer canonicalization or legacy input. ++func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ var request api.CheckRequest ++ if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil { ++ return request, ErrServiceInput ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} + + // DeclaredServiceBinding is public immutable metadata to bind at approval issue. + // It is not authentication evidence; Prepare independently verifies its principal. +diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go +--- a/internal/taskexecution/service_input_test.go ++++ b/internal/taskexecution/service_input_test.go +@@ -1,7 +1,9 @@ + package taskexecution + + import ( ++ "bytes" + "context" ++ "encoding/json" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "testing" +@@ -79,3 +81,35 @@ + t.Fatal("missing owner reviewer accepted") + } + } ++ ++func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) { ++ now := time.Now().UTC() ++ r := serviceRequest(t, now) ++ reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} ++ adapter, _ := NewServicePilotInputs(reviewer) ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := input.RequestAt(now); err != nil { ++ t.Fatal(err) ++ } ++ for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} { ++ if _, err := input.RequestAt(when); err == nil { ++ t.Fatal("stale/invalid runtime input accepted") ++ } ++ } ++ raw, _ := json.Marshal(r) ++ decoded, err := DecodeServiceRequest(bytes.NewReader(raw)) ++ if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) { ++ t.Fatal("valid decode changed canonical submitted input") ++ } ++ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context" ++ "bytes" ++ "encoding/json":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} ++ for _, body := range bad { ++ if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil { ++ t.Fatal("malformed/oversized task request accepted") ++ } ++ } ++} +diff --git a/internal/taskexecution/identity.go b/internal/taskexecution/identity.go +--- a/internal/taskexecution/identity.go ++++ b/internal/taskexecution/identity.go +@@ -1,5 +1,6 @@ +-// Package taskexecution contains proposed source-only identity input validation. +-// It is not registered with HTTP evaluation and grants no executable admission. ++// Package taskexecution contains task input validation, not policy evaluation. ++// The human component remains source-only. The B1 service HTTP path can emit ++// only signed DENY and grants no executable admission. + package taskexecution + + import ( +diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md +--- a/docs/service-task-execution-contract.md ++++ b/docs/service-task-execution-contract.md +@@ -51,9 +51,32 @@ + + Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication. + Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping +-admission must be proved before runtime use. The adapter is not wired into HTTP +-routes, decision engine evaluation or a native executor; source package remains +-hard-closed. A forged public service_actor_binding alone remains insufficient. ++admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only ++when the exact installed service-pilot v1 package is selected. Startup requires ++enforced retained `flex-auth` audience and the one exact Secrets mapping. The ++actual reviewer authenticates once; the detached ServiceInput is rechecked for ++caller/operation expiry at use. The single existing decision engine records only ++a signed DENY on this path; unsigned output or an accidental policy allow is ++withheld before recording. No native executor or Approval fact join is installed. ++A forged public service_actor_binding alone remains insufficient. ++ ++Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate ++`execute-ccr` under any package; the dedicated typed path is required. The exact ++service pin also refuses generic calls and batch requests. This deliberately ++invalidates the older separately named synthetic positive stack fixture on the ++new source. Its positive proof remains historical at Access `ec21518e`, not proof ++for B1. Do not add a fixture exception; a positive successor must use reviewed ++B2/B3 provider joins. The literal service policy false gate is unchanged. ++ ++B1 retains the exact original request context, carried claim, submitted digest ++and producer-owned canonicalization. Its bounded strict JSON decoder refuses ++duplicates, trailing/unknown typed fields and oversized input. The caller's ++Approval blob remains unverified and cannot enable anything. B2 requires the ++Approval owner's fact contract/vector review; B3 additionally requires exact ++server-owned native-act representation and the complete trusted join. Fresh ++provider observations stay out of band: no refreshed context or claim replacement ++may relax strict Secrets request correspondence. Access never invokes a ++credential-carrying Python subprocess, consumes approval or performs the effect. + + ## Exact operation and independent gates + +@@ -129,7 +152,8 @@ + admit grant/baseline/estate or perform an effect. Those are explicit implementation + requirements; a matching predicate and reviewed TokenReview input are insufficient. + The service adapter carries authenticated transport evidence outside the canonical +-request but is not connected to HTTP evaluation/provider validation. A caller's ++request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and ++executable eligibility remain absent. A caller's + service_actor_binding remains a public declaration independently compared by the + adapter, never a substitute for runtime provenance or exact operation authority. + diff --git a/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-r2-full.patch b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-r2-full.patch new file mode 100644 index 00000000..01d81826 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-16/access-native-source/b1/native-prep-r2-and-b1-r2-full.patch @@ -0,0 +1,1199 @@ +diff --git a/tenancy.yaml b/tenancy.yaml +--- a/tenancy.yaml ++++ b/tenancy.yaml +@@ -11,8 +11,16 @@ + workload_identity: + name: flex-auth + kind: platform-service +- responsible_repo: flex-auth ++ responsible_repo: access-engine + identity_bindings: ++ # Existing retained Secrets PDP pin; identity source, not key admission. ++ - scheme: kubernetes-service-account ++ authority: railiance01 ++ subject: system:serviceaccount:flex-auth:flex-auth-secrets-engine ++ principal_type: service ++ environment: prod ++ evidence: ++ - docs/native-signer-workload.json + - scheme: kubernetes-service-account + authority: railiance01 + subject: system:serviceaccount:flex-auth:flex-auth-ops-warden +diff --git a/values/secrets-engine.yaml b/values/secrets-engine.yaml +--- a/values/secrets-engine.yaml ++++ b/values/secrets-engine.yaml +@@ -13,6 +13,8 @@ + - /opt/flex-auth/examples/secrets-engine/policy_package.md + callerAuth: + mode: enforce ++ # Retained runtime audience is explicit; signer preparation does not rename it. ++ audience: flex-auth + kubernetesURL: https://10.43.0.1 + binding: secrets-engine=system:serviceaccount:secrets-engine:secrets-engine + consumer: +diff --git a/charts/flex-auth/templates/deployment.yaml b/charts/flex-auth/templates/deployment.yaml +--- a/charts/flex-auth/templates/deployment.yaml ++++ b/charts/flex-auth/templates/deployment.yaml +@@ -83,6 +83,8 @@ + {{- if not (and .Values.readback .Values.readback.enabled) }} + {{- $binding = required "callerAuth.binding is required when callerAuth.mode is warn or enforce" .Values.callerAuth.binding }} + {{- end }} ++ - --caller-audience ++ - {{ required "callerAuth.audience is required" .Values.callerAuth.audience | quote }} + - --caller-auth-mode + - {{ include "flex-auth.callerAuth.mode" . | quote }} + - --caller-kubernetes-url +diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml +--- a/.forgejo/workflows/image.yaml ++++ b/.forgejo/workflows/image.yaml +@@ -51,7 +51,7 @@ + SHORT="${REF:0:7}" + mkdir -p buildctx "${HOME}/bin" + wget -qO /tmp/repo.tar.gz \ +- "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" ++ "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz" + tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1 + wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ + | tar xz --strip-components=1 -C "${HOME}/bin" docker/docker +diff --git a/tools/verify_native_signer_receipt.py b/tools/verify_native_signer_receipt.py +new file mode 100644 +--- /dev/null ++++ b/tools/verify_native_signer_receipt.py +@@ -0,0 +1,135 @@ ++#!/usr/bin/env python3 ++"""Verify captured native decision receipts with the installed Secrets verifier. ++ ++Offline only: no token input, network request, key generation, build or effect. ++Use an independently reviewed admission file; success never admits that file. ++""" ++from __future__ import annotations ++import argparse ++import copy ++from datetime import datetime, timezone ++import hashlib ++import json ++from pathlib import Path ++import re ++ ++# These are the consumer's existing verifier/binding functions, not another PDP ++# or signature/canonicalization implementation. Install the reviewed bundle first. ++from secrets_engine.authorization import request_digest, validate_decision_envelope ++from secrets_engine.decision_signature import authenticate_response, load_trust, strict_json ++from secrets_engine.errors import DecisionError ++ ++FIELDS = {'schema','trust_file','verifier','verifier_sha256','allow_request','allow_envelope','allow_record','deny_request','deny_envelope','deny_record','expected_caller'} ++ ++def read(path: str) -> bytes: ++ target = Path(path) ++ if not target.is_absolute(): ++ raise ValueError('absolute input path required') ++ with target.open("rb") as stream: ++ data = stream.read(524289) ++ if len(data) > 524288: ++ raise ValueError('receipt input too large') ++ return data ++ ++def same_json_types(left, right) -> bool: ++ """JSON value correspondence only; never signature canonicalization.""" ++ if type(left) is not type(right): ++ return False ++ if isinstance(left, dict): ++ return left.keys() == right.keys() and all(same_json_types(value,right[key]) for key,value in left.items()) ++ if isinstance(left, list): ++ return len(left) == len(right) and all(same_json_types(a,b) for a,b in zip(left,right)) ++ return left == right ++ ++ ++def stamp(value: str) -> datetime: ++ parsed = datetime.fromisoformat(value.replace('Z','+00:00')) ++ if parsed.tzinfo is None: ++ raise ValueError('timezone required') ++ return parsed.astimezone(timezone.utc) ++ ++def verify(config: dict, *, now: datetime | None = None) -> dict: ++ if not isinstance(config,dict) or set(config) != FIELDS or config['schema'] != 'access-engine.signer-receipt-check.v1': ++ raise ValueError('unsupported admission fields') ++ current = now or datetime.now(timezone.utc) ++ caller = config['expected_caller'] ++ if caller != {'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}: ++ raise ValueError('retained native caller admission required') ++ executable = Path(config['verifier']) ++ if not executable.is_absolute() or not re.fullmatch('[a-f0-9]{64}',config['verifier_sha256']) or hashlib.sha256(executable.read_bytes()).hexdigest() != config['verifier_sha256']: ++ raise ValueError('installed verifier digest mismatch') ++ trust_bytes = read(config['trust_file']) ++ trust = load_trust(Path(config['trust_file'])) ++ if not same_json_types(trust,strict_json(trust_bytes)): ++ raise ValueError('trust input changed during verification') ++ if (trust['evaluator'],trust['mode'],trust['policy_package'],trust['policy_version']) != ('flex-auth/local','standalone','secrets-engine.catalog-lane.lifecycle','v2'): ++ raise ValueError('signer-only lifecycle package required') ++ checks = {} ++ envelopes = {} ++ inputs = {} ++ requests = {} ++ for label in ('allow','deny'): ++ blobs = {suffix:read(config[label+'_'+suffix]) for suffix in ('request','envelope','record')} ++ body = blobs['envelope'] ++ # Strict decode before helper; authenticated typed return alone is acted on. ++ strict_json(body.decode('utf-8')) ++ envelope = authenticate_response(body,trust=trust,verifier=executable) ++ request = strict_json(blobs['request']) ++ record = strict_json(blobs['record']) ++ if not same_json_types(record,envelope): ++ raise ValueError('captured original source record differs from authenticated response') ++ provenance = envelope['provenance']['caller'] ++ if any(provenance.get(key) != value for key,value in caller.items()) or stamp(provenance['not_after']) <= current: ++ raise ValueError('native caller provenance missing or expired') ++ if label == 'allow': ++ validate_decision_envelope(envelope,request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=current) ++ else: ++ if envelope['effect'] != 'deny' or envelope['binding']['submitted_request_digest'] != request_digest(request): ++ raise ValueError('deny receipt does not bind exact negative request') ++ envelopes[label] = envelope ++ requests[label] = request ++ for suffix in ('request','envelope','record'): ++ inputs[label+'_'+suffix] = hashlib.sha256(blobs[suffix]).hexdigest() ++ checks[label+'_authenticated_bound_and_record_matched'] = True ++ # These are local refusals against the captured real producer envelope. They ++ # do not claim a rejected caller was exercised on the native HTTP endpoint. ++ for label in ('altered','unsigned','unknown_kid','wrong_policy_pin'): ++ bad = copy.deepcopy(envelopes['allow']); pins = copy.deepcopy(trust) ++ if label == 'altered': bad['binding']['action'] = 'forged-action' ++ elif label == 'unsigned': bad['signature'] = {'mode':'unsigned'} ++ elif label == 'unknown_kid': bad['signature']['kid'] = 'unadmitted-key' ++ else: pins['policy_package_digest'] = 'sha256:'+'0'*64 ++ try: ++ authenticate_response(json.dumps(bad).encode(),trust=pins,verifier=executable) ++ except DecisionError: ++ checks[label+'_refused'] = True ++ else: ++ raise ValueError('negative authentication case accepted') ++ try: ++ expiry=stamp(envelopes['allow']['lifetime']['expires_at']) ++ request=requests['allow'] ++ validate_decision_envelope(envelopes['allow'],request,accepted_policy_packages={trust['policy_package']},accepted_policy_versions={trust['policy_version']},now=expiry) ++ except DecisionError: ++ checks['expiry_boundary_refused'] = True ++ else: ++ raise ValueError('expired allow accepted') ++ return {'schema':'access-engine.signer-receipt-result.v1','checks':checks,'input_sha256':inputs,'verifier_sha256':config['verifier_sha256'],'trust_file_sha256':hashlib.sha256(trust_bytes).hexdigest(),'boundary':'offline captured signed receipt checks only; no native caller-denial, record durability, receiver custody, trust admission, rotation or protected effect inferred'} ++ ++def main() -> int: ++ parser=argparse.ArgumentParser(description=__doc__) ++ parser.add_argument('--admission',type=Path,required=True) ++ parser.add_argument('--output',type=Path,required=True) ++ args=parser.parse_args() ++ try: ++ config=strict_json(read(str(args.admission))) ++ result=verify(config) ++ args.output.write_text(json.dumps(result,indent=2)+'\n') ++ except (OSError,ValueError,TypeError,KeyError,RecursionError,DecisionError): ++ # Never dump source objects, unexpected key material or verifier stderr. ++ print('signer receipt verification refused') ++ return 1 ++ print('captured signer receipt checks passed') ++ return 0 ++ ++if __name__ == '__main__': ++ raise SystemExit(main()) +diff --git a/tests/test_native_signer_receipt.py b/tests/test_native_signer_receipt.py +new file mode 100644 +--- /dev/null ++++ b/tests/test_native_signer_receipt.py +@@ -0,0 +1,100 @@ ++"""Offline tool regression using the producer's existing synthetic signer only.""" ++import copy ++from datetime import datetime,timedelta,timezone ++import hashlib ++import importlib.util ++import json ++import os ++from pathlib import Path ++import subprocess ++import tempfile ++import unittest ++from unittest import mock ++ ++ROOT=Path(__file__).resolve().parents[1] ++@unittest.skipUnless(os.environ.get('SECRETS_ENGINE_SOURCE_ROOT'), 'requires reviewed Secrets consumer source/bundle for integration checks') ++class NativeSignerReceiptTests(unittest.TestCase): ++ @classmethod ++ def setUpClass(cls): ++ global MODULE ++ spec=importlib.util.spec_from_file_location('signer_receipt',ROOT/'tools/verify_native_signer_receipt.py') ++ MODULE=importlib.util.module_from_spec(spec);spec.loader.exec_module(MODULE) ++ consumer=Path(os.environ['SECRETS_ENGINE_SOURCE_ROOT']) ++ cls.scratch=tempfile.TemporaryDirectory();cls.base=Path(cls.scratch.name) ++ env={**os.environ,'GOFLAGS':'-buildvcs=false'} ++ cls.verifier=cls.base/'verify';cls.signer=cls.base/'synthetic-sign' ++ for binary,package in [(cls.verifier,'.'),(cls.signer,'./testsupport/signer')]: ++ subprocess.run(['go','build','-p','2','-o',str(binary),package],cwd=consumer/'verifier',env=env,check=True) ++ cls.now=datetime.now(timezone.utc) ++ env=json.loads((consumer/'tests/fixtures/pdp-signature/decision_rotate_signed.json').read_text()) ++ env['provenance']['caller']={'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth','not_after':(cls.now+timedelta(minutes=20)).isoformat()} ++ env['lifetime']['not_before']=(cls.now-timedelta(seconds=1)).isoformat();env['lifetime']['expires_at']=(cls.now+timedelta(minutes=15)).isoformat() ++ def sign(envelope): ++ return json.loads(subprocess.check_output([str(cls.signer)],input=json.dumps(envelope).encode())) ++ env['diagnostics']['target']={'phase':{'uses':1}} ++ cls.allow=sign(env) ++ req=json.loads((ROOT/'examples/secrets-engine/check_request_allow_rotate.json').read_text()) ++ (cls.base/'allow-request.json').write_text(json.dumps(req)) ++ denied=copy.deepcopy(req);denied['action']='execute-ccr' ++ cls.deny=copy.deepcopy(env);cls.deny['effect']='deny';cls.deny['binding']['action']='execute-ccr';cls.deny['binding']['submitted_request_digest']=MODULE.request_digest(denied);cls.deny=sign(cls.deny) ++ (cls.base/'deny-request.json').write_text(json.dumps(denied)) ++ provenance=cls.allow['provenance'];cls.trust={key:provenance[key] for key in ('evaluator','mode','policy_package','policy_version','policy_package_digest')} ++ cls.trust.update(schema='secrets-engine.pdp-trust.v1',keys={'testdata-ed25519':'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'}) ++ @classmethod ++ def tearDownClass(cls):cls.scratch.cleanup() ++ def setUp(self): ++ for label,envelope in [('allow',self.allow),('deny',self.deny)]: ++ for kind in ('envelope','record'):(self.base/f'{label}-{kind}.json').write_text(json.dumps(envelope)) ++ (self.base/'trust.json').write_text(json.dumps(self.trust)) ++ self.config={'schema':'access-engine.signer-receipt-check.v1','trust_file':str(self.base/'trust.json'),'verifier':str(self.verifier),'verifier_sha256':hashlib.sha256(self.verifier.read_bytes()).hexdigest(),'expected_caller':{'mode':'enforce','principal':'system:serviceaccount:secrets-engine:secrets-engine','audience':'flex-auth'}} ++ for label in ('allow','deny'): ++ for kind in ('request','envelope','record'):self.config[f'{label}_{kind}']=str(self.base/f'{label}-{kind}.json') ++ def test_genuine_signed_receipt_and_refusal_matrix(self): ++ result=MODULE.verify(self.config,now=self.now) ++ self.assertTrue(all(result['checks'].values()));self.assertEqual(len(result['checks']),7) ++ def test_changed_source_record_refuses(self): ++ record=copy.deepcopy(self.allow);record['effect']='deny';(self.base/'allow-record.json').write_text(json.dumps(record)) ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_nested_record_number_type_change_refuses(self): ++ for replacement in (True,1.0): ++ with self.subTest(replacement_type=type(replacement).__name__): ++ record=copy.deepcopy(self.allow) ++ record['diagnostics']['target']['phase']['uses']=replacement ++ (self.base/'allow-record.json').write_text(json.dumps(record)) ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ ++ def test_receipt_hashes_use_the_once_read_verified_bytes(self): ++ actual_read=MODULE.read ++ seen={} ++ original={str(self.base/f'{label}-{suffix}.json'):actual_read(str(self.base/f'{label}-{suffix}.json')) for label in ('allow','deny') for suffix in ('request','envelope','record')} ++ def once(path): ++ if path in original: ++ seen[path]=seen.get(path,0)+1 ++ self.assertEqual(seen[path],1,'receipt was reopened after verification') ++ return actual_read(path) ++ with mock.patch.object(MODULE,'read',side_effect=once): ++ result=MODULE.verify(self.config,now=self.now) ++ self.assertEqual(len(seen),6) ++ for label in ('allow','deny'): ++ for suffix in ('request','envelope','record'): ++ self.assertEqual(result['input_sha256'][label+'_'+suffix],hashlib.sha256(original[str(self.base/f'{label}-{suffix}.json')]).hexdigest()) ++ ++ def test_oversized_file_read_is_bounded(self): ++ stream=mock.MagicMock();stream.__enter__.return_value=stream;stream.read.return_value=b'x'*524289 ++ with mock.patch.object(Path,'open',return_value=stream): ++ with self.assertRaises(ValueError):MODULE.read('/absolute/oversized.json') ++ stream.read.assert_called_once_with(524289) ++ ++ def test_unadmitted_helper_digest_refuses(self): ++ self.config['verifier_sha256']='0'*64 ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_disabled_caller_admission_refuses(self): ++ self.config['expected_caller']['mode']='disabled' ++ with self.assertRaises(ValueError):MODULE.verify(self.config,now=self.now) ++ def test_expired_captured_allow_refuses(self): ++ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now+timedelta(minutes=16)) ++ def test_wrong_independent_policy_pin_refuses(self): ++ wrong=copy.deepcopy(self.trust);wrong['policy_package_digest']='sha256:'+'0'*64;(self.base/'trust.json').write_text(json.dumps(wrong)) ++ with self.assertRaises(MODULE.DecisionError):MODULE.verify(self.config,now=self.now) ++ ++if __name__=='__main__':unittest.main() +diff --git a/docs/native-signer-workload.json b/docs/native-signer-workload.json +new file mode 100644 +--- /dev/null ++++ b/docs/native-signer-workload.json +@@ -0,0 +1,68 @@ ++{ ++ "schema": "access-engine.signer-workload-reference.v1", ++ "owner_repo": "access-engine", ++ "existing_workload": "flex-auth", ++ "authoritative_owner_declaration": "tenancy.yaml#/workload_identity", ++ "exact_signer_identity_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "declaration_scope": "Existing retained Secrets lifecycle PDP pin; declaration of identity source only. Not a new managed workload, credential grant, zone promotion or live admission.", ++ "observed_runtime": { ++ "cluster": "railiance01", ++ "node_internal_ip": "92.205.62.239", ++ "namespace": "flex-auth", ++ "release": "flex-auth-secrets-engine", ++ "deployment": "flex-auth-secrets-engine", ++ "reviewer_service_account": "flex-auth-secrets-engine", ++ "source_evidence": "the-custodian/docs/changes/netkingdom-target-2026-10-16/access-native-preparation/live-metadata-r1.json", ++ "evidence_sha256": "bafce991e02083ac30c8d6ce15f037dc22087bbb5dc8185c472149b46bca47c2" ++ }, ++ "identity_roles": { ++ "signer_workload": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "requesting_consumer": "system:serviceaccount:secrets-engine:secrets-engine", ++ "retained_caller_audience": "flex-auth", ++ "rule": "Signer custody binds the PDP workload, not the consumer or a human approver. Existing TokenReview reviewer projection does not grant signing-key custody." ++ }, ++ "custody_reference": { ++ "catalog_id": "flex-auth-decision-envelope-signing-key", ++ "owner_repo": "railiance-platform", ++ "current_state": "draft/nonresolvable", ++ "proposed_source_path": "platform/workloads/flex-auth/envelope-signing", ++ "proposed_private_field": "ED25519_PRIVATE_KEY", ++ "required_encoding": "unpadded base64url 32-byte Ed25519 seed; mount0440 allowed, group-write and world permissions refused", ++ "public_fields": [ ++ "ED25519_PUBLIC_KEY", ++ "kid" ++ ], ++ "proposed_catalog_workload_reference": { ++ "name": "flex-auth", ++ "declaration_ref": "access-engine/tenancy.yaml#/workload_identity", ++ "exact_binding": "system:serviceaccount:flex-auth:flex-auth-secrets-engine", ++ "resolution": "unknown until owner custody and managed-package admission; do not mark resolvable merely from source binding" ++ }, ++ "key_created_or_admitted": false ++ }, ++ "managed_package_limit": { ++ "rapp_declaration_ref": null, ++ "status": "Existing managed-deployable migration debt remains unknown under security-zones_v0.1 \u00a76; no matching authoritative rapp declaration was established. Existing source tenancy declaration supplies owner identity binding but cannot substitute for managed-package admission.", ++ "no_fiction": "Do not invent a rapp tuple or describe this Helm-managed Deployment as an independent operational unit to bypass the rule." ++ }, ++ "unchanged_limits": [ ++ "tenancy and zones grades are unchanged", ++ "service-pilot literal false gate unchanged", ++ "human variant unchanged", ++ "legacy twelve lifecycle actions remain the selected policy package", ++ "no signer Secret name, kid, public fingerprint, custody receipt or rollout permission is supplied" ++ ], ++ "consumer_admission": "Secrets owner independently admits public keys/pins and exact installed verifier; this file never bootstraps trust from PDP discovery.", ++ "native_tool": { ++ "path": "tools/verify_native_signer_receipt.py", ++ "requires": "Installed reviewed Secrets Engine package/helper and independent public admission; source request, authenticated response and original source record for one allow and one deny.", ++ "operation": "Offline captured receipt checks only, no network/token input/key build or protected effect. Current clock validation; no CLI backdating option.", ++ "result_limits": "Does not prove record durability, real rejected HTTP caller, audit receiver custody, rotation, or authorization to run effect." ++ }, ++ "ci": { ++ "trigger": "Existing ordinary push to main when .forgejo/workflows/image.yaml, Containerfile, cmd/**, internal/**, pkg/**, examples/**, go.mod/go.sum or tools/publish-runtime-images.sh changes; workflow_dispatch also exists. This patch changes the workflow, so an authorized push would trigger it.", ++ "scope": "Existing container-build runner and already admitted forgejo.coulomb.social/coulomb/flex-auth package only in legacy mode; paired access-engine package publication requires separate existing dispatch authority and is excluded.", ++ "provenance_preparation": "Download archive/${REF}.tar.gz using full validated GITHUB_SHA; existing publisher labels org.opencontainers.image.revision and reads back registry manifest digest.", ++ "not_invoked": "No push, workflow dispatch, Docker build, image publication or paid resource allocation in this source preparation." ++ } ++} +diff --git a/examples/secrets-engine/signer-receipt-admission.template.json b/examples/secrets-engine/signer-receipt-admission.template.json +new file mode 100644 +--- /dev/null ++++ b/examples/secrets-engine/signer-receipt-admission.template.json +@@ -0,0 +1,17 @@ ++{ ++ "schema": "access-engine.signer-receipt-check.v1", ++ "trust_file": "/absolute/owner-admitted/public-trust.json", ++ "verifier": "/absolute/installed/secrets-engine-pdp-verify", ++ "verifier_sha256": "REPLACE_WITH_INDEPENDENTLY_REVIEWED_INSTALLED_HELPER_SHA256", ++ "allow_request": "/absolute/sanitized-native-receipts/allow-request.json", ++ "allow_envelope": "/absolute/sanitized-native-receipts/allow-envelope.json", ++ "allow_record": "/absolute/sanitized-native-receipts/allow-source-record.json", ++ "deny_request": "/absolute/sanitized-native-receipts/deny-request.json", ++ "deny_envelope": "/absolute/sanitized-native-receipts/deny-envelope.json", ++ "deny_record": "/absolute/sanitized-native-receipts/deny-source-record.json", ++ "expected_caller": { ++ "mode": "enforce", ++ "principal": "system:serviceaccount:secrets-engine:secrets-engine", ++ "audience": "flex-auth" ++ } ++} +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 +@@ -184,7 +184,7 @@ + + ```task + id: FLEX-WP-0024-T06 +-status: wait ++status: progress + priority: high + ``` + +@@ -197,3 +197,43 @@ + source review. This task owns the producer-side native gate; SECRETS-WP-0014 owns + consumer admission/enforcement. The old fixture completion never established this + gate, so the workplan is reopened rather than claiming it already passed. ++ ++ ++2026-10-16 owner native preparation: T06 is `progress` because Access still owns ++signer workload declaration, artifact/provenance and source/provider-join preparation; ++no unqualified cross-owner wait is claimed. Read-only actual release metadata and ++bounded proposal are in the Custodian ++`docs/changes/netkingdom-target-2026-10-16/access-native-preparation/`. ++The real Platform custody route `flex-auth-decision-envelope-signing-key` exists but ++is draft/nonresolvable; no production kid/public fingerprint, Secret projection or ++custody receipt is admitted. Coordinate that named commitment through CUST-WP-0080 ++instead of inventing a Platform task/approval. SECRETS-WP-0014-T03 owns installed ++consumer native verification and must not form a circular blocking dependency. ++ ++The first proposed native trial only signs the unchanged lifecycle package on the ++existing retained release; `execute-ccr` remains denied. Current service-pilot ++`runtime_admission := false` remains mandatory: the source ServicePilotInputs is ++not yet wired into HTTP, and public service_actor_binding never authenticates the ++caller. Later activation needs a complete reviewed provider join, exact policy and ++operation admission, artifact digests, owner execution/rollback proposal and native ++receipts. Preparation/local synthetic success grants none of those permissions. ++ ++ ++2026-10-16 source-only native preparation adds the existing Secrets PDP ServiceAccount ++to `tenancy.yaml` with evidence in `docs/native-signer-workload.json`, makes the ++retained `flex-auth` audience explicit, pins CI archive fetch to the full source ++revision, and adds offline native signer receipt verification using the existing ++Secrets verifier. No key, trust, image publication or live activation is admitted. ++The managed workload's missing rapp declaration remains explicit migration debt; ++this source binding does not fabricate a package declaration or raise its grade. ++ ++ ++2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual ++HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls ++refuse across every policy, including the old separately named positive fixture; ++that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged. ++Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are ++source evidence only. B2 depends on independently reviewed Approval fact contract ++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. +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 +@@ -22,6 +22,7 @@ + "github.com/netkingdom/access-engine/internal/emission" + "github.com/netkingdom/access-engine/internal/policy" + "github.com/netkingdom/access-engine/internal/registry" ++ "github.com/netkingdom/access-engine/internal/taskexecution" + "github.com/netkingdom/access-engine/pkg/api" + ) + +@@ -397,7 +398,11 @@ + return fail(stderr, err) + } + +- mux := newServeMuxWithCallerAuth(engine, authenticator) ++ serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap()) ++ if err != nil { ++ return fail(stderr, err) ++ } ++ mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs) + if outbox != nil { + mountEmission(mux, outbox) + } +@@ -448,6 +453,10 @@ + } + + func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux { ++ return newServeMuxWithServicePilotInputs(engine, authenticator, nil) ++} ++ ++func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("content-type", "application/json") +@@ -458,6 +467,25 @@ + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } ++ if engine.IsServicePilot() { ++ if serviceInputs == nil { ++ http.Error(w, "service input unavailable", http.StatusServiceUnavailable) ++ return ++ } ++ request, err := taskexecution.DecodeServiceRequest(r.Body) ++ if err != nil || request.Purpose != "" { ++ http.Error(w, "invalid service task request", http.StatusBadRequest) ++ return ++ } ++ input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC()) ++ if err != nil { ++ http.Error(w, "service task input refused", http.StatusForbidden) ++ return ++ } ++ decision, err := engine.CheckServicePilot(r.Context(), input) ++ writeHTTP(w, decision, err) ++ return ++ } + var request api.CheckRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) +@@ -478,6 +506,10 @@ + writeHTTP(w, decision, err) + }) + mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) { ++ if engine.IsServicePilot() { ++ http.Error(w, "service task batch unavailable", http.StatusForbidden) ++ return ++ } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return +@@ -500,6 +532,22 @@ + writeHTTP(w, decisions, err) + }) + return mux ++} ++ ++// 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() { ++ return nil, nil ++ } ++ if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal { ++ return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping") ++ } ++ reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile) ++ if err != nil { ++ return nil, err ++ } ++ return taskexecution.NewServicePilotInputs(reviewer) + } + + func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) { +@@ -615,7 +663,7 @@ + + func writeHTTP(w http.ResponseWriter, value any, err error) { + w.Header().Set("content-type", "application/json") +- if errors.Is(err, decisioncore.ErrRecordNotCommitted) { ++ if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) { + // Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024). + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return +diff --git a/cmd/access-engine/service_pilot_http_test.go b/cmd/access-engine/service_pilot_http_test.go +new file mode 100644 +--- /dev/null ++++ b/cmd/access-engine/service_pilot_http_test.go +@@ -0,0 +1,376 @@ ++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" ++ "sync/atomic" ++ "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" ++) ++ ++func pilotRequest(now time.Time) api.CheckRequest { ++ return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}} ++} ++func pilotEngine(t *testing.T) *decision.Engine { ++ t.Helper() ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ return engine ++} ++func pilotToken(kind string, now time.Time) string { ++ payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()} ++ if kind == "expired" { ++ payload["exp"] = now.Add(-time.Second).Unix() ++ } ++ if kind == "no-expiry" { ++ delete(payload, "exp") ++ } ++ raw, _ := json.Marshal(payload) ++ return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token" ++} ++func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) { ++ now := time.Now().UTC() ++ var reviews atomic.Int64 ++ reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ++ reviews.Add(1) ++ if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" { ++ t.Error("unexpected reviewer route/auth") ++ w.WriteHeader(403) ++ return ++ } ++ var body struct { ++ Spec struct { ++ Token string `json:"token"` ++ Audiences []string `json:"audiences"` ++ } `json:"spec"` ++ } ++ if json.NewDecoder(r.Body).Decode(&body) != nil { ++ w.WriteHeader(400) ++ return ++ } ++ if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) { ++ t.Error("unexpected bounded runtime audience request") ++ } ++ pieces := strings.Split(body.Spec.Token, ".") ++ raw, _ := base64.RawURLEncoding.DecodeString(pieces[1]) ++ var claims map[string]any ++ json.Unmarshal(raw, &claims) ++ principal := taskexecution.ServicePilotPrincipal ++ audiences := []string{"flex-auth"} ++ authenticated := true ++ switch claims["case"] { ++ case "foreign": ++ principal = "system:serviceaccount:other:other" ++ case "alternate-audience": ++ audiences = []string{"access-engine"} ++ case "wrong-audience": ++ audiences = []string{"openbao"} ++ case "unauthenticated": ++ authenticated = false ++ } ++ json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}}) ++ })) ++ defer reviewer.Close() ++ folder := t.TempDir() ++ ca := filepath.Join(folder, "ca.crt") ++ reviewToken := filepath.Join(folder, "reviewer-token") ++ os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600) ++ os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600) ++ engine := pilotEngine(t) ++ private := ed25519.NewKeyFromSeed(make([]byte, 32)) ++ if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil { ++ t.Fatal("signer config") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl")) ++ engine.SetDecisionLog(log) ++ bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal} ++ inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings) ++ if err != nil { ++ t.Fatal(err) ++ } ++ server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs)) ++ defer server.Close() ++ post := func(path string, request api.CheckRequest, token string) (int, []byte) { ++ raw, _ := json.Marshal(request) ++ req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw)) ++ if token != "" { ++ req.Header.Set("Authorization", "Bearer "+token) ++ } ++ 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 ++ } ++ original := pilotRequest(now) ++ status, body := post("/v1/check", original, pilotToken("valid", now)) ++ if status != 200 { ++ t.Fatalf("status=%d", status) ++ } ++ var envelope api.DecisionEnvelope ++ if json.Unmarshal(body, &envelope) != nil { ++ t.Fatal("decode") ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" { ++ t.Fatal("B1 changed policy gate") ++ } ++ if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("actual response signature") ++ } ++ if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) { ++ t.Fatal("original submitted request/context changed") ++ } ++ if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" { ++ t.Fatal("caller provenance") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) { ++ t.Fatal("recorded signed response differs") ++ } ++ if reviews.Load() != 1 { ++ t.Fatal("request was reviewed more than once") ++ } ++ for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} { ++ t.Run(kind, func(t *testing.T) { ++ status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now)) ++ if status == 200 { ++ t.Fatal("invalid source caller accepted") ++ } ++ }) ++ } ++ for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) { ++ r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano) ++ }} { ++ request := pilotRequest(now) ++ mutate(&request) ++ if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 { ++ t.Fatal("invalid public request accepted") ++ } ++ } ++ if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 { ++ t.Fatal("missing caller accepted") ++ } ++ if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 { ++ t.Fatal("batch bypass") ++ } ++ records, _ = log.ReadAll() ++ if len(records) != 1 { ++ t.Fatal("refused request entered decision record") ++ } ++ ++ // Exercise the actual production serve flag/configuration path in a disposable ++ // subprocess of this compiled test binary. Its environment contains only the ++ // synthetic argument handoff; no ambient backend credentials are inherited. ++ seedFile := filepath.Join(folder, "synthetic-seed") ++ os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440) ++ listener, err := net.Listen("tcp", "127.0.0.1:0") ++ if err != nil { ++ t.Fatal(err) ++ } ++ address := listener.Addr().String() ++ listener.Close() ++ registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json")) ++ policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ cliRecord := filepath.Join(folder, "cli-records.jsonl") ++ cliArgs := []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", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"} ++ argsJSON, _ := json.Marshal(cliArgs) ++ 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 child.Start() != nil { ++ t.Fatal("start disposable CLI") ++ } ++ defer func() { child.Process.Kill(); child.Wait() }() ++ ready := false ++ for attempt := 0; attempt < 100; attempt++ { ++ resp, err := http.Get("http://" + address + "/healthz") ++ if err == nil { ++ resp.Body.Close() ++ ready = true ++ break ++ } ++ time.Sleep(20 * time.Millisecond) ++ } ++ if !ready { ++ t.Fatal("disposable production serve path not ready") ++ } ++ raw, _ := json.Marshal(original) ++ cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw)) ++ cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now)) ++ cliResponse, err := http.DefaultClient.Do(cliRequest) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer cliResponse.Body.Close() ++ var cliEnvelope api.DecisionEnvelope ++ if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil { ++ t.Fatal("production serve path refused supported source input") ++ } ++ if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil { ++ t.Fatal("production CLI did not emit signed DENY") ++ } ++ if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) { ++ t.Fatal("production CLI changed original request/context") ++ } ++ cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll() ++ if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) { ++ t.Fatal("production CLI response/record differ") ++ } ++ for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} { ++ if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("non-enforcing task mode accepted") ++ } ++ } ++ if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil { ++ t.Fatal("task pin renamed audience accepted") ++ } ++ missing := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body)) ++ newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req) ++ if missing.Code != 503 { ++ t.Fatal("unwired generic handler accepted task pin") ++ } ++} ++func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) { ++ engine := pilotEngine(t) ++ request := pilotRequest(time.Now()) ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("generic Check bypass") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil { ++ t.Fatal("generic Batch bypass") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil { ++ t.Fatal("generic List bypass") ++ } ++ if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil { ++ t.Fatal("zero input trusted") ++ } ++ folder := t.TempDir() ++ path := filepath.Join(folder, "request.json") ++ raw, _ := json.Marshal(request) ++ os.WriteFile(path, raw, 0600) ++ var stdout, stderr bytes.Buffer ++ code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr) ++ if code == 0 || stdout.Len() != 0 { ++ t.Fatal("generic CLI emitted task verdict") ++ } ++} ++ ++// The earlier separately named positive fixture policy is not an exception: ++// generic task actions cannot bypass the typed path on any loaded package. ++func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ p.Metadata.ID = "synthetic-legacy-stack-positive-only" ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ request := pilotRequest(time.Now()) ++ result, err := p.Evaluate(context.Background(), request) ++ if err != nil || result.Effect != api.DecisionEffectAllow { ++ t.Fatal("synthetic precondition did not allow") ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := engine.Check(context.Background(), request); err == nil { ++ t.Fatal("separately named generic task allow escaped") ++ } ++ if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task batch escaped") ++ } ++ if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil { ++ t.Fatal("generic task list escaped") ++ } ++ raw, _ := json.Marshal(request) ++ recorder := httptest.NewRecorder() ++ req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw)) ++ newServeMux(engine).ServeHTTP(recorder, req) ++ if recorder.Code != 503 { ++ t.Fatal("separate synthetic generic HTTP task route did not refuse") ++ } ++} ++ ++func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) { ++ now := time.Now().UTC() ++ adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}}) ++ if err != nil { ++ t.Fatal(err) ++ } ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ for _, accidentalAllow := range []bool{false, true} { ++ t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) { ++ p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md")) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n" ++ } ++ engine, err := decision.NewEngine(registry.NewStore(), p) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if accidentalAllow { ++ engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic") ++ } ++ log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl")) ++ engine.SetDecisionLog(log) ++ if _, err := engine.CheckServicePilot(context.Background(), input); err == nil { ++ t.Fatal("B1 released unsupported result") ++ } ++ records, err := log.ReadAll() ++ if err != nil || len(records) != 0 { ++ t.Fatal("withheld result entered recorder") ++ } ++ }) ++ } ++} ++ ++func TestServicePilotChildCLI(t *testing.T) { ++ raw := os.Getenv("ACCESS_B1_SERVER_ARGS") ++ if raw == "" { ++ return ++ } ++ var args []string ++ if json.Unmarshal([]byte(raw), &args) != nil { ++ os.Exit(2) ++ } ++ os.Exit(run(args, os.Stdout, os.Stderr)) ++} +diff --git a/internal/decision/engine.go b/internal/decision/engine.go +--- a/internal/decision/engine.go ++++ b/internal/decision/engine.go +@@ -20,6 +20,7 @@ + "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" + ) + +@@ -153,9 +154,44 @@ + return envelope, nil + } + ++// IsServicePilot selects the existing exact source package, never a request flag. ++func (e *Engine) IsServicePilot() bool { ++ return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion ++} ++ ++// ErrServicePilotUnavailable withholds any result that is not this B1 slice's ++// signed DENY. Provider joins and executable eligibility remain unimplemented. ++var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable") ++ ++// CheckServicePilot records only a signed DENY after authenticating the exact ++// immutable service input. It does not validate Approval facts or grant an act. ++func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) { ++ if !e.IsServicePilot() { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ request, err := input.RequestAt(e.now()) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request) ++ if err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } ++ if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil { ++ return api.DecisionEnvelope{}, err ++ } ++ return envelope, nil ++} ++ + // 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" { ++ return api.DecisionEnvelope{}, ErrServicePilotUnavailable ++ } + decision, err := e.decide(ctx, request) + if err != nil { + return api.DecisionEnvelope{}, err +@@ -169,6 +205,9 @@ + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + decisions := make([]api.DecisionEnvelope, 0, len(request.Resources)) + for _, resource := range request.Resources { + decision, err := e.decide(ctx, api.CheckRequest{ +@@ -193,6 +232,9 @@ + + // 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" { ++ return nil, ErrServicePilotUnavailable ++ } + candidates := e.store.ResourceRefs(request.System, request.ResourceType) + decisions := make([]api.DecisionEnvelope, 0, len(candidates)) + for _, resource := range candidates { +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 +@@ -1,20 +1,27 @@ + package taskexecution + + import ( ++ "bytes" + "context" + "encoding/json" + "errors" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" ++ "io" + "time" + ) + + var ErrServiceInput = errors.New("service task execution input refused") + ++const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot" ++const ServicePilotPackageVersion = "v1" ++ + const ServicePilotMappingVersion = "proposed-service-task-caller/v1" + const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine" + +-// ServicePilotInputs is source preparation only. It installs the explicit ++// ServicePilotInputs installs the exact B1 service input mapping for signed DENY. ++// It does not validate Approval facts or establish executable eligibility. ++// 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 } +@@ -46,6 +53,44 @@ + return r, nil + } + func (s ServiceInput) Caller() callerauth.Record { return s.caller } ++ ++// RequestAt rechecks transport and operation lifetime at the runtime use site. ++// Request() remains a detached source snapshot, never an execution capability. ++func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) { ++ r, err := s.Request() ++ if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ contract, ok := r.Context["execution_contract"].(map[string]any) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ value, ok := contract["operation_deadline"].(string) ++ if !ok { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ deadline, err := time.Parse(time.RFC3339Nano, value) ++ if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return r, nil ++} ++ ++// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded ++// task-only route. It never changes producer canonicalization or legacy input. ++func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) { ++ raw, err := io.ReadAll(io.LimitReader(reader, 524289)) ++ var request api.CheckRequest ++ if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil { ++ return request, ErrServiceInput ++ } ++ decoder := json.NewDecoder(bytes.NewReader(raw)) ++ decoder.DisallowUnknownFields() ++ if decoder.Decode(&request) != nil { ++ return api.CheckRequest{}, ErrServiceInput ++ } ++ return request, nil ++} + + // DeclaredServiceBinding is public immutable metadata to bind at approval issue. + // It is not authentication evidence; Prepare independently verifies its principal. +diff --git a/internal/taskexecution/service_input_test.go b/internal/taskexecution/service_input_test.go +--- a/internal/taskexecution/service_input_test.go ++++ b/internal/taskexecution/service_input_test.go +@@ -1,7 +1,9 @@ + package taskexecution + + import ( ++ "bytes" + "context" ++ "encoding/json" + "github.com/netkingdom/access-engine/internal/callerauth" + "github.com/netkingdom/access-engine/pkg/api" + "testing" +@@ -79,3 +81,33 @@ + t.Fatal("missing owner reviewer accepted") + } + } ++ ++func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) { ++ now := time.Now().UTC() ++ r := serviceRequest(t, now) ++ reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}} ++ adapter, _ := NewServicePilotInputs(reviewer) ++ input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := input.RequestAt(now); err != nil { ++ t.Fatal(err) ++ } ++ for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} { ++ if _, err := input.RequestAt(when); err == nil { ++ t.Fatal("stale/invalid runtime input accepted") ++ } ++ } ++ raw, _ := json.Marshal(r) ++ decoded, err := DecodeServiceRequest(bytes.NewReader(raw)) ++ if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) { ++ t.Fatal("valid decode changed canonical submitted input") ++ } ++ bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)} ++ for _, body := range bad { ++ if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil { ++ t.Fatal("malformed/oversized task request accepted") ++ } ++ } ++} +diff --git a/internal/taskexecution/identity.go b/internal/taskexecution/identity.go +--- a/internal/taskexecution/identity.go ++++ b/internal/taskexecution/identity.go +@@ -1,5 +1,6 @@ +-// Package taskexecution contains proposed source-only identity input validation. +-// It is not registered with HTTP evaluation and grants no executable admission. ++// Package taskexecution contains task input validation, not policy evaluation. ++// The human component remains source-only. The B1 service HTTP path can emit ++// only signed DENY and grants no executable admission. + package taskexecution + + import ( +diff --git a/docs/service-task-execution-contract.md b/docs/service-task-execution-contract.md +--- a/docs/service-task-execution-contract.md ++++ b/docs/service-task-execution-contract.md +@@ -51,9 +51,32 @@ + + Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication. + Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping +-admission must be proved before runtime use. The adapter is not wired into HTTP +-routes, decision engine evaluation or a native executor; source package remains +-hard-closed. A forged public service_actor_binding alone remains insufficient. ++admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only ++when the exact installed service-pilot v1 package is selected. Startup requires ++enforced retained `flex-auth` audience and the one exact Secrets mapping. The ++actual reviewer authenticates once; the detached ServiceInput is rechecked for ++caller/operation expiry at use. The single existing decision engine records only ++a signed DENY on this path; unsigned output or an accidental policy allow is ++withheld before recording. No native executor or Approval fact join is installed. ++A forged public service_actor_binding alone remains insufficient. ++ ++Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate ++`execute-ccr` under any package; the dedicated typed path is required. The exact ++service pin also refuses generic calls and batch requests. This deliberately ++invalidates the older separately named synthetic positive stack fixture on the ++new source. Its positive proof remains historical at Access `ec21518e`, not proof ++for B1. Do not add a fixture exception; a positive successor must use reviewed ++B2/B3 provider joins. The literal service policy false gate is unchanged. ++ ++B1 retains the exact original request context, carried claim, submitted digest ++and producer-owned canonicalization. Its bounded strict JSON decoder refuses ++duplicates, trailing/unknown typed fields and oversized input. The caller's ++Approval blob remains unverified and cannot enable anything. B2 requires the ++Approval owner's fact contract/vector review; B3 additionally requires exact ++server-owned native-act representation and the complete trusted join. Fresh ++provider observations stay out of band: no refreshed context or claim replacement ++may relax strict Secrets request correspondence. Access never invokes a ++credential-carrying Python subprocess, consumes approval or performs the effect. + + ## Exact operation and independent gates + +@@ -129,7 +152,8 @@ + admit grant/baseline/estate or perform an effect. Those are explicit implementation + requirements; a matching predicate and reviewed TokenReview input are insufficient. + The service adapter carries authenticated transport evidence outside the canonical +-request but is not connected to HTTP evaluation/provider validation. A caller's ++request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and ++executable eligibility remain absent. A caller's + service_actor_binding remains a public declaration independently compared by the + adapter, never a substitute for runtime provenance or exact operation authority. +