Executes Scribe flow jobs for a web API.
scribe-runner is one binary with the Scribe client compiled in. It is
started with three things — the web address, the runner's id and an API token
— checks in with the web API, asks it for work, and executes each job in a
workspace it keeps per repository and branch. It needs no scribe executable
on the machine and reads no configuration file; every setting is an argument
or an environment variable.
The web registration guide assumes scribe-runner is installed and links to no
download. Take the scribe-runner-<platform> archive from the manual build
workflow, or build it with scripts/install.sh (scripts/install.ps1 on
Windows) beside the other executables. scribe-runner has no --json;
--log-format json formats its logs.
See flows for definitions, runs, runner registration and the runner's behaviour (workspaces, shells, environment, stopping), and the web interface guide for the Flows tab.
scribe | scribe-server | scribe-auth | scribe-ctl | scribe-gateway
Reading this reference
Command synopses below come from this checkout’s built --help output.
Angle brackets denote values, square brackets optional arguments. [OPTIONS]
includes the logging options below and the command's own options. Logging
options work before or after a subcommand.
Run scribe-runner --help for the command list, scribe-runner COMMAND --help
for command help, and scribe-runner --version for the build version.
Commands
Usage: scribe-runner [OPTIONS] <COMMAND>
Commands:
run Run in the foreground; Ctrl+C finishes the current job first, a second one aborts it
service Install or remove the service that runs this runner with the machineUsage: scribe-runner service [OPTIONS] <COMMAND>
Commands:
install Install as a service that starts with the machine (systemd, launchd or the Windows service manager)
uninstall Stop and remove the service installed for a runnerRunner options
run and service install take the same runner options:
| Argument or option | Meaning |
|---|---|
--server <URL> | Web address, as in the browser: https://scribe.example (or SCRIBE_RUNNER_SERVER) |
--runner <ID> | The runner's id from its registration (or SCRIBE_RUNNER_ID) |
--token-file <FILE> | Read the API token (runners:write and flows:read) from a file; without it, SCRIBE_RUNNER_TOKEN |
--workdir <DIR> | Where workspaces, scripts and logs live (default: the user configuration root, runner/; or SCRIBE_RUNNER_WORKDIR) |
--ca <FILE> | CA certificates (PEM) that verify an https:// address instead of the public roots; repeatable |
--poll-interval <SECONDS> | Seconds between asks for work while idle [default: 5] |
--keep-workspaces <COUNT> | Branch workspaces kept per repository; the least recently used beyond this are removed [default: 8] |
The address is the one the browser uses, with any path prefix the deployment
serves the web interface under; the runner appends /api/v1. An https://
address is verified against the public web roots, or against the --ca
files alone when any are given, so a private issuer is one flag (as
scribe --tls-ca works); HTTPS_PROXY is honoured. The client offers the
hybrid post-quantum key exchange X25519MLKEM768 first and also accepts
classical X25519, which a proxy in front of the web API may offer alone.
http:// is accepted only for a loopback host (localhost or a loopback IP
such as 127.0.0.1 or [::1]); any other plaintext address is refused:
scribe-runner: "http://scribe.example" is plaintext on a network address; use https:// (http:// is accepted only for localhost)The token comes from --token-file, or from SCRIBE_RUNNER_TOKEN when no
file is named; there is no option that takes the token itself, because
process listings show arguments to every account on the machine. It must
carry runners:write and flows:read, and its owner must be the runner's
owner or a Maintainer of every repository the runner serves. It is never
logged and never passed to a step (see Trust boundary).
The token is for the web API only; it never reaches a storage server.
The runner also has its own device key, device-key.pem, created on first
use in a private file (mode 0600 on Unix): in the directory of the
--token-file, or in runner-credentials/<id>/ under the user configuration
root when the token comes from SCRIBE_RUNNER_TOKEN. Only its public key
leaves the machine. At check-in the runner sends that public key, and the web
API has auth derive a credential for it from the token's automation
credential (reader ceiling, same account and expiry, revoked with the token)
and returns the credential certificate with the storage and auth endpoints
and their CAs. The runner fetches its access certificates from auth itself
and keeps every certificate in memory. Those CAs are all it trusts: a CA
handover is followed in memory, and after a job that met any other CA change
the runner checks in again for the CAs the API delivers now (see
CA rotation and handover). A web deployment without a TLS auth
service configured answers check-in with 409 runner_auth_unconfigured, which
the runner logs while it keeps retrying. On Unix a token file or
device-key.pem that other accounts can access is refused ("… is accessible
to other accounts (mode 644); run chmod 600 on it"), as is one in a directory
other accounts can write that is not sticky; on Windows one whose access list lets another account
read it, or whose directory lets another account delete entries, is refused the same way. See
Registration and tokens and
identity.
The work directory holds workspaces/ (one directory per repository and
branch, or per workspaces: folder a flow declares, and workspaces.json
describing them; each is registered on the server with the label
<runner name>@<host> and the tags runner, runner-id=<id>), jobs/
(scripts of the job in progress, and the archives
its bundles are packed into before upload), logs/ for a service
installation, and scribe-runner.pid while a runner is up. It never holds
the token or the device key: a service installation keeps both apart, under
runner-credentials/<id>/ in the user configuration root. It is locked while a runner uses it; a second runner on the
same machine needs its own. Without --workdir it is runner/<id> under the
user configuration root (~/.config/scribe, ~/Library/Application Support/Scribe, or %LOCALAPPDATA%\Scribe); put a build machine's on the
disk with room for its projects.
Bundles
A job whose flow declares bundles: ends with a Publish bundles step. The
runner resolves the rules against the workspace, walking only the directories
the rules can reach, packs archive targets into stored (uncompressed) zips
under jobs/, and uploads three files at a time in parts of the size the API
names, resuming after a lost connection from the byte the API has. Each file
is hashed (BLAKE3) as it is read for sending and the digest goes with its last
part, so the web can verify what it stored. The step's log
says what matched, what was packed and how long each upload took; a rule
that matches nothing is noted, not an error. The job's timeout-minutes
covers publishing. See Bundles for the rule syntax
and retention.
Logging options
| Argument or option | Meaning |
|---|---|
--log-level <LEVEL> | off|error|warn|info|debug|trace (default info) |
--log-filter <FILTER> | Target filter, e.g. 'info,scribe_client=debug' (or SCRIBE_LOG) |
--log-format <FORMAT> | text|json |
--log-dir <DIR> | Write daily log files here instead of stderr |
--log-keep <COUNT> | Daily log files kept in --log-dir (default 14) |
-v, --verbose... | -v for debug, -vv for trace |
-q, --quiet | Errors only |
--timings | Report nested phase timings to the diagnostic sink |
The runner's own diagnostics (check-in, claims, job outcomes, failures to reach the API) go to stderr or to daily files. A step's output is not diagnostics: it goes to the run's log in the web interface, not to the runner's log, streamed as the step writes it over one request held open per step (reopened after a silence, a lost connection, a megabyte or five minutes, resuming where the API confirmed). What a step wrote while the API was unreachable is kept, up to 8 MiB, and sent when it is back.
run
scribe-runner run --server https://scribe.example --runner <id> --token-file /etc/scribe-runner/tokenRuns in the foreground until stopped. Ctrl+C or SIGTERM lets the job in progress finish, then exits; a second one aborts the job, which is reported failed with "the runner was stopped". A runner that cannot reach the API, is disabled, or whose token is rejected logs why and keeps trying at a slower pace; it never exits over a state the API named, so it picks up as soon as the cause is fixed.
While the runner is up — started this way or by a service — the root of its
work directory holds scribe-runner.pid with the process id, one line, and
the file is removed when the runner exits. It answers "is a runner using
this directory, and which process" without a process listing; a file that is
still there while no runner runs was left by a crash, and the next start says
so as it replaces it. The work directory is also locked while a runner uses
it, so a second runner started on the same directory is refused rather than
sharing it.
service install
scribe-runner service install --server https://scribe.example --runner <id> --token-file FILE [--workdir DIR] [--system [--user NAME]]| Option | Meaning |
|---|---|
--system | A system-wide service rather than one for the current user (needs administrator rights) |
--user <NAME> | The account a system service runs as (systemd only) |
SCRIBE_RUNNER_TOKEN in place of --token-file works too. Checks the
registration and token in with the API first, so a typo is a message rather
than a service that restarts forever. Then copies the token to
runner-credentials/<id>/token under the installing user's configuration
root (~/.config/scribe, ~/Library/Application Support/Scribe, or
%LOCALAPPDATA%\Scribe), in a directory and file readable by that user only
and outside the work directory, and registers a service that runs
scribe-runner run with the same options and --token-file pointing at it.
The service's device key is runner-credentials/<id>/device-key.pem: the
installing check-in creates it there (not beside the --token-file given), so
the key the web API had certified is the one the service uses. The command
prints the service name, the runner, the unit file, the stored token and the
work directory.
A --workdir that contains that directory, or lies inside it, is refused.
- Linux: a systemd unit,
~/.config/systemd/user/scribe-runner-<id>.serviceor with--system/etc/systemd/system/…, enabled and started withsystemctl [--user] enable --now. A user service stops with the login session unlessloginctl enable-lingeris on; the command says so. The unit restarts the runner after a failure, and its stop timeout is an hour so a running job can finish. - macOS: a launchd agent,
~/Library/LaunchAgents/com.flyingrat.scribe-runner-<id>.plist, or with--systema daemon under/Library/LaunchDaemons, bootstrapped at once and kept alive; output goes to<workdir>/logs/launchd.logand the runner's own daily files beside it. - Windows: a service created with
sc.exe, started automatically at boot, logging to<workdir>/logs. It runs as LocalSystem; to build under a user account,sc.exe config scribe-runner-<id> obj= .\user password= …. The service manager starts the executable with the hiddenservice entrycommand.
The service name is scribe-runner- followed by the first eight characters of
the runner id. Installing again for the same runner replaces the service.
service uninstall
scribe-runner service uninstall --runner <id> [--system]Stops and removes the service installed for the runner (--system when it
was installed that way), then deletes the token service install stored and
its runner-credentials/<id> directory, device key included. The work directory and its
workspaces are left in place; remove them by hand when the machine is done
with them.
Removing the runner's registration in the web interface is separate.
Environment
| Variable | Meaning |
|---|---|
SCRIBE_RUNNER_SERVER | Default for --server |
SCRIBE_RUNNER_ID | Default for --runner |
SCRIBE_RUNNER_TOKEN | The token, when --token-file is not given; the device key is then kept in runner-credentials/<id>/ |
SCRIBE_RUNNER_WORKDIR | Default for --workdir |
SCRIBE_LOG | Diagnostic filter, as for every Scribe executable |
SCRIBE_TRANSFER_JOBS | Parallel object transfers during checkout, 1–8 (default 4) |
Trust boundary
A runner executes code written by anyone who can write the flows of a
repository it is bound to: every run: step is such code, running as the
runner's OS account. Treat binding a runner to a repository as giving that
repository's Writers a shell on the machine, under that account.
- Steps get no runner credentials. A step starts from an empty
environment plus
PATH,HOME,USER,LOGNAME,SHELL,LANG,LANGUAGE,LC_*,TZ,TERM,TMPDIR,TEMPandTMP, and on WindowsSystemRoot,SystemDrive,windir,ComSpec,PATHEXT,OS,USERNAME,USERPROFILE,HOMEDRIVE,HOMEPATH,APPDATA,LOCALAPPDATA,ProgramData,ProgramFiles,ProgramFiles(x86),ProgramW6432,CommonProgramFiles,CommonProgramFiles(x86),CommonProgramW6432,NUMBER_OF_PROCESSORSandPROCESSOR_ARCHITECTURE; then the run's variables and the flow's and job'senv. Nothing else the runner was started with reaches a step — notSCRIBE_RUNNER_TOKEN, not anotherSCRIBE_*setting, not a cloud or signing key the operator exported. A build that needs a host variable sets it in the flow'senv. - The account is the boundary. Job steps run as the runner's OS user, so
a step can still read every file, and inspect every process, that account
can: the token file,
device-key.pem, the runner's own environment (/proc/<pid>/environ,ps eww) and memory, the account's SSH keys and caches. Keeping the token and the key out of the work directory and out of step environments stops them leaking by accident, not a step written to find them. Run runners under a dedicated low-privilege OS account that holds nothing else of value, and do not reuse a developer's or an administrator's account. - Choose the token's account for what it exposes. The runner's token and its device key can be taken by such a step. Whoever holds them can read what the token's owner can read (the derived credential's ceiling is reader), and can act as that owner's runners — claim their jobs, report results and upload bundles — for the repositories it reaches. Use the token of an account (a shared CI account, for instance) whose read access you are comfortable exposing to the Writers of every repository bound to runners it drives, and bind runners of one token only to repositories with the same set of trusted Writers.
Exit status
0 after a clean stop; 1 for an argument, installation or startup failure,
reported on stderr. A job's failure is reported to the web API, not through
the exit status: the runner keeps running.