@ship.zone/runner
@ship.zone/runner is the outbound-only OCI execution agent for ship.zone CI/CD. It implements the
foss-ci-runner/2.0-draft.2 protocol, validates compiled jobs against @foss.global/ci-spec, and
runs one fenced attempt at a time on a local Docker or Podman engine.
The control plane owns workflow compilation, scheduling, authorization, secrets policy, and canonical run state. This package owns runner registration, lease handling, isolated execution, bounded transfers, redacted logs, and terminal reporting.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Security Model
- Only local Unix-socket Docker and Podman endpoints are accepted. Remote contexts and connections are rejected before capabilities are advertised.
- Job images must be Linux images pinned by an exact SHA-256 digest. The image OS, architecture, and digest are checked before lease acceptance.
- Every compiled step runs in a fresh container as a numeric non-root user with a read-only root,
all capabilities dropped,
no-new-privileges, private IPC, and CPU, memory, PID, time, and writable-storage limits. - Network mode defaults to
none.bridgemust be allowed by both runner policy and the compiled job; it is not an egress allowlist. - Job containers receive no host paths, runtime sockets, devices, host namespaces, or arbitrary OCI
arguments. Their only persistent writable mount is
/workspace. - The shared workspace is an attempt-specific
nodev,nosuidtmpfs with hard byte and entry ceilings. Imported executable files may run directly from it. A trusted static Rust helper imports source/cache staging, measures the workspace around every step, and exports only declared artifact/cache paths. - Helper containers use the exact job image only as an inert root filesystem. They override the
entrypoint with the packaged static helper, have no network, and receive only the narrowly scoped
CHOWNandDAC_OVERRIDEcapabilities required for private staging ownership. Job containers do not receive those capabilities. - Step containers receive exactly the compiled step environment plus job secrets. Secret values are passed through the runtime environment rather than OCI command arguments and are redacted from logs. Redaction is defense in depth: a malicious job can transform a secret or split it across streams, so the coordinator must not grant protected secrets to untrusted jobs.
Requirements
- Deno 2
- pnpm 11
- Docker or Podman on Linux
- A rootless runtime by default
- Rust and configured cross-compilers when building the packaged helpers from source
Install
Add the published source package to a Deno project:
deno add npm:@ship.zone/runner
Then import the package through the generated import-map name. The npm export resolves to the prebuilt Deno JavaScript bundle and its rolled-up TypeScript declarations:
import { ShipRunner } from '@ship.zone/runner';
Without an import-map entry, use the npm package directly:
import { ShipRunner } from 'npm:@ship.zone/runner@0.2.0';
For development in this repository, install dependencies and build the npm bundle plus the static
musl-based linux_amd64 and linux_arm64 helpers:
pnpm install
pnpm build
The published package includes dist_rust/runnerworkspace_linux_amd64_musl and
dist_rust/runnerworkspace_linux_arm64_musl, the Rust source and manifests,
rust/.cargo/config.toml, and the dependency notices. oci.workspaceHelperPath can select an
explicit absolute helper path for development or controlled deployments.
Configure And Run
Create a private runner configuration:
deno run --allow-env --allow-net --allow-read --allow-write --allow-run \
npm:@ship.zone/runner init \
--control-plane https://foss.global \
--runtime-path /usr/bin/docker
Register with a one-time bootstrap token, then start the daemon:
SHIP_RUNNER_BOOTSTRAP_TOKEN='<one-time token>' \
deno run --allow-env --allow-net --allow-read --allow-write --allow-run \
npm:@ship.zone/runner register
deno run --allow-env --allow-net --allow-read --allow-write --allow-run \
npm:@ship.zone/runner run
Replace run with once to claim at most one job or config to print the effective configuration
with credentials masked. Within this repository, deno task dev -- <command> is the equivalent
development invocation. SHIP_RUNNER_CONFIG selects a config path; the default is
./runner.config.json.
init writes the config with mode 0600. Registration verifies the runtime and protocol discovery
document before advertising capabilities, durably journals the exact registration request without
the bootstrap token, then atomically replaces the bootstrap token with the returned runner
credential. An interrupted registration replays the journaled request and rejects configuration
drift. Credential rotation from session and runner-heartbeat responses is persisted before the new
credential becomes active.
The default policy includes:
- rootless Docker at
/usr/bin/docker; - labels for Linux, the current architecture, and OCI;
- network
noneonly; - job user
65532:65532; - a 4 GiB maximum logical workspace and 100,000 entries;
- bounded source, artifact, cache, log, environment, and staging limits;
- a 1 GiB free-space and 10,000-inode staging reserve.
Plain HTTP control planes are rejected except for explicitly enabled loopback development URLs.
Execution Lifecycle
For each claim, the runner:
- Validates the exact lease shape, compiled-job JSON Schema, RFC 8785
compiledPlanDigest, internal transfer/permission coherence, fit against the immutable advertised capability snapshot, and all runner execution policy before image inspection or pull. - Pulls or locates the exact image digest, validates its Linux architecture, and probes it with the inert helper.
- Writes a private interrupted-attempt journal and sends the explicit fenced acceptance request. Source transfer and execution do not start before HTTP 204.
- Downloads and validates the source
tar.gz, then restores readable caches in declaration order into private host staging. - Imports staging into the bounded tmpfs and runs compiled steps in declaration order, stopping at the first non-zero exit.
- Publishes successful read-write caches and artifacts selected by their
whenpolicy as deterministic, replayabletar.gztransfers. - Cleans staging, helper/step containers, and the tmpfs volume before sending the idempotent terminal completion, then clears the recovery journal after the coordinator accepts it.
An attempt-scoped inert holder container keeps Docker's local-driver tmpfs mounted while fresh step and helper containers come and go. It runs no image-provided command and is removed with the workspace.
On restart, a new session first removes stale labeled containers and volumes locally, scavenges only the runner's known host-staging directories and journal temporary files, then reads the secret-free journal and calls the v2 recovery endpoint to revoke and reconcile the interrupted attempt. Work is never resumed, and a coordinator outage cannot retain prior staging.
Cancellation And Fencing
Job heartbeats renew the coordinator-time lease and acknowledge cancellation directives. A
cancellation immediately begins termination, bounds TERM-to-KILL by the coordinator's terminateBy
deadline, and completes with the same cancellation request ID. Heartbeats continue during bounded
termination.
HTTP 409 fencing loss, authentication loss, or coordinator-time lease expiry stops work and suppresses subsequent logs, transfers, and completion. Graceful runner shutdown abandons the exact accepted identity instead of reporting a successful terminal result.
Logs use nested fenced identity, zero-based sequencing, exact acknowledgements, streaming secret
redaction, and UTF-8 byte limits. Reaching the negotiated total log limit fails closed and
terminates the attempt as system_error; output is never silently truncated.
Runner Protocol V2
All endpoints are below /api/runner/v2. Registration uses the one-time bootstrap bearer token;
subsequent requests use the renewable runner credential.
| Operation | Endpoint | Idempotency |
|---|---|---|
| Discover | GET /api/runner/v2 |
Public exact descriptor |
| Register | POST runners/register |
registrationRequestId and canonical body |
| Start session | POST runners/{runnerId}/sessions |
sessionRequestId and canonical body |
| Runner heartbeat | POST runners/{runnerId}/heartbeat |
Current runner session |
| Claim | POST jobs/claim |
claimRequestId and capability snapshot |
| Accept | POST jobs/{jobId}/attempts/{attemptId}/accept |
acceptRequestId and full identity |
| Job heartbeat | POST jobs/{jobId}/attempts/{attemptId}/heartbeat |
Fenced identity |
| Log | POST jobs/{jobId}/attempts/{attemptId}/logs |
Attempt and sequence |
| Complete | POST jobs/{jobId}/attempts/{attemptId}/complete |
completionRequestId and terminal body |
| Abandon | POST jobs/{jobId}/attempts/{attemptId}/abandon |
abandonRequestId and canonical body |
| Recover | POST jobs/recover |
Current session and recoveryRequestId |
| Source | GET sources/{sha256} |
Immutable attempt-bound digest |
| Artifact | PUT jobs/{jobId}/attempts/{attemptId}/artifacts/{name} |
transferRequestId and metadata |
| Cache | GET/PUT jobs/{jobId}/attempts/{attemptId}/caches/{name} |
Fenced declaration and transfer ID |
Transient retry statuses are 429, 502, 503, and 504. Exact retries reuse the same request ID and canonical body. Request, response, archive, extracted-data, entry, and stream sizes are bounded. Each source, artifact, or cache archive also has a fixed 134,217,728-byte metadata ceiling across its normalized UTF-8 entry paths and symbolic-link targets. Implicit parent directories count against the workspace entry limit, and symbolic-link validation retains at most 1,000,000 canonical path components.
Library API
The package exports its runner components for controlled embedding:
import {
ArtifactManager,
CacheManager,
defaultRunnerConfig,
OciExecutor,
RunnerApiClient,
ShipRunner,
SourceWorkspaceManager,
} from '@ship.zone/runner';
The CLI is the supported complete assembly. Embedded callers must preserve the same runtime verification, discovery, lifecycle, fencing, cleanup, and credential-persistence ordering.
Verification
deno task fmt --check
deno task lint
deno task check
pnpm test
pnpm build
pnpm pack --dry-run
The real Docker integration is opt-in and requires a locally available digest-pinned Linux image:
SHIP_RUNNER_OCI_TEST=1 \
SHIP_RUNNER_OCI_IMAGE='alpine@sha256:<digest>' \
deno test --allow-all test/oci-executor.test.ts
It verifies direct execution of an imported workspace file, shared multi-step state, helper ownership, selected export, hardlink rejection, inode exhaustion, and container/volume cleanup against the real runtime.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file. Dependency notices for the npm bundle and the packaged static helper binaries are in third-party-licenses.md, with the complete applicable musl inventory in musl-copyright.md.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.