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:
- 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.
- Acquire the publication/recovery lock and export exact candidate blobs into a
private tree. Launch the verified consumer's
nativeentry 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. - 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. - 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.
- Authenticate a receipt, re-read config/key/credentials and both bundle trees, and verify output inventory again. Interrupted re-pin or rotation refuses.
- 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.