> Documentation version: 3. [HTML page](security-model.html)

<a id="automation-trust-model"></a>

# Automation trust model

This chapter states who may do what in the workflows and shared actions this
manual describes, and why each boundary is drawn where it is. It is normative
for the shared actions in roc-automation: a change to their behavior or
permissions must keep these boundaries and arrive with regression tests for
them.

Two rules carry most of the weight:

- A job that runs repository or candidate code has no write access.


- A job with write access runs only reviewed code at a full commit SHA, and
  handles candidate output as inert files.

Read-only permissions limit repository authority; they are not a process sandbox
or proof that build outputs are benign. Code in a job can read its workspace,
use its network access and influence its outputs. Run floating compilers and
consumer code on disposable hosted runners without sensitive workload data or
publication credentials. Clearing child token variables reduces exposure but
does not isolate same-job processes from one another. Reviewed inputs, exact
source/artifact binding and consumer-facing tests remain separate controls.

<a id="risk-execution-authority"></a>

## Risks and controls for execution authority

A compiler, example or prerequisite script can be malicious or compromised,
and generated output can try to cross into a job that can publish or edit
issues. Separate execution from authority. Use read-only disposable test jobs,
reviewed pinned controllers in write jobs, inert data handoffs and validation
of source/run/artifact identities before writes. Security regressions exercise
wrong identities and rejected transitions.

<a id="the-three-workflows"></a>

## The three workflows

| Workflow and job | Token access | Runs repository code |
| --- | --- | --- |
| ` ci.yml ` test | ` contents: read ` | Yes, including pull-request code |
| ` release.yml ` build and test | ` contents: read ` | Yes |
| ` release.yml ` publish | ` contents: write `, behind a protected environment | No: it runs ` actions/publish-release ` on artifacts produced by earlier jobs |
| Shared ` release-package.yml ` build | ` contents: read ` | Yes: one prerequisite program the caller names, the package’s ` check ` task, its examples and the compiler, beside the ` release-package ` utility its lock fixes |
| Shared ` release-package.yml ` test | ` contents: read ` | The repository’s Blueprint, to provision the locked utility; otherwise only the archived examples from the build job’s artifact, against the bundles from the same artifact |
| Shared ` release-package.yml ` publish | ` contents: write `, ` id-token: write `, ` attestations: write `, behind the ` release ` environment | No: roc-automation’s own checkout at the caller’s pinned SHA, ` actions/attest ` and ` actions/publish-release ` on the build job’s artifact |
| ` release.yml ` site deploy | ` pages: write `, ` id-token: write ` | No: it deploys an artifact built by a read-only job |
| ` nightly.yml ` test | ` contents: read ` | Yes: published examples and optional default-branch native prerequisites |
| ` nightly.yml ` selection and aggregation | ` contents: read ` | No: pinned shared runner and inert release/result metadata |
| ` nightly.yml ` report | ` issues: write ` | No: it runs ` actions/nightly-report ` |

The publish job accepts only a manual dispatch on the default branch, uses the
event commit throughout, and consumes the artifacts of its own run. It never
checks out the repository, so a script changed in the tested commit cannot run
with release credentials.

The nightly compatibility workflow has no contents, pull-request, actions or
status write access in any job. It cannot commit, merge, dispatch or publish.
Its report job runs ` actions/nightly-report ` from the same immutable commit as
the consumer-pinned reusable workflow, through a ` $/ ` self-repository reference,
and checks out nothing. The controller and nested installer use the same
revision; no workspace action can replace them. It receives only the outcome of the examples step, the nightly tag and
the release tag from the test job, never text produced by the examples under
test. It edits a single tracking issue, identified by its exact title and a bot
author. Every workflow in a repository shares the GitHub Actions bot identity,
so that test excludes issues opened by people and does not prove which workflow
opened the issue.

No workflow in the model opens a pull request, approves one, or merges one.
Every change to the repository is a maintainer’s pull request under the
repository’s own rules.

<a id="what-the-jobs-with-write-access-run"></a>

### What the jobs with write access run

The publish job and the report job hold the tokens that can change a
repository’s releases and issues. Each runs one shared action,
` actions/publish-release ` or ` actions/nightly-report `, and each action is a Roc
script that runs from the action’s own copy of roc-automation, at the full
commit SHA the workflow names. What runs in those two jobs is therefore:

- Nix, installed by an install action that the shared action references by
  full commit SHA; that action fixes the Nix version and downloads its
  installer from the Nix project over HTTPS;

- the ` blueprint ` binary, for the release that roc-automation’s own
  ` Blueprint.roc ` names, admitted by its reviewed SHA-256 as
  [The ` blueprint ` binary](index.md#the-blueprint-binary)
  describes;

- the environment that roc-automation’s ` Blueprint.lock ` pins: ` roc-stable `,
  which runs the script, the ` gh ` CLI, which makes every request, and coreutils,
  which provides ` sha256sum `;

- the content-addressed basic-cli platform and its dependencies, named by the
  reviewed script headers and ` RocPackages ` URLs; and

- the script and its modules under ` scripts/ ` in roc-automation.


The one full SHA in the workflow’s ` uses: ` line fixes all of these. The install
action’s SHA, the recorded hashes of the binary, the lock and the script are all
files of that commit. The lock pins provider inputs by revision and content
digest; the task-only Roc bundles are pinned by their URL content hashes. Nix
takes the compiler by the hash its pinned overlay records, and takes the tools
from nixpkgs at the pinned revision, as built by its binary cache, whose
signatures it checks. Blueprint prepares missing task bundles through Roc’s
verified unbundling path, described below.

Neither job checks out the calling repository, and neither action runs any of
its code. The calling repository’s ` Blueprint.roc `, lock and scripts play no
part. The report action reads no file from the job’s workspace. The publish
action reads the notes file and the asset files it is given, which an earlier
job built: it sends the notes as text, and it hashes, uploads and compares the
assets as bytes, and executes none of them.

The token is in the environment of the one step that runs the script, where
` gh ` reads it. It is never a command-line argument and is never printed, and
the scripts check every input before the first request.

Nix does not hold the token. The install action would otherwise write the job’s
default token into Nix’s configuration, for Nix to present to GitHub on its own
fetches. These two actions clear that setting, and stop before installing
anything further if Nix still reports a token. Every input Nix fetches is public
and pinned by the lock, so it needs none.

<a id="compilers"></a>

## Compilers

` Blueprint.roc ` names the release tag used for repository tooling, and
` Blueprint.lock ` pins the overlay by revision and content digest. Changing the
tooling compiler is therefore a reviewed source change. Nix verifies the
compiler archive against the hash the pinned overlay revision records for that
tag. This is content integrity under a reviewed hash, as in
[Released artifact verification](index.md#released-artifact-verification);
it is not provenance for the upstream compiler build.

Task and shell bundles listed under ` RocPackages ` are not provider inputs in
` Blueprint.lock `. Their committed URLs name the content hash of each bundle.
Blueprint downloads a missing archive and invokes ` roc unbundle `, which rejects
bytes that do not match that hash, before publishing the extracted package into
Roc’s package cache. A bundle a script names but the environment does not list
is fetched and checked by Roc itself. Sandboxed builds cannot download: their
` RocPackages ` sources are provider-locked and materialized into a separate build
package cache.

The
[Blueprint’s Roc package contract](https://lukewilliamboswell.github.io/roc-blueprint/configuration.html#roc-packages)
distinguishes Roc’s extracted package cache from Blueprint’s verified artifact
store. Blueprint reuses a package directory that already has
` main.roc ` without rehashing that directory. Reuse therefore assumes its contents
have not been modified since verified extraction; the content-addressed URL
does not attest to an existing extracted tree. The shared privileged actions
restore no candidate-produced package cache, run no candidate code and rely on
the disposable runner’s intact local cache. This does not establish integrity
of arbitrary pre-existing package caches on a maintainer’s machine.

The package compiler is the latest published nightly and is not pinned. It is
installed by the official ` roc-lang/setup-roc ` action, referenced by full
commit SHA, from a ` roc-lang/nightlies ` release over HTTPS. That action does not
check a nightly download against a digest, so the compiler’s integrity rests on
GitHub’s release hosting and the action’s pinned revision. This is accepted
because the nightly only ever runs in jobs that hold no write access. A workflow
resolves the moving release once and passes the tag to every job. Release notes
record the version used.

<a id="the-blueprint-binary"></a>

## The ` blueprint ` binary

` actions/setup-blueprint ` downloads the prebuilt ` blueprint ` binary from a
roc-blueprint release. Nix installation is an explicit caller option, through
an action referenced by full commit SHA, for environments/builds that need it. It builds nothing. Before the binary runs, the action checks it against
a reviewed SHA-256 and fails on a mismatch. Routine CI verifies that reviewed
hash locally and does not call an attestation service. The hash establishes
which bytes run; it is not provenance for the roc-blueprint build.

The reviewed hashes are recorded in the action, one set for each roc-blueprint
release, and are reviewed in the pull request that adds them. A caller
references the action by full commit SHA, so the hashes it trusts are fixed by
the same reference as the code that checks them. A caller may supply a hash of
its own for its runner, which then takes the place of the recorded one.

A release’s binary must be used with that release’s platform URL in
` Blueprint.roc `, so the action takes the release from that URL. The file is
candidate content in a pull request, and that is safe: the version it names can
only choose among releases whose hashes are already reviewed. A version the
action does not record, with no hash from the caller, fails the step.

The action accepts only a release version, from its ` version ` input or from
` Blueprint.roc `, and builds the download URL itself, under the roc-blueprint
releases of this project, so neither can redirect the download. The binary is
written to a private temporary file and becomes executable and visible on ` PATH `
only after its hash matches. A mismatch, a release with no reviewed hash for the
runner, or a system with no released binary fails the step and installs nothing.

Blueprint evaluates ` Blueprint.roc ` with its own fixed compiler, embedded in
the admitted executable and verified against its recorded SHA-256 before
extraction into the store, or an exactly matching host compiler. No GitHub
compiler download is needed for this bootstrap. The executable digest therefore
also fixes the embedded compiler archive; it does not prove that compiler
is free of defects. This compiler remains separate from fixed repository tooling
and the floating package compiler. ` Programs `/` Fetch `/` Expect ` environments need
no Nix; environments selecting Nix-sourced tools and sandboxed builds do.

The default executable cache is separate from the artifact store. Its exact key
includes release, system and reviewed digest. The installer copies bounded
regular bytes into a private file, hashes that copy and exposes an executable
only on a match. Corrupt regular bytes cause a fresh verified download; symlink
and nonregular cache entries are rejected. A cache hit never replaces hash
admission. Cache visibility does not establish provenance, and no moving release
lookup is needed to reuse the pinned executable.

Optional artifact caching restores only ` v1/artifacts `, never extracted trees or
temporary task links. Keys separate logical tool subsets and include the lock,
installer/registry identity, Blueprint release, system and workflow. Blueprint
checks restored artifact bytes against their content identity and locked length
before use. Corrupt artifacts and their derived trees are discarded and refetched;
a mismatching download is not usable. Derived trees are rebuilt on a fresh
runner rather than trusted from a cache. GitHub cache-hit status is not admission.
Roc’s package cache is separate and is not included by this action.

` actions/blueprint-tool ` runs a repository task and therefore belongs only in
read-only build jobs. The task resolves a temporary program link before its
lifetime ends; the action reports the actual file identity and can measure calls
through an optional host-side wrapper. It does not independently authenticate a
program merely because a task printed its path. The repository’s reviewed Fetch
recipe and lock, plus Blueprint’s verification during provisioning, supply that
admission. Privileged controllers never evaluate candidate Blueprint files or
consume candidate executable caches.

The setup action takes no token as an input. When requested, the Nix installer
can configure Nix with the job’s default token. The report/publisher actions
retain their explicit token-clearing checks described above. An optional cache
requiring ` id-token: write ` must remain an explicit caller choice; the artifact
cache here requires no such token permission. See
[the complete Roc package adoption guide](pure-package.md#adopt-a-pure-package-without-nix).

<a id="sbom-sidecars-and-supplier-documents"></a>

## SBOM sidecars and supplier documents

` actions/sbom-sidecars ` and ` actions/fetch-dependency-sboms ` evaluate repository
configuration and run only in jobs with ` contents: read `. The action revision
fixes reviewed SBOM tool records; native binaries are admitted by SHA-256 and
app bundles by their content hash and required compiler.

Supplier fetching uses HTTPS with a 1 MiB document bound. A recorded document
must retain its digest and describe the locked artifact. Changed or missing
bytes fail, and stale opt-outs require review. A document digest establishes
which bytes were reviewed, not who authored them.

The shared package publish job rechecks the transferred envelope, Blueprint
manifests and sidecar subject identities before attestation. Expected source,
run and producing attempt come from workflow context and build outputs;
publication retries retain the producer identity. The controller reads bounded
regular files and executes no candidate code or Blueprint tool.

Tests transfer generated source/native records and reject wrong identities,
changed bytes and unsafe paths. This establishes final-byte consistency, not
composition completeness or provenance of producer claims. The
[SBOM chapter](software-bill-of-materials.md#publish-and-consume-software-bills-of-materials)
owns generation, coverage and supplier-admission guidance.

<a id="documentation-deployment"></a>

## Documentation deployment

Renderer compilation, source conversion and execution run with ` contents: read `.
The executable cache is keyed by target, compiler, source and dependency/font
pins; restored binaries still undergo native acceptance tests. The deploy job
holds ` pages: write ` and ` id-token: write `, runs only on the default branch, and
deploys the uploaded Pages artifact. It checks out no source, executes no
renderer and restores no candidate executable cache. A pull request can build
the site and cannot deploy it.

<a id="downstream-status-inventory"></a>

## Downstream status inventory

The README’s downstream version inventory is also built in the read-only
Documentation job. ` scripts/downstream_status.roc ` makes only GET requests,
reads immutable default-branch workflow and composite-action source as text,
and emits JSON badge data and an escaped HTML report into the Pages artifact.
It executes no downstream code. The daily schedule uses the same separate
deployment job and permissions as other documentation builds. Publication does
not commit to this repository or update a consumer. Unknown API evidence and
incomplete trees never establish that a consumer is current.

Compatibility evidence requires an eligible latest-pair run of the pinned
shared nightly workflow. The dashboard verifies its immutable caller revision,
aggregate-job origin, complete current-attempt inventory and pass freshness.
The [dashboard contract](nightly-compatibility.md#read-the-downstream-dashboard)
defines eligible scheduled/manual runs and stale results.

<a id="shared-release-compatibility-runner"></a>

## Shared release-compatibility runner

Selection, testing and aggregation have only ` contents: read `. Selection fixes
one release/compiler pair; each target verifies the selected archive’s identity,
size and SHA-256, then rejects unsafe paths, aliases, special files and resource
limit violations before extraction. Archived examples and native prerequisites
run only in the read-only test job. Prerequisites require schedule/manual
execution on the actual default branch and a checkout matching the event SHA.
Checkout credentials are disabled and child token variables are cleared.

The runner bounds child execution and output, cleans up descendants, preserves
successful and failed diagnostics, and escapes workflow commands in console
output. Tests cover archive admission, interruptions and actual native targets.
Allowed compiler switches and suite fields are defined in the
[suite contract](nightly-compatibility.md#publish-the-suite-contract).

Aggregation checks every declared target’s source/compiler identity, counts and
run attempt as inert JSON. A single matrix output cannot establish a complete
pass. Reporting holds only ` issues: write `, checks out nothing and receives a
constrained outcome and selected tags, never archive-generated diagnostic text.
Complete passes can close the issue; known failures can open/update it. Unknown,
missing, skipped or interrupted work cannot close it, and override probes do not
modify the latest-pair tracking issue. The runner has no pin-update, PR, merge,
dispatch, release or deployment authority.

<a id="linker-input-release-publisher"></a>

## Linker-input release publisher

The pull-request linker-input publisher is a separate controller action for inert,
fully declared files produced by a caller’s unprivileged build jobs. A privileged
consumer workflow invokes the controller at an exact reviewed SHA. Caller source
is never checked out, imported, sourced, or executed in its privileged job.
Publication requires manual dispatch on the caller’s live default-branch
commit. The controller validates the complete regular-file set and content
identities, verifies the producer’s attestations, refuses mismatched existing
tags/releases, re-downloads and hashes the complete draft asset inventory, and requires immutable-release
enforcement after publication. See
[Trusted publisher contract](linker-input-releases.md#trusted-publisher-contract)
for the caller contract.

<a id="released-artifact-verification"></a>

## Released artifact verification

Consumers use reviewed content hashes as the runtime identity of released
binary inputs. Routine CI verifies downloaded and cached bytes locally before
extraction or execution. It must not depend on a remote signing, transparency,
or attestation service when a reviewed hash is available. Provenance
attestations are verified when a new release or lock is adopted, during an
explicit audit, or by a downstream verifier. See
[Verify released artifacts](artifact-verification.md#separate-admission-from-routine-use) for the complete
admission and consumption policy.

