34 KiB
Build
This page records the stable build, CI, docs, and release architecture. It is
not a command reference. Contributor-facing workflow details live in
CONTRIBUTING.md, CI.md, and published docs.
Artifacts
OpenShell builds these main artifacts:
| Artifact | Source |
|---|---|
| Gateway binary | crates/openshell-gateway |
| CLI binaries and system packages | crates/openshell-cli plus release packaging |
| E2E conformance CLI | crates/openshell-conformance-cli |
| Python SDK wheel | python/openshell |
| TypeScript SDK package | sdk/typescript |
| Gateway container image | deploy/docker/Dockerfile.gateway |
| Supervisor container image | deploy/docker/Dockerfile.supervisor |
| Helm chart | deploy/helm/openshell |
| VM driver/runtime assets | crates/openshell-driver-vm |
| Published docs site | docs/ rendered by Fern config in fern/ |
Sandbox community images are built outside this repository.
Build Features
Rust builds require Rust 1.94 or newer. TLS and certificate generation use AWS-LC, including the CLI and standalone examples. Native and cross-build environments must provide the C toolchain required by aws-lc-sys; the Nix development shells provide static AWS-LC libraries.
SQLx uses AWS-LC with native certificate roots. The server enables
sqlx-core/rustls-native-certs directly because SQLx's facade does not expose
that root selection independently of the crypto provider. Credential storage
continues to use the same AES-256-GCM envelope format across backend changes.
Anonymous telemetry emission is gated behind a default-on telemetry Cargo
feature. It is defined in openshell-core (where the emission code, HTTP
client, and endpoint live) and forwarded by the binary crates that emit or
collect telemetry: openshell-gateway, openshell-sandbox
(supervisor), and openshell-driver-vm. Every crate depends on
openshell-core with default-features = false, so the binary crate's feature
is the single switch that enables openshell-core/telemetry for its build
graph. In-process drivers (docker, kubernetes, podman) inherit the
gateway's setting through feature unification and carry no passthrough.
Building a binary without the telemetry feature compiles out telemetry
entirely: no endpoint, no telemetry HTTP client, and no emission code. With
telemetry compiled out, telemetry::enabled() is always false and the
emit_* helpers are no-ops, so the data-model types stay available and
dependent crates compile unchanged. The runtime OPENSHELL_TELEMETRY_ENABLED
switch remains the way to disable telemetry in a default (telemetry-enabled)
build.
Cargo cannot subtract a single default feature, so each of the three binary
crates also defines a defaults-without-telemetry alias listing every default
except telemetry. Telemetry-free builds use
--no-default-features --features defaults-without-telemetry and stay correct
as the default set grows, instead of dropping unrelated defaults the way a bare
--no-default-features does on openshell-sandbox. The alias is a keep-list,
not a switch: enabling it on top of the defaults would otherwise yield a
telemetry-on binary that reads as telemetry-free, so each crate root carries a
compile_error! for the telemetry + defaults-without-telemetry combination.
rust:verify:defaults-without-telemetry guards both properties — that each
alias still equals its crate's defaults minus telemetry, and that the
mutual-exclusion error is wired up — and rust:verify:telemetry-off builds
through the alias and inspects the resulting binaries for telemetry markers.
Supervisor upstream TLS root-store selection is controlled by the
bundled-ca-roots Cargo feature (on by default). Default builds use Mozilla
roots through webpki-roots plus locally-installed CAs from the system bundle.
Building without bundled-ca-roots switches to the platform trust store via
rustls-native-certs and excludes bundled Mozilla root crates such as
webpki-roots and webpki-root-certs from the dependency graph. The
system-ca-roots feature alias on openshell-sandbox includes all other
defaults (currently telemetry) except bundled-ca-roots, so Linux
distribution builds (e.g. RPM) can use
--no-default-features --features system-ca-roots without manually re-adding
unrelated defaults. Other Rustls clients use native roots directly because that
already satisfies Linux distribution trust-store policy.
The workspace uses z3 versions whose z3-sys dependency keeps downloader
HTTP/TLS support behind explicit build features, so default system-Z3 builds do
not reintroduce bundled Mozilla roots. Release builds that need bundled Z3
continue to opt in with bundled-z3.
Linux Runtime Environments
OpenShell uses different Linux libc environments for different host artifacts.
The standalone openshell CLI is built as a static musl binary so it can run on
a wide range of Linux distributions without depending on the host's glibc. Host
runtime binaries that use the GNU/Linux runtime environment are GNU-linked.
openshell-gateway and openshell-driver-vm are built with a glibc 2.28 floor.
The gateway bundles z3 into the release binary so Linux packages, standalone
tarballs, and gateway images do not depend on distro-specific z3 shared-library
SONAMEs.
The supervisor is the one binary whose libc is selectable, because it is the one
binary executed inside a userland OpenShell does not control. SUPERVISOR_LIBC
chooses between musl (default) and glibc-static. Both produce a fully static
binary; the choice does not change the runtime layout or the supervisor image base.
Static linkage is a hard requirement rather than a preference, so both variants
are verified by tasks/scripts/verify-static-binary.sh, which fails the build on
any PT_INTERP or DT_NEEDED entry.
The two variants differ only in build-time constraints:
musl (default) |
glibc-static |
|
|---|---|---|
| Cross-compiles | yes, via cargo zigbuild |
no — must build natively per architecture |
| Host requirement | zig + cargo-zigbuild | glibc static libraries (glibc-static on Fedora/RHEL, libc6-dev on Debian/Ubuntu) |
| libc license | MIT | LGPL-2.1-or-later, statically linked |
cargo zigbuild cannot produce the glibc-static variant: zig cc accepts
-static for *-linux-gnu targets and emits a dynamically linked binary
anyway. The staging script therefore refuses to cross-compile that variant
instead of silently degrading linkage.
Selecting glibc-static statically links LGPL glibc into a redistributed
binary, which carries relinking obligations that musl (MIT) does not. Treat the
default as the shipping configuration unless that has been reviewed.
Container Builds
The Docker image pipeline is a two-step flow: build the Rust binary natively
for the target architecture, then assemble the container image from the
prebuilt binary. The gateway image is built from deploy/docker/Dockerfile.gateway
and the supervisor image from deploy/docker/Dockerfile.supervisor. Neither
Dockerfile compiles Rust — both copy a staged binary out of
deploy/docker/.build/prebuilt-binaries/<arch>/ into the final image.
Local binary staging is driven by tasks/scripts/stage-prebuilt-binaries.sh. Because
staging cross-compiles on the host, it sources tasks/scripts/build-env.sh and
raises the per-process open-file limit before invoking cargo zigbuild on
macOS — the static musl link opens hundreds of .rlib files at once and would
otherwise fail with ProcessFdQuotaExceeded under macOS's default soft limit of
256. The guard is a no-op on Linux and when cargo-zigbuild is absent. Gateway
binaries use cargo zigbuild with GNU targets pinned to glibc 2.28, including
native-architecture builds, so the gateway image, standalone tarballs, and Linux
packages share the same host portability floor. The gateway build enables
bundled-z3. Linux VM driver release artifacts use the same glibc floor so
package-managed VM support does not raise the package runtime requirement.
Gateway staging and release workflows set up the Zig C/C++ wrapper before
bundled Z3 builds and verify the maximum referenced GLIBC_* symbol version
before publishing or copying artifacts.
Supervisor binaries are static in every configuration. The default musl
variant uses cargo zigbuild when available, including native CPU
architectures, so C dependencies are compiled for the musl target instead of the
host GNU libc target. The glibc-static variant uses plain cargo build with
+crt-static and requires a native per-architecture build. Local Docker image tasks infer the
target architecture from DOCKER_PLATFORM when set. Otherwise, they require
valid container engine host metadata and fail when the engine query is
unavailable or reports an unsupported architecture, avoiding host-kernel
fallbacks that can target the wrong architecture. CI instead compiles binaries
in platform-specific Nix development shells through reusable workflows and the
shared build-rust-binary action. The image build downloads each binary artifact
into the staging directory before running Buildx.
The Nix flake exposes one development shell with target-specific toolchains.
The shared mkToolchain function in nix/toolchain/default.nix assembles native
libraries and Cargo environment settings. Linux and Darwin modules select the
compiler and sysroot and generate the compiler wrapper for their platform.
The nix/toolchain/glibc-2.28/ directory contains the pinned glibc build,
GCC environment, and sysroot assembly used by GNU Linux targets.
The glibc build reuses a pinned historical Nixpkgs recipe with current build
tools; its headers, shared libraries, and static archives are assembled into
the sysroot from separate outputs.
Each toolchain supplies its compiler driver, assembler, archiver, native
libraries, and Cargo environment through derivation passthru. The shell omits
an implicit host C compiler; Cargo builds select the appropriate tools with
--target. GNU targets use a glibc 2.28 sysroot and static GCC runtimes, while
musl targets produce static executables.
On macOS, the shell also provides a native Darwin toolchain with static Z3 and AWS-LC. Its Clang driver uses the pinned, unprocessed Apple SDK so system library stubs, including libiconv and libc++, retain their Apple install names. System libraries and frameworks remain dynamically linked. The deployment target matches the Nix host platform's minimum macOS version. The Rust toolchain does not propagate Nix's replacement system libraries into the link environment.
Gateway and supervisor binaries staged into branch E2E, Release Dev, and Release
Tag images are compiled through cargo auditable (pinned in mise.toml), which
embeds a .dep-v0 section describing the Rust dependencies actually compiled
into the binary. That section holds data rather than symbols, so it survives the
workspace's strip = true release profile, and Syft can catalog the crates
present in image binaries instead of inferring them from the source tree. This
is a different artifact from the source SBOM produced by syft dir:. in
tasks/sbom.toml, which describes the checkout, and from the image SBOM
attestation below, which describes a published image.
The shared binary build action uses the default Nix shell and compiles release
artifacts with cargo auditable build --target <triple>. Verification and upload
read binaries from target/<triple>/release/.
Branch E2E, Release Dev, and Release Tag image jobs stage those same artifacts
instead of rebuilding binaries in Docker. Each binary build scans its output with
Syft and requires at least one decoded Cargo package before uploading the
artifact. The action checks each binary's --version output and leaves its
linkage as produced by the Nix toolchain, without post-link rewriting or
platform-specific linkage checks. The CI image gains the pinned cargo-auditable
tool through mise install --locked but ships no auditable OpenShell binary of
its own.
Pushed Docker images carry minimal SLSA provenance and a per-platform SPDX SBOM
generated by BuildKit's default Syft scanner. The registry exporter uses OCI
media types and oci-artifact=true, so each attestation identifies its subject.
GHCR exposes these through the image index because it has no referrers API.
Attestations require a registry-backed image index. Local builds therefore keep
--provenance=false, and Podman builds carry neither attestation.
tasks/scripts/verify-image-sbom.sh verifies the merged multi-arch tag and runs
with --require-cargo for auditable builds, so those attestations must also
contain Cargo packages.
Runtime layout:
- Gateway:
gcr.io/distroless/cc-debian13:nonrootbase, GNU-linked binary at/usr/local/bin/openshell-gateway, runs as UID/GID1000:1000. Linux GNU gateway binaries must not referenceGLIBC_*symbols newer thanGLIBC_2.28; release workflows verify this before publishing artifacts. The gateway bundles z3, so the image does not need a distro-provided z3 runtime. The base is pinned to a multi-architecture digest; distro security updates require refreshing that digest and rebuilding the gateway image. Updating the container's glibc package does not raise the binary's glibc compatibility floor. - VM driver: host GNU-linked binary installed at
/usr/libexec/openshell/openshell-driver-vmin Linux packages and published as a release artifact. Linux GNU VM driver binaries must not referenceGLIBC_*symbols newer thanGLIBC_2.28; release workflows verify this before publishing artifacts. Nix produces the platform-specific compressed runtime inputs. CI combines them with the matching supervisor artifact in a runner-temporary directory outside Cargo'starget/before the shared Rust cache action runs. An explicitly configured VM runtime bundle is required to contain every non-empty embedding input; the driver build fails before packaging when an input is absent or empty. - Supervisor: Alpine base with
nftables; base packages are upgraded before installing firewall tools to pick up distro security fixes. Static binary at/openshell-sandbox(musl by default; seeSUPERVISOR_LIBCabove). Static linkage keeps the binary usable when the image is mounted/extracted into sandbox environments (Docker extraction, Podman image volumes, Kubernetes init-container copy-self), whose libc and glibc version are not known at build time, whilenftablessupports Kubernetes supervisor sidecar egress enforcement. The VM driver bundles its own supervisor build (tasks/scripts/vm/build-supervisor-bundle.sh) and does not readSUPERVISOR_LIBC.
Gateway image builds bake the corresponding supervisor image tag into the
gateway binary so Docker sandboxes do not depend on :latest by default.
The Helm chart omits the supervisor image from gateway configuration unless an
operator supplies a repository or tag override, preserving that build-time
pairing for Kubernetes sandboxes as well.
Package formulas also pin Docker supervisor extraction to the matching release
image tag so standalone gateway binaries do not infer image tags from package
versions.
The Homebrew service keeps gateway TLS under the Homebrew state directory but
mirrors Docker sandbox client TLS into $HOME/.local/state/openshell/homebrew/tls
at service start, because Docker Desktop bind mounts must use paths visible to
the macOS user's shared home directory.
Local image work should use mise tasks rather than direct Docker commands so
the same staging and tagging assumptions are used locally and in CI.
Container-engine selection is centralized in tasks/scripts/container-engine.sh.
CONTAINER_ENGINE=docker|podman is the only explicit override. Docker- and
Podman-backed e2e wrappers validate that override against their lane, set
OPENSHELL_E2E_DRIVER, and reject the removed
OPENSHELL_E2E_CONTAINER_ENGINE selector so build helpers and Rust e2e support
containers use the same engine. When no explicit override is present, an e2e
driver requirement wins, then a local-cluster requirement, then host
auto-detection.
Local Kubernetes image workflows opt into cluster-aware selection with
CONTAINER_ENGINE_TARGET=local-k8s-cluster. The hint is intentionally scoped to
Skaffold-style push: false builds where the image must land in the engine
backing the active local cluster: k3d-* contexts require Docker, kind-*
contexts use KIND_EXPERIMENTAL_PROVIDER=docker|podman when set, and ambiguous
or unknown contexts require an explicit CONTAINER_ENGINE. Other image builds
do not infer from kube context.
Disposable Test Guests
The Nix test guest harness under nix/test-guest boots native-architecture cloud images
through QEMU for package, release, and E2E validation. A prepared cache entry is
captured after the exact ordered Ansible configuration list and before
test-specific packages, copied binaries, forwarded ports, or commands.
Prepared disks are flattened, sanitized QCOW2 images. The local cache keeps them read-only and each test receives a fresh writable overlay and cloud-init identity. The optional shared cache stores the compressed standalone disk and its compatibility metadata as a custom OCI artifact. Normal test runs ensure the exact local entry exists, invoking the cache builder automatically on a miss before booting a disposable overlay. The separate cache app owns OCI pulls and explicit publication. OCI pulls require a trusted manifest digest and retain that provenance with the local entry; mutable tags are used only for explicit publication.
CLI conformance runs after target provisioning and operates only through the configured OpenShell CLI. The smoke scenario verifies the black-box sandbox lifecycle by creating, inspecting, executing in, and deleting a sandbox.
Python Wheel Packaging
The generated protobuf/gRPC stubs under python/openshell/_proto/ are gitignored
build outputs of mise run python:proto. Setuptools includes them through the
package-data configuration in pyproject.toml. Release workflows build the
wheel directly and do not produce a source distribution. Setuptools SCM derives
local versions from Git and accepts the release workflow's computed version
through its distribution-specific override.
The build produces one platform-independent py3-none-any wheel. A verifier
checks its tag, metadata, version, required package files, and the absence of
native files or an openshell executable entry point. Release workflows build
the wheel once, install it in a clean virtual environment, import the public
package modules, and confirm that installation did not create an openshell
command.
TypeScript SDK Packaging
The native TypeScript SDK in sdk/typescript uses Connect over the generated
OpenShell protobuf surface. sdk/typescript/buf.gen.yaml selects the client
proto closure, and mise run sdk:ts:proto generates gitignored sources under
src/gen. TypeScript compilation includes those sources in dist, so package
consumers do not run code generation.
Branch checks run mise run sdk:ts:ci, enforce an 80% line-coverage floor, and
exercise version stamping plus npm publish --dry-run. Tagged releases publish
@nvidia/openshell-sdk to GitHub Packages. The repository keeps package version
0.0.0; the release task derives and temporarily stamps the npm version from
the release tag.
CI and E2E
Required checks run on GitHub Actions. Pull-request workflows that use NVIDIA self-hosted runners trigger from copy-pr-bot mirror branches, so trusted PRs are mirrored into pull-request/<N> branches before those workflows run. main also uses GitHub merge queue so the final queued integration commit is validated before it merges.
The high-level CI model:
- PR-context gate jobs publish required statuses for the PR head commit.
- Standard branch checks run from trusted mirror branches.
- Label-gated Docker, Podman, VM, GPU, and Kubernetes E2E checks run from trusted mirror branches.
- Merge-group checks run against GitHub's temporary queue branch for the final integration state.
- Gate jobs verify that the mirror branch matches the PR head, or that the merge-group workflow ran for the queued SHA, and that the expected non-gate workflow actually ran.
- Release workflows rebuild and publish binaries, wheels, images, and docs.
Repository CI keeps telemetry compiled into release-parity artifacts but disables emission for Rust tests, E2E runs, and release canaries. This prevents synthetic activity from contributing to product usage metrics.
Static security checks are deliberately outside the mirror-branch path. They run
directly on GitHub-hosted runners and none of them consume NVIDIA self-hosted
capacity. The change-oriented ones receive no secrets, so they also cover fork
pull requests. Codex Security release qualification is the exception: it needs a
scoped API key, which routes its model calls to NVIDIA-hosted inference while
the job itself stays GitHub-hosted. That placement is load-bearing rather than
incidental: on the repository self-hosted runner the scan agent executes no
shell commands at all, so its preflight never scopes the diff and it seals no
draft. Scanner jobs request security-events: write and upload SARIF to Code
Scanning directly on every event they run on, including fork and Dependabot
pull requests, which Code Scanning permits for
pull_request runs despite their read-only GITHUB_TOKEN. No privileged
intermediate workflow relays those uploads. Manually dispatched Codex Security
runs are the one opt-in exception, described below. Report retention differs by
scanner: Actionlint, Zizmor, and CodeQL keep their reports as workflow artifacts,
and Codex Security keeps no raw report.
Triggers differ by workflow: .github/workflows/workflow-security.yml runs on
pull_request, merge_group, main, and a weekly schedule;
.github/workflows/dependency-review.yml runs on pull_request and
merge_group only, because it needs a base and head commit to compare;
.github/workflows/codeql.yml runs nightly on the default branch (main) via
schedule, with workflow_dispatch kept for manual diagnostics; and
.github/workflows/codex-security.yml runs on pushed v*.*.*-pre.* tags, and is
also callable through workflow_call and workflow_dispatch. CodeQL has no
automatic pull_request, merge_group, or push trigger, so it reports
repository-level Code Scanning state on the default branch instead of per-PR
results, and its four-language matrix stays off the per-change critical path.
Codex Security is release-scoped rather than change-scoped, so it never runs on
a pull request or merge group automatically.
.github/workflows/security-scan.yml is a manual and reusable parent for Codex
Security, CodeQL, Trivy, Cargo Deny, and Actionlint/Zizmor. Each child runs
independently against the supplied pre-release tag; Codex retains its cumulative
stable-to-candidate range. The parent forwards OCI references to Trivy and
publishes SARIF, including Codex results on manual parent runs. Codex publishes
against the candidate commit on main; the other SARIF producers use the
candidate tag and commit. Cargo Deny retains its self-hosted runner and CI
container. Dependency Review and Trivy Changes remain separate comparison
workflows. Existing standalone triggers remain active.
The parent enforces HIGH/CRITICAL findings for Codex, CodeQL, Trivy, and Zizmor.
Its allow-high-critical input disables that threshold while keeping execution
and publication failures fatal. Each scanner evaluates its existing report
after publication. Actionlint stays informational; Cargo Deny runs its native
advisory check, which is independent of the severity override. Standalone
finding policies are unchanged. Child concurrency groups distinguish the
scanner as well as the caller, so sibling workflows cannot cancel one another.
Codex retains its repository-wide release qualification group.
See CI.md for invocation examples.
- Actionlint and Zizmor analyze the workflow definitions themselves.
Repository configuration lives in
.github/actionlint.yml(self-hosted runner labels, scoped per-file ignores) and.github/zizmor.yml(scoped rule suppressions). Zizmor runs offline and reports only High severity, which is its maximum level. Both publish SARIF to Code Scanning and retain report artifacts. The Nix flake provides both scanners, so local runs usenix develop --command actionlint -shellcheck= -pyflakes=andnix develop --command zizmor --offline --persona=regular --min-severity=high --no-exit-codes .. - Dependency Review compares the base and head dependency graphs. It preflights the GitHub Dependency Graph compare API and neutralizes itself with a warning while that repository feature is unavailable, so the check begins reporting on its own once the feature is enabled. Reviews run in warn-only mode.
- CodeQL analyzes product Rust code, examples, and the Go, Python, and
TypeScript SDKs, scoped by
.github/codeql/codeql-config.yml. Rust test code is excluded in two layers: the analyze job setsCODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES=-testso the extractor skips#[cfg(test)]blocks, andpaths-ignoredropscrates/*/tests, whose integration targets the cfg override does not reach. Examples remain in scope, and E2E test code stays excluded becausee2e/is not an analyzed path. Only Go requires a build; the other languages use build modenone. Analysis runs on the nightly schedule, by manual dispatch, or throughworkflow_call. Results are uploaded to Code Scanning and always retained as workflow artifacts. - Codex Security qualifies release candidates rather than individual
changes. The job installs a pinned
@openai/codex-securityrelease into the runner temp directory before the repository is checked out and invokes it by absolute path, so repository-controlled files cannot shadow the scanner. Model calls go to NVIDIA-hosted inference athttps://inference-api.nvidia.com/v1, declared as a custom Codex provider namednvidiathat uses the Responses wire API with WebSockets disabled. The scan runsopenai/openai/gpt-5.6-solatmediumreasoning effort, with the multi-agent runtime capped at eight concurrent threads throughfeatures.multi_agent_v2.max_concurrent_threads_per_session. TheCODEX_SECURITY_API_KEYsecret holds the NVIDIA key and is exposed to the scan step alone, asOPENAI_API_KEYso the CLI selects API-key auth and asNVIDIA_INFERENCE_API_KEY, the providerenv_keyread by the Codex child process.CODEX_SECURITY_STATE_DIRandSCAN_DIRare suffixed withgithub.run_idandgithub.run_attemptand created mode700, so no scanner state or result set from a previous run or retry attempt is reused even on a runner with a reusable temp directory.tasks/scripts/codex_security_range.pyresolves the scan range, reusing the tag parsers intasks/scripts/release.pyso both stay on one definition of a release tag while requiring thevprefix that a release workflow needs. The job stages both files out of the workspace from the workflow's own revision and runs the resolver by absolute path, because a scanned candidate predates them and a revision under scan must not choose its own scan range. The range itself is resolved against the checked-out candidate: the candidate must be avX.Y.Z-pre.Ntag that is an ancestor oforigin/main, and the base is the newest stablevX.Y.Ztag merged into the candidate that is strictly older than the release trainvX.Y.Zthe candidate targets. A full-repository scan is only possible when no such stable tag exists and the caller passesallow_full_bootstrap. Each candidate scans the cumulative stable-to-candidate diff, so later candidates re-cover earlier ones. SARIF is uploaded againstrefs/heads/mainat the candidate commit under the train-scoped categorycodex-security/vX.Y.Z, which makes each candidate's analysis replace the previous one for that train. Automatic pre-release tag pushes andworkflow_callruns always upload.workflow_dispatchruns still perform the scan and the SARIF export, but skip the Code Scanning upload unless the caller sets theupload_sarifinput, so manual diagnostics do not overwrite a train's published analysis by default. Codex Security 0.1.24 cannot apply--max-costto a slash-qualified model identifier, so the run has no CLI-enforced cost ceiling. Spend is bounded instead by the 120-minute job timeout, a single repository-wide concurrency group that serializes qualification so starting a newer candidate cancels an in-flight one, and NVIDIA account-side controls. No raw report is retained. - The job clears
kernel.apparmor_restrict_unprivileged_usernsbefore installing the scanner. Codex confines model-run commands with bubblewrap, which needs unprivileged user namespaces; Ubuntu 24.04 restricts those through AppArmor, so bubblewrap fails to configure the sandbox network namespace (bwrap: loopback: Failed RTM_NEWADDR) and the agent executes no commands at all. The failure is silent: the agent retries its shell tool, gives up, and seals no draft, while the scanner only reports a missing or incomplete draft. Lifting a kernel restriction on the runner is what allows the sandbox that confines the agent to start, and the runner is ephemeral and GitHub-hosted. - The scan sets
approval_policy="never". Codex Security keepsapprovals_reviewer="auto_review"unconditionally, and that reviewer runs on its own model rather than the configured one. Because the workflow declares a single provider that serves onlyopenai/openai/gpt-5.6-sol, any approval request reaches a model the endpoint does not serve, so the agent never gets a shell command approved and seals no draft. The scan stays confined by itsworkspace-writesandbox with network access disabled and by the scanner's own permission profile, which grants read access to the filesystem root and write access only to the workspace roots. - A scan that cannot execute commands reports only a missing or incomplete
draft, so diagnosing one means reading the scanner's session rollouts under
CODEX_SECURITY_STATE_DIR, where every shell command the agent ran is recorded. No command at all is the signal that the sandbox failed to start.
Findings do not fail standalone informational runs; reusable callers can enable
HIGH/CRITICAL enforcement with fail-on-findings. Scanner and build failures
always fail. A scanner that
cannot run, a CodeQL analyzer that does not complete, an unexpected Dependency
Graph API error, and a Codex Security range, scan, or export failure are all
errors, which keeps an informational check from silently degrading into a no-op.
Codex Security also rejects any scan scope other than the resolved
cumulative diff or an approved full bootstrap, so a qualification run either
covers the whole stable-to-candidate range or fails; a separate no-permission
job republishes the analysis job's outcome as the
OpenShell / Codex Security (informational) status. None of these checks are
required statuses, so they do not gate merges.
Codex Security findings are informational during the observation phase, and the workflow only reports on candidates that already exist. Creating pre-release tags and gating stable promotion on qualification results are part of RFC 0014 and are not implemented yet.
Artifact Scanning
Two entry points share tasks/scripts/trivy-scan.sh: a standalone analysis
workflow and a pull-request change gate. Nix supplies Trivy, Helm and yq.
The standalone workflow scans deployment configuration and supplied OCI references independently of release publication. Detailed JSON reports feed the differential gate; the summary and published SARIF consolidate configuration findings across profiles while preserving resource identity and affected profiles. Images and packaged charts retain separate identities based on their full references. Publication batches respect GitHub's limit of 20 SARIF runs.
The PR/merge-group gate scans base and candidate with the same scanner and rejects
new HIGH or CRITICAL configuration findings. Its stable
OpenShell / Trivy Changes status succeeds when nothing relevant changed.
For PRs, the baseline is the first parent of the exact merge commit being tested,
not the event's potentially older base SHA or the current branch tip. Change
detection and both scans use that same immutable pair, including on reruns.
Merge groups and manual runs retain their explicit baseline and always scan.
Image CVEs need the standalone scan. The reporting and gate invariants are:
- A structurally invalid Trivy report is an error, not an empty finding set.
- Scanner failures prevent publication of incomplete analyses; findings alone do not prevent publishing complete reports.
- Findings compare per profile against the same baseline profile, by semantic identity and count rather than line number; a profile absent from the baseline falls back to that identity's maximum across all profiles.
- The candidate's ignore file is validated, but the baseline's policy applies to both scans, so an exemption takes effect only after merge.
See CI.md for profiles, report paths, severity settings, exceptions, and the contributor and maintainer workflows.
Docs Site
Published docs live in docs/, and Fern site configuration lives in fern/. See fern/README.md for the source layout, local development commands, version model, and publishing workflows.
Validation Expectations
Rust CI rejects stale or modified Cargo lockfiles and runs Clippy for the workspace, E2E crate, and standalone examples.
- Run
mise run pre-commitbefore committing. - Run
mise run testafter code changes. - Run
mise run e2efor sandbox, policy, driver, or deployment changes when the affected runtime can be exercised. - Run
mise run cibefore opening a PR when practical. - Run
mise run docswhendocs/orfern/changes.
Architecture-only changes should still check links and references because this directory is used by agents during implementation and review.