Skip to content
666 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1//! Running what g1t hands the runner: a workflow job or agent work, with
2//! the same harness g1t's sandboxes run (this program, in another mode).
3//!
4//! - **In Docker** (the default): one container per job, removed when it
5//! ends, from the job's `container:` image or the runner's `--image`. A
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.6//! Linux build of the harness is mounted into it read-only; that and the
7//! job's own work volume, removed after, are its only mounts. It runs
8//! with every capability dropped but the few a build needs, no new
9//! privileges, a process, memory and CPU limit, and a network of its
10//! own. With nested-container isolation (sandbox.rs), all of this is on
11//! the runner's own Docker Engine, inside the runner's container, and a
12//! job that uses Docker gets an engine of its own (engine.rs).
13//! - **Directly** (`--no-docker`, deprecated): the harness runs as a child
14//! process, in a fresh folder under the runner's work folder that is
15//! removed after. Unsandboxed: the job's log says so, and a public
16//! repository's work is refused.
Fast pages, required checks on the branch, self-hosted runners, honest incidents17//!
18//! The runner's own credential never reaches either: a job only ever has
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.19//! its own short-lived token, as it would in g1t's sandbox. Credentials are
20//! passed by name (`-e NAME`), never on the command line.
Fast pages, required checks on the branch, self-hosted runners, honest incidents21
22use std::collections::BTreeMap;
23use std::path::{Path, PathBuf};
24use std::process::{Child, Command, Stdio};
25use std::time::{Duration, Instant};
26
27use anyhow::{Context, Result, bail};
28
29use super::api::Assignment;
30use super::config::Config;
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.31use super::sandbox::{self, Isolation};
Fast pages, required checks on the branch, self-hosted runners, honest incidents32use super::{log, update};
33
34/// The image workflow jobs run in when they name none: Debian with Node,
35/// git and Python, as g1t's sandbox has.
36pub const DEFAULT_IMAGE: &str = "node:24-bookworm";
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.37/// The most processes in a job's container, unless the runner says.
38pub const DEFAULT_PIDS_LIMIT: u32 = 4096;
39
40/// The capabilities a job's container keeps; Docker's others, and
41/// everything else, are dropped. A workflow job runs as its image's user,
42/// root for most CI images, and these are what installing packages and
43/// building need:
44///
45/// - `CHOWN`, `FOWNER`, `FSETID`, `DAC_OVERRIDE`: a package manager (apt,
46/// npm as root) writing files it then hands to another owner, keeping
47/// setgid bits, and root writing files it does not own.
48/// - `SETUID`, `SETGID`: apt and others drop to their own user (`_apt`) to
49/// download; with no new privileges, nothing can gain a user back.
50/// - `KILL`: a step stopping processes another user started.
51/// - `NET_BIND_SERVICE`: a test server on port 80 or 443.
52///
53/// Not kept, among others: `NET_RAW` (raw sockets, `ping`), `MKNOD`,
54/// `SYS_CHROOT`, `SETFCAP`, `AUDIT_WRITE`, and every `SYS_*` and `NET_ADMIN`.
55pub const JOB_CAPS: &[&str] = &["CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "SETUID", "SETGID", "KILL", "NET_BIND_SERVICE"];
Fast pages, required checks on the branch, self-hosted runners, honest incidents56
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.57/// Agent work's image runs as `node` and uses `sudo` once, to put its
58/// guardrails' policy where the agent cannot change it, and then removes
59/// its own sudo (guard.rs). sudo needs a setuid program to work and
60/// `AUDIT_WRITE` to log, so agent work keeps those and has no
61/// `no-new-privileges`; it still has every other limit.
62pub const AGENT_EXTRA_CAPS: &[&str] = &["AUDIT_WRITE"];
63
Fast pages, required checks on the branch, self-hosted runners, honest incidents64/// Work in progress.
65pub struct Running {
66 pub id: String,
67 pub kind: String,
68 pub name: String,
69 child: Child,
70 container: Option<String>,
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.71 /// The job's network and work volume, removed after.
72 network: Option<String>,
73 volume: Option<String>,
74 /// The job's own Docker Engine, when it may have one.
75 engine: Option<JobEngine>,
Fast pages, required checks on the branch, self-hosted runners, honest incidents76 folder: Option<PathBuf>,
77 /// Past its time limit and a grace for g1t to say so: stopped here.
78 deadline: Instant,
79}
80
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.81#[cfg(unix)]
82use super::engine::JobEngine;
83
84/// No job has an engine of its own off Unix.
85#[cfg(not(unix))]
86pub struct JobEngine;
87
88#[cfg(not(unix))]
89impl JobEngine {
90 fn remove(&self) {}
91 fn job_args(&self) -> Vec<String> {
92 Vec::new()
93 }
94}
95
96/// `docker`, at the runner's own engine when it has one.
97fn docker() -> Command {
98 #[cfg(unix)]
99 return super::engine::docker();
100 #[cfg(not(unix))]
101 Command::new("docker")
102}
103
104fn quietly(command: &mut Command) {
105 let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).status();
106}
107
Fast pages, required checks on the branch, self-hosted runners, honest incidents108impl Running {
109 pub fn overdue(&self) -> bool {
110 Instant::now() > self.deadline
111 }
112
113 /// Its exit code once it has ended.
114 pub fn ended(&mut self) -> Option<i32> {
115 match self.child.try_wait() {
116 Ok(Some(status)) => Some(status.code().unwrap_or(1)),
117 Ok(None) => None,
118 Err(_) => Some(1),
119 }
120 }
121
122 /// Stops it: the container, then the process and everything it started.
123 pub fn stop(&mut self) {
124 if let Some(container) = &self.container {
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.125 quietly(docker().args(["kill", container]));
126 }
127 if let Some(engine) = &self.engine {
128 quietly(docker().args(["kill", &engine.container]));
Fast pages, required checks on the branch, self-hosted runners, honest incidents129 }
130 let pid = self.child.id().to_string();
131 if cfg!(windows) {
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.132 quietly(Command::new("taskkill").args(["/T", "/F", "/PID", &pid]));
Fast pages, required checks on the branch, self-hosted runners, honest incidents133 } else {
134 // Its own process group (see `directly`): the steps' processes too.
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.135 quietly(Command::new("kill").args(["-TERM", &format!("-{pid}")]));
Fast pages, required checks on the branch, self-hosted runners, honest incidents136 }
137 let _ = self.child.kill();
138 let _ = self.child.wait();
139 }
140
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.141 /// Removes what it left: its container, its engine and that engine's
142 /// data, its network, its work volume, its folder.
Fast pages, required checks on the branch, self-hosted runners, honest incidents143 pub fn clean(&mut self) {
144 if let Some(container) = &self.container {
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.145 quietly(docker().args(["rm", "-f", "-v", container]));
146 }
147 if let Some(engine) = self.engine.take() {
148 engine.remove();
Fast pages, required checks on the branch, self-hosted runners, honest incidents149 }
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.150 if let Some(network) = &self.network {
151 quietly(docker().args(["network", "rm", network]));
152 }
153 if let Some(volume) = &self.volume {
154 quietly(docker().args(["volume", "rm", "-f", volume]));
155 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents156 if let Some(folder) = &self.folder {
157 let _ = std::fs::remove_dir_all(folder);
158 }
159 }
160}
161
162/// A name Docker takes, from a job's id.
163fn container_name(id: &str) -> String {
164 let clean: String = id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' { c } else { '-' }).collect();
165 format!("g1t-{clean}")
166}
167
168/// The environment the harness gets for this work.
169fn harness_env(config: &Config, work: &Assignment) -> Result<BTreeMap<String, String>> {
170 let mut env = BTreeMap::new();
171 match work.kind.as_str() {
172 "workflow" => {
173 let Some(token) = &work.token else { bail!("the job came without its token") };
174 env.insert("MODE".into(), "actions".into());
175 env.insert("G1T_API".into(), config.api.clone());
176 env.insert("ACTIONS_JOB".into(), work.id.clone());
177 env.insert("ACTIONS_TOKEN".into(), token.clone());
178 }
179 "agent" => {
180 for (name, value) in work.env.clone().unwrap_or_default() {
181 let value = match value {
182 serde_json::Value::String(text) => text,
183 other => other.to_string(),
184 };
185 env.insert(name, value);
186 }
187 if !env.contains_key("MODE") && !env.contains_key("PROMPT") {
188 bail!("the agent work came without its environment");
189 }
190 // Where its API calls go: this installation's.
191 env.insert("G1T_API".into(), config.api.clone());
192 }
193 other => bail!("this runner does not know work of kind {other}; update it"),
194 }
195 // The machine is the workspace's own: no mining watch.
196 env.insert("G1T_ABUSE".into(), "off".into());
197 Ok(env)
198}
199
200/// Starts work. The harness reports a workflow job's steps and log itself;
201/// the runner tells g1t how the process ended.
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.202pub fn start(config: &Config, isolation: Isolation, folder: &Path, work: &Assignment) -> Result<Running> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents203 let env = harness_env(config, work)?;
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.204 if isolation == Isolation::Host {
205 refuse_public_on_host(work)?;
206 return directly(config, work, env);
207 }
208 in_docker(config, isolation, folder, work, env)
209}
210
211/// Host mode does not take a public repository's work: anyone can propose
212/// code to one, and here it would run on the machine itself.
213fn refuse_public_on_host(work: &Assignment) -> Result<()> {
214 match work.visibility.as_deref() {
215 Some("public") => bail!("{}", sandbox::host_mode_refuses(&work.repo)),
216 Some(_) => Ok(()),
217 None => {
218 log(&format!("g1t did not say whether {} is public (an older g1t), so it runs; host mode refuses public repositories' work once it does.", work.repo));
219 Ok(())
220 }
221 }
222}
223
224/// Each job container's limits.
225#[derive(Debug, Clone, PartialEq)]
226pub struct Limits {
227 pub memory: String,
228 pub cpus: String,
229 pub pids: u32,
230}
231
232/// Three quarters of `MemTotal` in `/proc/meminfo`, in MiB, which under
233/// Sysbox is the runner container's own memory.
234fn default_memory(meminfo: &str) -> Option<String> {
235 let kb: u64 = meminfo.lines().find_map(|l| l.strip_prefix("MemTotal:"))?.trim().trim_end_matches("kB").trim().parse().ok()?;
236 let mib = kb / 1024 * 3 / 4;
237 (mib >= 256).then(|| format!("{mib}m"))
238}
239
240pub fn limits(config: &Config) -> Limits {
241 let memory = config
242 .memory
243 .clone()
244 .or_else(|| default_memory(&std::fs::read_to_string("/proc/meminfo").unwrap_or_default()))
245 .unwrap_or_else(|| "4g".into());
246 let cpus = config.cpus.clone().unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(2).to_string());
247 Limits { memory, cpus, pids: config.pids_limit.unwrap_or(DEFAULT_PIDS_LIMIT) }
248}
249
250/// Everything a job's `docker run` is made from.
251pub struct Plan<'a> {
252 pub kind: &'a str,
253 pub name: &'a str,
254 pub runner: &'a str,
255 pub image: &'a str,
256 pub harness: &'a Path,
257 pub network: &'a str,
258 /// The job's work volume, for a workflow job's `/home/runner`.
259 pub volume: Option<&'a str>,
260 pub limits: &'a Limits,
261 /// The names of the harness's variables, whose values come from the
262 /// runner's process, never its command line.
263 pub env_names: Vec<&'a str>,
264 /// What the job's own engine adds (`JobEngine::job_args`).
265 pub engine_args: Vec<String>,
266}
267
268/// The arguments of `docker` that start a job's container.
269pub fn run_args(plan: &Plan) -> Vec<String> {
270 let mut args: Vec<String> = Vec::new();
271 fn add(args: &mut Vec<String>, items: &[&str]) {
272 args.extend(items.iter().map(|s| s.to_string()));
273 }
274 add(&mut args, &["run", "--rm", "--name", plan.name, "--label", &format!("sh.g1t.runner={}", plan.runner)]);
275 add(&mut args, &["--pull", "missing", "--init"]);
276 // The walls.
277 add(&mut args, &["--cap-drop", "ALL"]);
278 let mut caps: Vec<&str> = JOB_CAPS.to_vec();
279 if plan.kind == "agent" {
280 caps.extend(AGENT_EXTRA_CAPS);
281 } else {
282 add(&mut args, &["--security-opt", "no-new-privileges"]);
283 }
284 for cap in caps {
285 add(&mut args, &["--cap-add", cap]);
286 }
287 let pids = plan.limits.pids.to_string();
288 add(&mut args, &["--pids-limit", &pids, "--memory", &plan.limits.memory, "--memory-swap", &plan.limits.memory, "--cpus", &plan.limits.cpus]);
289 add(&mut args, &["--network", plan.network, "--ipc", "private"]);
290 // The job is in its `container:` image already: the harness inside
291 // does not start it again (actions/containers.rs).
292 add(&mut args, &["-e", "G1T_JOB_CONTAINER=1"]);
293 for name in &plan.env_names {
294 add(&mut args, &["-e", name]);
295 }
296 // Its only mounts: the harness, read-only, and its own volume.
297 add(&mut args, &["-v", &format!("{}:/opt/g1t/g1t-runner:ro", plan.harness.display())]);
298 if let Some(volume) = plan.volume {
299 add(&mut args, &["-v", &format!("{volume}:/home/runner")]);
300 }
301 args.extend(plan.engine_args.iter().cloned());
302 add(&mut args, &["--entrypoint", "/opt/g1t/g1t-runner", plan.image]);
303 args
Fast pages, required checks on the branch, self-hosted runners, honest incidents304}
305
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.306fn in_docker(config: &Config, isolation: Isolation, folder: &Path, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> {
Fast pages, required checks on the branch, self-hosted runners, honest incidents307 let image = match work.kind.as_str() {
308 "agent" => match config.agent_image.clone().or_else(|| update::agent_image(config)) {
309 Some(image) => image,
310 None => bail!(
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.311 "agent work needs an image with git, Node and the agent's CLI: register this runner with --agent-image IMAGE"
Fast pages, required checks on the branch, self-hosted runners, honest incidents312 ),
313 },
314 _ => work.image.clone().or_else(|| config.image.clone()).unwrap_or_else(|| DEFAULT_IMAGE.to_owned()),
315 };
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.316 #[cfg(unix)]
317 if super::engine::in_use() {
318 super::engine::ensure_running()?;
319 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents320 let harness = update::linux_harness(config, folder)?;
321 let name = container_name(&work.id);
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.322 // What an earlier run of this job may have left.
323 quietly(docker().args(["rm", "-f", "-v", &name]));
324 quietly(docker().args(["network", "rm", &name]));
325 let output = docker()
326 .args(["network", "create", "--label", &format!("sh.g1t.runner={}", config.runner), &name])
327 .output()
328 .context("could not run docker: is Docker installed and running?")?;
329 if !output.status.success() {
330 bail!("could not make the job's network: {}", String::from_utf8_lossy(&output.stderr).trim());
Fast pages, required checks on the branch, self-hosted runners, honest incidents331 }
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.332 let volume = (work.kind == "workflow").then(|| format!("{name}-work"));
333 if let Some(volume) = &volume {
334 quietly(docker().args(["volume", "rm", "-f", volume]));
335 quietly(docker().args(["volume", "create", "--label", &format!("sh.g1t.runner={}", config.runner), volume]));
336 }
337 // A job's own Docker, only inside the runner's own sandbox: on a
338 // shared engine it would be the machine's.
339 let engine: Option<JobEngine> = job_engine(config, isolation, &name);
340 let limits = limits(config);
341 let plan = Plan {
342 kind: &work.kind,
343 name: &name,
344 runner: &config.runner,
345 image: &image,
346 harness: &harness,
347 network: &name,
348 volume: volume.as_deref(),
349 limits: &limits,
350 env_names: env.keys().map(String::as_str).collect(),
351 engine_args: engine.as_ref().map(JobEngine::job_args).unwrap_or_default(),
352 };
353 let mut command = docker();
354 command.args(run_args(&plan));
Fast pages, required checks on the branch, self-hosted runners, honest incidents355 command.envs(&env);
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.356 log(&format!(
357 "Running {} {} in {image}: its own container and network, capabilities dropped, {} memory, {} CPUs, {} processes{}",
358 work.kind,
359 work.name,
360 limits.memory,
361 limits.cpus,
362 limits.pids,
363 if engine.is_some() { ", Docker of its own when it uses it" } else { "" }
364 ));
365 let child = match command.spawn() {
366 Ok(child) => child,
367 Err(error) => {
368 if let Some(engine) = &engine {
369 engine.remove();
370 }
371 quietly(docker().args(["network", "rm", &name]));
372 return Err(error).context("could not run docker: is Docker installed and running?");
373 }
374 };
375 Ok(Running {
376 id: work.id.clone(),
377 kind: work.kind.clone(),
378 name: work.name.clone(),
379 child,
380 container: Some(name.clone()),
381 network: Some(name),
382 volume,
383 engine,
384 folder: None,
385 deadline: deadline(work),
386 })
387}
388
389#[cfg(unix)]
390fn job_engine(config: &Config, isolation: Isolation, name: &str) -> Option<JobEngine> {
391 if isolation != Isolation::NestedContainer || !config.job_docker {
392 return None;
393 }
394 let image = config.job_docker_image.as_deref().unwrap_or(super::engine::DEFAULT_JOB_ENGINE_IMAGE);
395 match JobEngine::prepare(name, &config.runner, image) {
396 Ok(engine) => Some(engine),
397 Err(error) => {
398 log(&format!("This job has no Docker of its own: {error:#}"));
399 None
400 }
401 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents402}
403
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.404#[cfg(not(unix))]
405fn job_engine(_config: &Config, _isolation: Isolation, _name: &str) -> Option<JobEngine> {
406 None
407}
408
Fast pages, required checks on the branch, self-hosted runners, honest incidents409fn directly(config: &Config, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> {
410 let me = std::env::current_exe().context("could not find this program to run the job")?;
411 let mut command = Command::new(me);
412 let mut folder = None;
413 if work.kind == "workflow" {
414 // GitHub's layout, in a folder of its own.
415 let root = config.work_dir.join(container_name(&work.id));
416 let _ = std::fs::remove_dir_all(&root);
417 std::fs::create_dir_all(&root).with_context(|| format!("could not make {}", root.display()))?;
418 command.env("G1T_RUNNER_ROOT", &root);
419 folder = Some(root);
420 } else if !cfg!(target_os = "linux") || std::fs::create_dir_all("/work").is_err() {
421 // Agent work expects g1t's sandbox layout (/work), which only a
422 // Linux machine (or a container) set aside for it has.
423 bail!("agent work runs in Docker, or directly on Linux where /work can be written; this runner can do neither");
424 }
425 // Its own variables, less anything of the runner's.
426 for (name, _) in std::env::vars_os() {
427 if name.to_string_lossy().starts_with("G1T_RUNNER_") && name != "G1T_RUNNER_ROOT" {
428 command.env_remove(&name);
429 }
430 }
431 command.envs(&env);
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.432 // The job's log says it ran unsandboxed (actions/mod.rs).
433 command.env(JOB_NOTICE, sandbox::HOST_MODE_IN_JOB);
Fast pages, required checks on the branch, self-hosted runners, honest incidents434 // A group of its own, so stopping it stops what its steps started.
435 #[cfg(unix)]
436 std::os::unix::process::CommandExt::process_group(&mut command, 0);
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.437 log(&format!("Running {} {} on this machine, unsandboxed (host mode, deprecated)", work.kind, work.name));
Fast pages, required checks on the branch, self-hosted runners, honest incidents438 let child = command.spawn().context("could not start the job's process")?;
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.439 Ok(Running {
440 id: work.id.clone(),
441 kind: work.kind.clone(),
442 name: work.name.clone(),
443 child,
444 container: None,
445 network: None,
446 volume: None,
447 engine: None,
448 folder,
449 deadline: deadline(work),
450 })
Fast pages, required checks on the branch, self-hosted runners, honest incidents451}
452
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.453/// A warning the harness puts at the top of the job's log.
454pub const JOB_NOTICE: &str = "G1T_JOB_NOTICE";
455
Fast pages, required checks on the branch, self-hosted runners, honest incidents456fn deadline(work: &Assignment) -> Instant {
457 Instant::now() + Duration::from_secs(u64::from(work.timeout_minutes.max(1)) * 60 + 10 * 60)
458}
459
460/// Whether Docker answers.
461pub fn docker_ready() -> bool {
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.462 docker()
Fast pages, required checks on the branch, self-hosted runners, honest incidents463 .args(["version", "--format", "{{.Server.Version}}"])
464 .stdout(Stdio::null())
465 .stderr(Stdio::null())
466 .status()
467 .is_ok_and(|status| status.success())
468}
469
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.470/// Removes what jobs of this runner's left behind, if it stopped while one ran.
471pub fn sweep(config: &Config) {
472 let label = format!("label=sh.g1t.runner={}", config.runner);
473 for (list, remove) in [
474 (vec!["ps", "-aq", "--filter", &label], vec!["rm", "-f", "-v"]),
475 (vec!["network", "ls", "-q", "--filter", &label], vec!["network", "rm"]),
476 (vec!["volume", "ls", "-q", "--filter", &label], vec!["volume", "rm", "-f"]),
477 ] {
478 let Ok(output) = docker().args(&list).output() else { return };
479 let ids: Vec<String> = String::from_utf8_lossy(&output.stdout).split_whitespace().map(str::to_owned).collect();
480 if !ids.is_empty() {
481 quietly(docker().args(&remove).args(&ids));
482 }
483 }
484}
485
Fast pages, required checks on the branch, self-hosted runners, honest incidents486#[cfg(test)]
487mod tests {
488 use super::*;
489 use serde_json::json;
490
491 fn config() -> Config {
492 Config {
493 url: "https://g1t.sh".into(),
494 api: "https://api.g1t.sh".into(),
495 runner: "rnr_1".into(),
496 name: "a".into(),
497 credential: "g1tr_secret".into(),
498 workspace: "acme".into(),
499 repo: None,
500 group: None,
501 labels: vec![],
502 ephemeral: false,
503 work_dir: PathBuf::from("/tmp/w"),
504 docker: true,
505 image: None,
506 agent_image: None,
507 harness: None,
508 auto_update: false,
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.509 memory: None,
510 cpus: None,
511 pids_limit: None,
512 job_docker: true,
513 job_docker_image: None,
Fast pages, required checks on the branch, self-hosted runners, honest incidents514 }
515 }
516
517 fn assignment(kind: &str) -> Assignment {
518 serde_json::from_value(json!({
519 "kind": kind, "id": "job_1", "name": "build", "repo": "acme/web", "timeout_minutes": 60,
520 "token": "jobtoken", "env": { "MODE": "checks", "G1T_TOKEN": "g1t_run", "G1T_API": "https://elsewhere" }
521 }))
522 .unwrap()
523 }
524
525 #[test]
526 fn a_workflow_job_gets_its_own_token_and_never_the_runners() {
527 let env = harness_env(&config(), &assignment("workflow")).unwrap();
528 assert_eq!(env["MODE"], "actions");
529 assert_eq!(env["ACTIONS_JOB"], "job_1");
530 assert_eq!(env["ACTIONS_TOKEN"], "jobtoken");
531 assert_eq!(env["G1T_API"], "https://api.g1t.sh");
532 assert!(env.values().all(|value| !value.contains("g1tr_")));
533 }
534
535 #[test]
536 fn agent_work_gets_the_sandboxs_environment_and_this_api() {
537 let env = harness_env(&config(), &assignment("agent")).unwrap();
538 assert_eq!(env["MODE"], "checks");
539 assert_eq!(env["G1T_TOKEN"], "g1t_run");
540 assert_eq!(env["G1T_API"], "https://api.g1t.sh");
541 assert_eq!(env["G1T_ABUSE"], "off");
542 }
543
544 #[test]
545 fn unknown_work_is_refused() {
546 assert!(harness_env(&config(), &assignment("deploy")).is_err());
547 }
548
549 #[test]
550 fn container_names_are_dockers() {
551 assert_eq!(container_name("job_01ab"), "g1t-job_01ab");
552 assert_eq!(container_name("a/b c"), "g1t-a-b-c");
553 }
The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so.554
555 fn args_for(kind: &str, engine_args: Vec<String>) -> (Vec<String>, BTreeMap<String, String>) {
556 let config = config();
557 let env = harness_env(&config, &assignment(kind)).unwrap();
558 let limits = Limits { memory: "6144m".into(), cpus: "4".into(), pids: 4096 };
559 let harness = PathBuf::from("/usr/local/bin/g1t-runner");
560 let plan = Plan {
561 kind,
562 name: "g1t-job_1",
563 runner: "rnr_1",
564 image: DEFAULT_IMAGE,
565 harness: &harness,
566 network: "g1t-job_1",
567 volume: (kind == "workflow").then_some("g1t-job_1-work"),
568 limits: &limits,
569 env_names: env.keys().map(String::as_str).collect(),
570 engine_args,
571 };
572 (run_args(&plan), env.clone())
573 }
574
575 fn value_of<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> {
576 args.windows(2).filter(|w| w[0] == flag).map(|w| w[1].as_str()).collect()
577 }
578
579 #[test]
580 fn a_jobs_container_is_walled_in() {
581 let (args, _) = args_for("workflow", Vec::new());
582 assert_eq!(args[0], "run");
583 assert!(args.contains(&"--rm".to_owned()));
584 assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]);
585 assert_eq!(value_of(&args, "--cap-add"), JOB_CAPS.to_vec());
586 assert_eq!(value_of(&args, "--security-opt"), vec!["no-new-privileges"]);
587 assert_eq!(value_of(&args, "--pids-limit"), vec!["4096"]);
588 assert_eq!(value_of(&args, "--memory"), vec!["6144m"]);
589 assert_eq!(value_of(&args, "--memory-swap"), vec!["6144m"]);
590 assert_eq!(value_of(&args, "--cpus"), vec!["4"]);
591 assert_eq!(value_of(&args, "--network"), vec!["g1t-job_1"]);
592 for forbidden in ["--privileged", "--pid", "--userns", "--device", "host"] {
593 assert!(!args.iter().any(|a| a == forbidden), "{forbidden} in {args:?}");
594 }
595 assert!(!JOB_CAPS.iter().any(|c| c.starts_with("SYS_") || *c == "NET_ADMIN" || *c == "NET_RAW"));
596 }
597
598 #[test]
599 fn a_jobs_only_mounts_are_the_harness_and_its_own_volume() {
600 let (args, _) = args_for("workflow", Vec::new());
601 let mounts = value_of(&args, "-v");
602 assert_eq!(mounts, vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro", "g1t-job_1-work:/home/runner"]);
603 assert!(!args.iter().any(|a| a.contains("docker.sock")), "no engine socket: {args:?}");
604 assert!(!args.iter().any(|a| a == "--mount"));
605 }
606
607 #[test]
608 fn credentials_go_by_name_and_the_runners_never() {
609 let (args, env) = args_for("workflow", Vec::new());
610 let passed = value_of(&args, "-e");
611 assert!(passed.contains(&"ACTIONS_TOKEN"));
612 // Never a value on the command line, and never the runner's credential.
613 assert!(!args.iter().any(|a| a.contains("jobtoken")));
614 assert!(!args.iter().any(|a| a.contains("g1tr_")));
615 assert!(env.values().all(|v| !v.contains("g1tr_secret")));
616 assert!(!env.contains_key("G1T_RUNNER_TOKEN"));
617 }
618
619 #[test]
620 fn agent_work_keeps_sudo_for_its_guardrails_and_every_other_wall() {
621 let (args, _) = args_for("agent", Vec::new());
622 assert!(value_of(&args, "--security-opt").is_empty());
623 assert!(value_of(&args, "--cap-add").contains(&"AUDIT_WRITE"));
624 assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]);
625 assert_eq!(value_of(&args, "-v"), vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro"]);
626 }
627
628 #[test]
629 fn a_job_with_docker_gets_a_socket_of_its_own_not_the_runners() {
630 let engine_args = vec!["-v".into(), "/run/g1t-runner/jobs/g1t-job_1/proxy:/run/g1t-docker:ro".into(), "-e".into(), "DOCKER_HOST=unix:///run/g1t-docker/docker.sock".into()];
631 let (args, _) = args_for("workflow", engine_args);
632 let mounts = value_of(&args, "-v");
633 assert!(mounts.contains(&"/run/g1t-runner/jobs/g1t-job_1/proxy:/run/g1t-docker:ro"));
634 assert!(!mounts.iter().any(|m| m.starts_with("/var/run/docker.sock") || m.starts_with("/run/g1t-runner/docker.sock")));
635 // The entrypoint and image stay last.
636 assert_eq!(&args[args.len() - 3..], ["--entrypoint", "/opt/g1t/g1t-runner", DEFAULT_IMAGE]);
637 }
638
639 #[test]
640 fn limits_default_sensibly() {
641 assert_eq!(default_memory("MemTotal: 16384000 kB\nMemFree: 1 kB\n"), Some("12000m".into()));
642 assert_eq!(default_memory(""), None);
643 let mut config = config();
644 config.memory = Some("2g".into());
645 config.pids_limit = Some(100);
646 let limits = limits(&config);
647 assert_eq!(limits.memory, "2g");
648 assert_eq!(limits.pids, 100);
649 assert!(limits.cpus.parse::<f64>().unwrap() >= 1.0);
650 }
651
652 #[test]
653 fn host_mode_refuses_a_public_repositorys_work() {
654 let mut work = assignment("workflow");
655 work.visibility = Some("public".into());
656 let refused = refuse_public_on_host(&work).unwrap_err().to_string();
657 assert!(refused.contains("acme/web is public"), "{refused}");
658 work.visibility = Some("private".into());
659 assert!(refuse_public_on_host(&work).is_ok());
660 work.visibility = None;
661 assert!(refuse_public_on_host(&work).is_ok());
662 // And refuses it before anything runs.
663 work.visibility = Some("public".into());
664 assert!(start(&config(), Isolation::Host, Path::new("/tmp"), &work).is_err());
665 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents666}