website/docs/forge-execution-closure.md

23 KiB

C0-B execution closure — source complete, admission-gated execution

Schema 2 adds independently accepted policy and release-consumer bundles to the native run adapter. A repository job, an in-repository manifest, a workflow hash, an environment flag, or a status row cannot establish this execution boundary. The shipped source is preparatory tooling; it installs nothing and provides no production pin, key, runtime image, controller service or live-run attestation. No repository flag enables publication: the repository publication entrypoints always refuse, and execution happens only through a separately admitted operator run on the independently installed controller. Admission is dynamic by design: an admitted run holds its credentials only for that run, closes its ledger record and removes credentials afterwards, and between runs admission is closed. The boundary is fail-closed admission — neither a permanently disabled nor a permanently enabled execution switch. The 2026-10-15 accepted publication is live proof that this path ran: verified at the origin and edge by the authenticated publisher, rollback data retained, the rollback path not exercised in that run, and admission closed with no pending records. The earlier schema/quota BLOCK findings have source corrections; their accepted evidence does not attest another candidate or an operational controller. Any S1 BLOCK or incomplete engine scan still fails the policy stage, never becoming a REPORT-only waiver.

Independent bootstrap root and installation prerequisite

Root must independently review, acquire and install the exact bootstrap bytes (scripts/forgejo_closure.py) at /opt/oss-oo/trust/bootstrap.py, using a separate protected controller identity and immutable host/interpreter installation. Record the accepted source repository/commit, bootstrap SHA-256, host/runtime provenance and service launch command outside the candidate repository before first execution. The fixed service command invokes that installed file with python3 -I -B, a fixed /etc/oss-oo/trust/controller.json, and an authenticated operator/scheduler request. The immutable host/kernel, /usr/bin/podman, Python stdlib, external config and isolated controller account are the explicit trust root. Bootstrap cannot verify its own execution into trust; copying a candidate script and running its self-hash is not this installation procedure.

No existing repository-loaded workflow satisfies this boundary. Arbitrary earlier or later steps can request repository secrets or abuse a runner with privileged host access. The preparatory static-publication.yml therefore has no checkout, secret expressions or release execution and always refuses. Its former bytes are retained as a passive fixture. Root must remove privileged credentials and privileged runner access from candidate workflow scope before activation. OFF alone does not make already available secrets safe. Native CI remains a signal with its existing required context; no stronger platform merge guarantee is claimed. Separate controller installation/secret migration is an architectural dependency, not something this source change silently performs.

Exact pins and manifests

Schema-2 pins retain all seven C0 fields (schema, repository, workflow_path, workflow_sha256, events, job_name, required_context) and require exactly two additional fields: policy_bundle and consumer_bundle. Each identity has exactly repository, source_commit, manifest_sha256, runtime_sha256. Commits are full lowercase 40-hex; digests are lowercase 64-hex. Unknown or missing fields, bool-as-version, schema downgrade and a bundle source equal to the candidate are refusals. The same-candidate rule means reviewed policy promotion precedes the subsequent application candidate; it cannot install its own pin.

Runtime identity is the OCI image config/content ID, without sha256:. The config's ordered rootfs diff IDs bind the image layers. Root accepts its provenance independently; the controller selects sha256:<ID> with --pull=never, checks the local image ID, creates the container and checks the actual container image ID before starting it. A candidate-supplied observation is never a pin. Tags, apt, downloads, mutable image resolution and network package installation cannot extend the trusted runtime. Image publication/acquisition is a Root gate.

Manifest keys are exactly schema (integer 2), role (policy or consumer), repository, source_commit, entrypoints, files. Required entrypoint keys:

Role Keys Current entrypoint
policy sast, tests, build scripts/closure-policy.py
consumer native, publication, recovery, commons, verify scripts/closure-consumer.py

All entrypoints must be present Python files. Files are sorted by exact relative path, each with exactly path, Git mode (100644 or 100755), and SHA-256. Paths use an ASCII subset, reject absolute paths, dot/empty segments, trailing dots, backslashes and case aliases. Files must be regular single-link files; directories must have mode 0755 and correspond exactly to file ancestors. Symlinks, hardlinks, devices, omitted/extra files, empty extra directories, ambiguous modes and duplicate/unsorted entries refuse before execution.

There is one canonical byte representation for JSON used by this controller: json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=True, allow_nan=False) encoded as ASCII plus one LF. The decoder requires those exact bytes, rejecting duplicate keys, BOM, alternate spacing/encoding and non-finite values. Manifest digest is over those canonical bytes, stored next to the bundle tree, never inside its own recursive inventory.

The builder deliberately exports the entire exact reviewed Git source tree, not a selective list of guessed imports. It uses ls-tree/cat-file against a full commit, no checkout, archive filters, working-tree files or candidate code. All imports, helpers, test fixtures, rules, exception inventory, coverage policy, build scripts and consumers are thereby included. Root separately verifies the claimed canonical repository/commit; a local builder cannot attest remote origin. All non-source libraries/tools belong to the independently pinned runtime image.

For each role, call the independently installed builder with bundle --source <reviewed repository> --commit <full SHA> --repository <canonical owner/repo> --role <policy|consumer> --entrypoints <canonical JSON file> --destination <new private path> --runtime <independently accepted image ID>. It returns candidate identity values for independent review. It never writes an external pin. A manifest or matching pin carried by an application candidate is powerless.

Controller inputs and ordering

The external canonical controller config is schema 3, with exactly schema, pins, policy, consumer, source_repository, credentials, attestation_key, workspace. The fixed pins table has exactly production and review. Both pins remain schema 2 and bind the same independent bundles, runtime identities, repository, workflow path/hash and job. Production binds only push and its required context; review binds only pull_request and its required context. Neither a request nor a candidate can supply a pin or change an event/context. All path fields are absolute and controller-owned; policy/consumer contain manifest.json and tree/. Root installs the config atomically, outside candidate write scope. attestation_key is a regular 0600 file with at least 32 random bytes. This block creates/reads no real key: tests use an explicitly synthetic constant in private fixtures.

credentials is the one fixed protected active-credentials JSON string map. The operating package selects a Root-owned mode-specific template and materializes only that profile at the fixed path. The bootstrap rejects missing or extra keys before any native/container action. It supplies authorized scheduler context (repository, protected ref, candidate SHA, globally unique controller run id/attempt) plus exactly the selected target's credential keys. Verify has only a repository read token and no production credentials. These are controller scheduler inputs, not inherited CI environment claims. The request's selected mode is translated to the existing dispatch context by the pinned consumer. The native CI identity stays separate in the proof. Root must provide a durable unique run allocator, authenticated dispatch, protected audit/receipt storage and one controller host. Publication and recovery share .controller.lock; overlapping local requests fail busy. Multiple hosts are not supported by a local flock and must not be activated.

source_repository is a separately hydrated credential-free local Git object store owned by the controller. It must contain the candidate, installed Commons release and predecessor commits needed for data-only compatibility/history reads. No candidate .git, hooks, config, credentials, alternates or filters are copied into it. Hydration is an external read-only acquisition step, not a consumer fetch with privileged credentials. Missing objects refuse; no artifact bypass or historical raw-SHA recovery route is added.

The controller performs this actual sequence:

  1. Validate config schema 3, the two whole schema-2 external pins, the exact mode/target/credential profile and protected request identity, both manifests and trees, and both pinned images, before any bundle entrypoint/import.
  2. Acquire the publication/recovery lock and export exact candidate blobs into a private tree. Launch the verified consumer's native entry in its image to read protected main and the full C0 native run proof (all pages and re-read). Recovery first reads/validates the open ledger entry and requires its commit equal the protected scheduler dispatch SHA, then checks fresh main and native CI. Failed sync, stale/foreign/closed records or missing preflight data stop before SAST/tests/build and without any endpoint call.
  3. Select application data and trusted policy files into one complete input snapshot. Fixed host /usr/bin/git, with no hooks, templates or system/user configuration, creates neutral metadata for fixture clones; its exported committed tree must match the selected bytes/modes exactly. Inventory this whole snapshot including that metadata. Run data-only SAST, the complete fixed test commands, and each of three builds in a fresh container. Remove each container and all its processes before starting the next. Every command must exit zero. No key or credential is mounted. Application-running containers receive the snapshot, bundle and raw candidate only as read-only binds; they do not receive the controller's source Git object store.
  4. Re-read protected main/native provenance and require identical native records. Recovery repeats its read-only preflight and requires the same record number and canonical complete payload digest; drift cannot reach the final consumer. All application containers have already been removed. Recheck the complete input inventory and read the exact generated artifact inventory. Commons then prepares its deterministic module packet in a pinned consumer container with network disabled, a readonly candidate data tree and no credential mount; that packet joins the authenticated artifact inventory. Verify compares the same full current PR identity and native payload at the pre-build, post-build and final consumer boundaries, with current main equal to the bound base SHA. It reads no branch-protection administration route.
  5. Authenticate a receipt, re-read config/key/credentials and both bundle trees, and verify output inventory again. Interrupted re-pin or rotation refuses.
  6. Start the verified consumer in its pinned image with only the authenticated private channel, readonly artifacts and Git object data, protected credentials, and its own writable temporary/output space. Candidate executables are not mounted. The consumer re-verifies the HMAC and native server data before using artifacts or contacting the production endpoint. Recovery re-reads the selected record once more, requires its number/payload digest match the authenticated pre-build selection, and repeats dispatch, fresh main and native proof checks before endpoint work. No fallback selects a different record. The existing endpoint candidate/ticket/runtime binding still precedes artifact consumption and any positive close.

Container flags include read-only image, ignored image volumes, no capabilities, no-new-privileges, private PID/IPC, resource/output bounds, narrow bind mounts and fixed work/temp directories. Policy stages use --network=none; only the pinned consumer has networking. --unsetenv-all plus a fixed environment clears caller PYTHONPATH, preload variables, tool options and credentials. Python uses -I; the only bootstrap-added import directory is /bundle/scripts. No container socket, host home or unrelated host mount is exposed to application tests. Only containers created by this invocation are force-removed on exit/failure.

Test policy versus application input

closure-policy.json is included in the pinned policy bundle. It fixes commands, checkers and application selection paths. Ordinary candidate input may replace site/, Commons migrations and explicitly named Commons application modules. The explicit module list includes provider-neutral actors. Identity-v1 schema, preparer and manifest, the fixed Commons schema-profile JSON, and all identity test/support files remain pinned policy inputs. The fixed Commons and UI commands include their complete foundation, activity and growth regression modules. Changes to scripts, test files/support or other service policy require independent policy re-pin; replacement is rejected, not silently selected. The host prepares the selected snapshot before starting any application. The same-user writable copy formerly used for test execution is removed. Actual checks, support code, selected application bytes and neutral Git metadata stay behind read-only mount boundaries throughout application execution. Chmod is not that boundary; the entrypoint also refuses a writable input mount. A candidate process cannot reach a writable alias of those files. The trusted host and kernel remain prerequisites.

Each command receives fresh writable /work, /tmp and /output only. Ordinary Git fixtures clone into temporary space. check-site.sh accepts the explicit SITE_CHECK_OUTPUT scratch path while retaining dist as its local default. Builds write /output/tree; the host removes the container before moving that tree into final artifacts. Neither a later test nor a later build can access a previous command's scratch, output or process namespace.

Application tests necessarily execute hostile program behavior; a program can misbehave within a test process. Their logs are not an independent trust source. The outer pinned orchestrator selects and records the complete fixed command list and process exit results. This proves file/execution identity and isolation from controller credentials; it does not prove an independently observed semantic test verdict against arbitrary hostile application imports sharing the unit-test interpreter. Such imports can affect that process's assertions or exit. Fresh containers prevent that process from changing a later command's inputs or surviving into its process namespace. SAST separately parses raw candidate blobs with the pinned S1 runner/rules/engine, never importing candidate helpers to decide coverage or success. SAST findings are not inferred from a candidate-supplied receipt. New native .oss-oo/.oss workflow discovery still requires the separate S1 coverage update before that platform feature rolls out.

CI=true makes mandatory runtime tools fail rather than silently skip. The policy image needs Python 3.12, Git, Node 24, pinned OpenGrep at /opt/oss-oo/bin/opengrep, PyYAML, unshare support, Foundry and the exact offline solc/npm cache. Npm is forced offline with its cache inside the immutable image. OpenGrep's pinned onefile launcher expands files larger than the unchanged 32 MiB file limit. The policy explicitly selects an immutable pre-expanded cache using sast_cache in the protected policy JSON: root and manifest paths plus manifest SHA-256 671042acd4f5135d4c77536e54227685d16368ede5da2da41dfbbf5d48c606c8. The manifest lives outside the cache tree and binds all 102 files (256775858 bytes), modes and hashes, with exactly their implicit 0755 directories. S1 verifies that complete read-only inventory before every engine child. The original engine launcher SHA remains required. Only explicit CLI options select this cache; ambient XDG_CACHE_HOME and adjacent manifests grant no trust. The local S1 default remains cache-free. Its fixed test command carries the same explicit options and executes every existing case, including a separate default check.

The consumer image needs Python 3.12, Git, SSH and CA roots. Locally prepared images are test inputs, pending independent image/provenance acceptance. A missing tool, unavailable nested namespace, S1 BLOCK or incomplete engine scan therefore refuses. The tested cached Python fixture image is not offered as a production policy image.

Native proof and protected consumption

The adapter keeps C0-R1's triggering commit versus workflow source distinction, internal run id versus display index, complete pagination, newest-run rule and single-run re-read. Schema 2 also binds positive job attempts and rejects duplicate job ids. verify_native alone never authorizes a protected consumer.

The private receipt schema is 3, with production kind forge-execution-hmac and MAC prefix oss-oo/execution/v3 followed by one NUL. Read-only review uses kind forge-review-hmac and a separate prefix oss-oo/review/v1 followed by one NUL. There is no receipt-schema downgrade or unprefixed MAC fallback. Production plan, publication, recovery and Commons consumers all reject a review receipt. This is HMAC-SHA256 with Python's standard library, not a public-signature attestation. It binds the canonical whole pin digest, complete native record (repository, candidate, event, source, run/jobs/attempts and status linkage), both accepted bundle identities, fixed successful stage identities/transcript hashes and exact artifact inventory. It also binds the full prepared-input inventory digest and every command's index, exact argument list, timezone, zero exit code and transcript hash in order; the stage hash covers its whole command sequence. SAST has one command and build has three. The selected input digest is execution identity, not an application-supplied claim. Policy bundle identity independently binds the required command list. For recovery it additionally binds the pre-build ledger record number and full canonical payload digest (recovery_record); other modes carry JSON null. The controller and pinned consumer share the independently protected key. No candidate can supply that key or use an in-repository receipt as authority. The whole typed request additionally binds candidate, mode, target, credential profile, immutable repository ID, protected scheduler run ID/attempt and the exact PR number/head/base refs and SHAs for verify. Review also binds hashes of the complete current PR object and native event payload. A review result grants no static or Commons publication gate; it must not be reused after merge.

The mode/key contracts, native Commons adapter, separate append-only ledger and remaining operator gates are specified in the native controller contract.

forgejo_trust.verify accepts legacy schema 1 only for its explicitly staged transport interface; Forge.trusted_ci requires schema 2 and a real Prepared handoff. forgejo_publication.run and forgejo_recovery.run refuse a missing handoff before ledger/endpoint work. Their artifact path imports only the pinned consumer's checker, validates the three prebuilt trees/descriptor, and never invokes candidate build scripts or release helpers. Legacy local build functions remain fixture helpers, unreachable from protected entries. Publication retains fresh native/main checks at its existing transition boundaries and records full schema-2 provenance. Recovery retains E5's exact recorded candidate == dispatch == fresh main binding, endpoint candidate/ticket/runtime binding, fail-closed ledger sync, loss/rollback rules and predecessor data reads. The final recovery entry performs one native trust read before endpoint work. The new controller separately repeats its record/main/native preflight after the isolated build as documented above; this is added by C0-B, not attributed to the unchanged E5 engine. Older records retain the evidence-bound manual operator path.

Activation evidence requirements (defined before the first admitted run)

This section records the evidence requirements as written before the first admitted live run. The 2026-10-15 accepted publication has since demonstrated the closure path end-to-end on the current infrastructure; the per-run admission, custody and reconciliation gates below remain the specification for every future run.

Independent source review and final integration precede Root's acceptance of new source/manifest/runtime identities. UI/identity changes may alter build/check/test inventories; rebuild the full resulting bundles after their integration. Root must separately prove the immutable bootstrap/host/runtime installation and credential withdrawal from candidate runners, scheduler/run allocation, local serialization, source hydration and protected evidence storage. Then obtain real native push execution proof (and deployed target-source evidence before enabling target events), bind a complete passing S1 run to that exact candidate, and satisfy the unchanged release, backup/restore, OFF-to-ON, endpoint, HTTP, ledger and rollback gates.

Local tests include hostile manifests, checker/import replacement, omitted dependencies, extras/aliases/symlinks/modes, joint candidate pin changes, runtime mismatch, downgrade, native identity changes and pre-execution sentinels. python3 scripts/test-forgejo-closure.py --podman-smoke executes a real isolated fixture with only a cached image and synthetic key/credentials, tests import/env shadowing and attempted credential/network access, then authenticates and consumes fixture artifacts. Its native server and website policy bodies are fixture code; it is real container isolation evidence, not a live native/SAST/release verdict. --readonly-smoke --fixture-runtime <local image ID> additionally checks real write/chmod/unlink/rename refusal for protected inputs, fresh scratch/output, ordinary repository checks, Git fixtures and three deterministic builds. It neither changes a production runtime pin nor creates a publication approval.