diff --git a/.forgejo/workflows/purpose-overlay-image.yaml b/.forgejo/workflows/purpose-overlay-image.yaml new file mode 100644 index 0000000..633d8a8 --- /dev/null +++ b/.forgejo/workflows/purpose-overlay-image.yaml @@ -0,0 +1,65 @@ +name: Publish exact advisory-purpose overlay candidate +on: + push: + branches: [main] + paths: + - ".forgejo/workflows/purpose-overlay-image.yaml" + - "deploy/purpose-overlay/**" + - "tools/purpose_overlay/**" + - "informed_decision/policy.py" + workflow_dispatch: +concurrency: + group: informed-decision-purpose-overlay + cancel-in-progress: false +permissions: + contents: read +env: + REGISTRY: forgejo.coulomb.social + IMAGE_NAME: coulomb/informed-decision + DOCKER_HOST: tcp://127.0.0.1:2375 +jobs: + publish-candidate: + runs-on: container-build + steps: + - name: Verify and publish single-module overlay + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -eu + umask 077 + REF="${GITHUB_SHA:?exact source revision required}" + echo "${REF}" | grep -Eq '^[0-9a-f]{40}$' + test "${GITHUB_REPOSITORY}" = "coulomb/informed-decision" + if [ -z "${REGISTRY_USER:-}" ] || [ -z "${REGISTRY_TOKEN:-}" ]; then + echo "Admitted repository registry publication capability is unavailable; no borrowed credential is permitted." >&2 + exit 2 + fi + BUILD_ROOT="$(mktemp -d)" + export DOCKER_CONFIG="${BUILD_ROOT}/docker-config" + cleanup() { rm -rf "${BUILD_ROOT}"; } + trap cleanup EXIT + mkdir -p "${BUILD_ROOT}/source" "${BUILD_ROOT}/overlay" "${DOCKER_CONFIG}" "${HOME}/bin" + wget -qO "${BUILD_ROOT}/source.tar.gz" "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${REF}.tar.gz" + tar xzf "${BUILD_ROOT}/source.tar.gz" -C "${BUILD_ROOT}/source" --strip-components=1 + echo "9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62 ${BUILD_ROOT}/source/informed_decision/policy.py" | sha256sum -c - + cp "${BUILD_ROOT}/source/informed_decision/policy.py" "${BUILD_ROOT}/overlay/policy.py" + cp "${BUILD_ROOT}/source/deploy/purpose-overlay/Containerfile" "${BUILD_ROOT}/overlay/Containerfile" + 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 + export PATH="${HOME}/bin:${PATH}" + echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin + BASE="${REGISTRY}/${IMAGE_NAME}@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173" + docker pull "${BASE}" + IMAGE="${REGISTRY}/${IMAGE_NAME}:purpose-overlay-${REF}" + docker build --network none --pull=false --build-arg "SOURCE_REVISION=${REF}" --file "${BUILD_ROOT}/overlay/Containerfile" --tag "${IMAGE}" "${BUILD_ROOT}/overlay" + docker run --rm -i --network none --read-only --tmpfs /tmp:rw,noexec,nosuid,size=8m --entrypoint python "${IMAGE}" - < "${BUILD_ROOT}/source/tools/purpose_overlay/check_installed.py" + INSTALLED_POLICY_SHA="$(docker run --rm --network none --read-only --entrypoint sha256sum "${IMAGE}" /opt/venv/lib/python3.12/site-packages/informed_decision/policy.py | awk '{print $1}')" + test "${INSTALLED_POLICY_SHA}" = "9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62" + OUTPUT="$(docker push "${IMAGE}")" + printf '%s\n' "${OUTPUT}" + DIGEST="$(printf '%s\n' "${OUTPUT}" | tr '\r' '\n' | awk '{for(i=1;i<=NF;i++)if($i~/^sha256:[a-f0-9]{64}$/)print $i}' | tail -1)" + printf '%s' "${DIGEST}" | grep -Eq '^sha256:[a-f0-9]{64}$' + echo "published=${REGISTRY}/${IMAGE_NAME}@${DIGEST}" + echo "source_revision=${REF}" + echo "policy_sha256=9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62" + echo "candidate_only=true; native rollout, backup and restore require exact supervisor approval" diff --git a/deploy/purpose-overlay/Containerfile b/deploy/purpose-overlay/Containerfile new file mode 100644 index 0000000..80d6b6f --- /dev/null +++ b/deploy/purpose-overlay/Containerfile @@ -0,0 +1,7 @@ +FROM forgejo.coulomb.social/coulomb/informed-decision@sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173 +ARG SOURCE_REVISION +LABEL org.opencontainers.image.revision="$SOURCE_REVISION" \ + org.opencontainers.image.base.digest="sha256:7f5c6cb29c9f70a026bfe30e72934274ddc69261676cf6d476f81dd32812d173" \ + social.coulomb.source.policy.sha256="9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62" \ + social.coulomb.release.scope="INFD-WP-0009-T04/purpose-overlay-v1" +COPY policy.py /opt/venv/lib/python3.12/site-packages/informed_decision/policy.py diff --git a/docs/evidence/2026-10-11-purpose-publication-preparation/README.md b/docs/evidence/2026-10-11-purpose-publication-preparation/README.md new file mode 100644 index 0000000..3d9f8ef --- /dev/null +++ b/docs/evidence/2026-10-11-purpose-publication-preparation/README.md @@ -0,0 +1,11 @@ +# INFD-WP0009-T04 source publication preparation + +Stable trial/proposal: `INFD-WP-0009-T04/purpose-overlay-v1`. Source correction is committed1e0df2f; exact policy.py SHA9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62. The earlier b8b589 local overlay and its13 synthetic container/10 purpose checks remain historical; no registry/native result is backfilled into that receipt. New publication retains the exact7f5c6cb running base and replaces only its installed policy.py, adding source/scope labels. It does not build the whole current repository, migrate storage or change dependencies/configuration. + +The public Forgejo repository has Actions enabled and0 recorded jobs at discovery; remote main caab9a90. Fleet User Engine/State Hub/PolicyNexus source already uses the admitted `container-build` runner and normal `REGISTRY_USER`/`REGISTRY_TOKEN` bindings. Availability to this repository is not proven by sibling success. The proposed owner workflow fails closed when its admitted bindings are absent and does not borrow a workstation Docker configuration, PAT or sibling secret. It fetches the exact pushed public Git archive anonymously, checks full source SHA and exact policy hash, builds only a private two-file context with network disabled, checks installed source/ten synthetic purpose forms, and publishes only a full-source-revision candidate tag to the owner's image repository. No moving main/latest tag or cluster operation is included. Registry authentication exists only inside the normal job's private temporary Docker custody, removed on exit. + +Normal source commit/consistency triggering this owner job is construction/publication preparation, not native release approval. Observe the exact Actions head SHA/status and actual pushed manifest, source/scope labels, base layer prefix and installed module before making any Deployment pin. Preserve job failure if registry capability is not admitted; route that concrete prerequisite to the existing platform owner, without requesting/reading a new credential or borrowing one. Source preparation itself remains useful and does not imply admitted execution. + +Publication gates before image rollout proposal: exact registry digest and source labels; original immutable base layers/config preserved except labels and one policy.py layer; installed module hash and all10 purpose variants; all13 container/schema/restart/synthetic restore checks; fresh unfiltered vulnerability scan/SBOM of actual published image; exact currently serving config/schema5/policy packages/audiences/credentials/network/PVC unchanged in source and native dry-run/diff. Local synthetic recovery is never native production-data recovery. + +Runtime rollout remains separate founder-approved supervised authority. The attached protected protocol makes consistent native backup and isolated actual-data restore part of the same concrete conditional release proposal, gated by exact founder approval; neither operation has occurred in this preparation. No source acceptance, existing config-only approval7db8485 or publication job result substitutes for it. diff --git a/docs/evidence/2026-10-11-purpose-publication-preparation/native-backup-restore-protocol.md b/docs/evidence/2026-10-11-purpose-publication-preparation/native-backup-restore-protocol.md new file mode 100644 index 0000000..9840eec --- /dev/null +++ b/docs/evidence/2026-10-11-purpose-publication-preparation/native-backup-restore-protocol.md @@ -0,0 +1,33 @@ +# Protected native backup/restore and image-only release protocol + +Prepared protocol for `INFD-WP-0009-T04/purpose-overlay-v1`. No actual native backup, data export, restore, workload change or approval is recorded here. Candidate registry image identity remains a prerequisite; only after actual artifact proof may the founder approve one exact conditional protocol/rollout packet. The config-only7db8485 approval is outside this scope. + +## Preconditions and protected custody + +Freeze exact candidate and prior7f5c6cb image digests, source/installed policy hashes, current manually managed Argo Application/revision, runtime ConfigMap6c221878d051, Deployment UID/template/spec, schema5, policy package pins, database path `/data/private/review.sqlite`, PVC binding, replicas1/Recreate and stable keys for intended verification. Confirm no concurrent writer or custody change. Use fixed metadata; never fetch Secret objects or token values. A changed image/config/schema/owner invalidates the packet. Fresh resources/headroom and exact source/native server-dry-run/diff remain required. + +Backups are private evidence containing real decision records, not logs or packet attachments. Allocate a unique backup directory under existing protected PVC custody, owned10001, mode0700, snapshot file0600, no symlink/hardlink/reused output path. An off-cluster copy belongs only in the already established ignored `.private-backups/` custody with0700/0600 permissions; no Git, StateHub, public object store or terminal-content dump. Record only schema, integrity pass/fail, bounded table/state counts and protected receipt identifier; retain byte digests in protected custody if needed for internal copy identity. No credential, record payload or token fingerprint enters public evidence. + +## Approved online compatibility drill + +Within the explicit founder-approved action, run the existing base image's `informed-decision-admin backup --db /data/private/review.sqlite --output ` in the actual serving workload. This uses SQLite's backup API for a transactionally consistent snapshot; copying a live main database while ignoring WAL is prohibited. Suppress raw failures/output and retain only sanitized status. Observe schema/integrity and count-only metadata from the resulting snapshot, not record content. + +Transfer that consistent snapshot only into protected off-cluster custody through the existing authorized operator lane. The copy is real production data even in build mode. Verify private permissions, unique identity and protected internal digest agreement. Do not export mounted workload credentials or directory trees; copy only the exact snapshot. + +Create a separate private disposable restore volume/directory; retain the original snapshot unchanged. Run the exact published candidate with network disabled, no production ConfigMap/token mounts, read-only image and only that isolated volume writable. Override the service entrypoint with the owner's count-only administrative inspection/integrity check; do not start HTTP listeners, audit delivery, requester exchanges or approval operations. The current candidate changes policy.py only and retains the schema/store module bytes; still prove schema5, SQLite integrity and exact bounded counts before/after opening the restored copy. Refuse any migration/downgrade, lost/changed counts, unexpected write class or unknown outcome. Keep pending/uncertain submission and outbox states; never replay, discard or mark them delivered to green the drill. Cleanup only the disposable copy/volume after verified receipt; retain the original protected backup for the approved recovery interval. + +This step is actual native-data compatibility evidence once performed. Its previous synthetic fixture checks remain mechanism evidence and cannot be renamed native success. A failed drill stops rollout and remains a failed/unverified outcome in the same trial. + +## Fresh cutover snapshot and single-writer rollout + +After successful actual-data compatibility checks, create another unique SQLite-consistent online snapshot immediately before the image cutover, retaining its point-in-time schema/integrity/count receipt. This is an online transactional snapshot, not a claim that dispatchers or human writes were quiesced. Every later committed record remains in the live database; same-schema rollback retains that database and never restores the earlier snapshot over it. The narrow image-only update does not need dispatcher/configuration/PVC/temporary-pod changes or a schema cutover. If fresh verification shows a migration or any data-restoration requirement, stop and refine the proposal rather than applying this protocol. + +The existing Recreate rollout drains/stops the single old writer before starting the new image on the same PVC; the inherited single-writer lock independently refuses concurrent serving processes. Do not add an admin pod, scale-to-zero, dispatcher stop, network-policy rewrite or snapshot overwrite as an improvised image-only step. The existing owner admin backup operation runs inside the admitted serving workload, and its separate protected restore drill runs off-cluster with network disabled and no workload credentials. Those exact custody operations must be enumerated in the founder packet alongside the one Deployment image change. + +Once backup/compatibility/preflight gates pass, manually pin the existing Argo Application to the exact reviewed source revision and sync only Deployment/informed-decision with prune false, keeping the current immutable runtime ConfigMap and all other resources. The source patch changes only the image in admission/rendered Deployment; no Secret, PVC, policy, database or dispatcher change is included unless separately enumerated in the final proposal. Observe five-minute readiness and exact image/config/schema/pins. Verify native lifecycle binding using stable keys and an isolated installed-client probe based on a real provider response contaminated with advisory purpose; prove refusal before any write. Do not contaminate the live PDP, create human approvals, read credentials through CLI, or replay unknown external outcomes for testing. + +## Same-schema rollback and evidence retention + +On startup/readiness/binding/refusal failure, retain current database, cutover backup and every post-cutover evidence/uncertain outcome. Restore only the prior7f5c6cb image through the same manual Argo owner path, retaining schema5 and current DB/configuration; never restore an old snapshot over current live evidence just to roll the image back. Verify old image/Ready1, same schema/counts and prior behavior, and reconcile any external effects using existing lifecycle/audit ownership. The narrow overlay's identical store/schema/dependency bytes permit this image-only rollback; it is not permission for a schema downgrade or data loss. + +Record original/approved/executed source and image revisions, backup/drill/cutover/rollout timestamps, exact disposition and sanitized outcomes in the existing supervised trial receipt. EUR0 incremental infrastructure estimate uses existing service/storage/operator lane; protected temporary copy/storage consumption and15–30minute recovery time remain operational costs. No EUR incident-loss cap or autonomy grant is inferred. TaskT04 cannot close until actual published image, approved native compatibility and release plus post-release refusal/binding evidence all exist. diff --git a/docs/evidence/2026-10-11-purpose-publication-preparation/validation.json b/docs/evidence/2026-10-11-purpose-publication-preparation/validation.json new file mode 100644 index 0000000..8e7124f --- /dev/null +++ b/docs/evidence/2026-10-11-purpose-publication-preparation/validation.json @@ -0,0 +1,31 @@ +{ + "proposal_id": "INFD-WP-0009-T04/purpose-overlay-v1", + "source_policy_revision": "1e0df2f", + "source_policy_sha256": "9d7c1be4cb6199d14ef0aaead5f85951f45b0ccbd7c800a7fde922a253566b62", + "workflow_shell_syntax": "passed", + "installed_script_checked_against_retained_local_b8b5_candidate": "10purposevariants passed; this is local unpublished proof, not new registry/native result", + "discovery": { + "public_repo": true, + "has_actions": true, + "native_action_jobs_before_new_workflow": 0, + "remote_main_observed": "caab9a90ec0ba2da36ede262a94b9cc17f794f38", + "runner_pattern": "existingfleetcontainer-build", + "normal_binding_names": [ + "REGISTRY_USER", + "REGISTRY_TOKEN" + ], + "repository_binding_admission_observed": false + }, + "actual_registry_candidate_available": false, + "native_backup_executed": false, + "native_restore_executed": false, + "native_workload_changed": false, + "remaining_gates": [ + "root applies source/commit/consistency; normaladmittedCIjob maypublish exactcandidate", + "observe exactnativejob/source/packagepublication or actualmissingadmissionfailure", + "registry digest/installedsource/baseprefix/config/10purpose/13container checks and fullscanSBOM", + "exactimageonlyArgo source/diff/preflight and protecteddata protocol staged forfounderreview", + "actualfounderdisposition before nativebackup/restore andimagecutover; no oldconfigapproval reuse", + "actualadmittedbinding/refusal/recovery evidencebeforeT04done" + ] +} diff --git a/tools/purpose_overlay/check_installed.py b/tools/purpose_overlay/check_installed.py new file mode 100644 index 0000000..0736145 --- /dev/null +++ b/tools/purpose_overlay/check_installed.py @@ -0,0 +1,21 @@ +import copy,json,time +from datetime import datetime,timezone +from informed_decision.policy import PolicyClient,CONTRACT,submitted_digest +pin='sha256:'+'b'*64 +req={'id':'image-purpose-fixture','tenant':'tenant:platform','subject':{'id':'synthetic','type':'service','tenant':'tenant:platform'},'resource':{'id':'memo:fixture','type':'memo','system':'informed-decision','tenant':'tenant:platform'},'action':'register','context':{}} +class Transport: + def request(self,*args,**kwargs): + now=time.time(); stamp=lambda t:datetime.fromtimestamp(t,timezone.utc).isoformat() + b={**copy.deepcopy({k:req[k] for k in ('tenant','subject','action','resource','context')}),'request_digest':'sha256:'+'e'*64,'submitted_request_digest':submitted_digest(req)} + if self.purpose!='ABSENT':b['purpose']=self.purpose + d={'id':'synthetic-decision','contract_version':CONTRACT,'request_id':req['id'],'effect':'allow','matched_policy_version':'v1','subject':req['subject'],'resource':req['resource'],'binding':b,'lifetime':{'kind':'ttl','not_before':stamp(now-1),'expires_at':stamp(now+60)},'provenance':{'policy_package':'fixture','policy_version':'v1','policy_package_digest':pin,'registry_snapshot_digest':'sha256:'+'c'*64,'evaluator':'flex-auth/fixture','decision_time':stamp(now)}} + return 200,d +checks=[] +for purpose in ('ABSENT','','readback','enforcement','unknown',None,False,0,[],{}): + t=Transport();t.purpose=purpose + c=PolicyClient('https://fixture.invalid',lambda:'synthetic-only',package='fixture',version='v1',package_digest=pin,transport=t) + o=c.check(req) + if purpose in ('ABSENT',''):assert o.outcome=='allow';o.require_current(time.time()) + else:assert o.outcome=='invalid_decision' and o.decision_json is None + checks.append(str(purpose)) +print(json.dumps({'installed_image_purpose_variants':len(checks),'binding_forms_allowed':2,'advisory_or_malformed_refused':8,'synthetic_only':True,'native_provider':False}))