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 incidents | 1 | //! 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 | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 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 incidents | 17 | //! |
| 18 | //! The runner's own credential never reaches either: a job only ever has | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 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 incidents | 21 | |
| 22 | use std::collections::BTreeMap; | |
| 23 | use std::path::{Path, PathBuf}; | |
| 24 | use std::process::{Child, Command, Stdio}; | |
| 25 | use std::time::{Duration, Instant}; | |
| 26 | ||
| 27 | use anyhow::{Context, Result, bail}; | |
| 28 | ||
| 29 | use super::api::Assignment; | |
| 30 | use super::config::Config; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 31 | use super::sandbox::{self, Isolation}; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 32 | use 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. | |
| 36 | pub const DEFAULT_IMAGE: &str = "node:24-bookworm"; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 37 | /// The most processes in a job's container, unless the runner says. |
| 38 | pub 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`. | |
| 55 | pub 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 incidents | 56 | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 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. | |
| 62 | pub const AGENT_EXTRA_CAPS: &[&str] = &["AUDIT_WRITE"]; | |
| 63 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 64 | /// Work in progress. |
| 65 | pub struct Running { | |
| 66 | pub id: String, | |
| 67 | pub kind: String, | |
| 68 | pub name: String, | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 69 | pub repo: String, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 70 | child: Child, |
| 71 | container: Option<String>, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 72 | /// The job's network and work volume, removed after. |
| 73 | network: Option<String>, | |
| 74 | volume: Option<String>, | |
| 75 | /// The job's own Docker Engine, when it may have one. | |
| 76 | engine: Option<JobEngine>, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 77 | folder: Option<PathBuf>, |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 78 | /// The job's own VM, for a job native to this OS (native.rs); `child` |
| 79 | /// is then `wsb exec`, whose output carries the job's exit code. | |
| 80 | native: Option<WsbJob>, | |
| 81 | /// How it ended, once read: a native job's code is read once from | |
| 82 | /// `wsb exec`'s output. | |
| 83 | exit: Option<i32>, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 84 | /// Past its time limit and a grace for g1t to say so: stopped here. |
| 85 | deadline: Instant, | |
| 86 | } | |
| 87 | ||
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 88 | use super::native::{self, WsbJob}; |
| 89 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 90 | #[cfg(unix)] |
| 91 | use super::engine::JobEngine; | |
| 92 | ||
| 93 | /// No job has an engine of its own off Unix. | |
| 94 | #[cfg(not(unix))] | |
| 95 | pub struct JobEngine; | |
| 96 | ||
| 97 | #[cfg(not(unix))] | |
| 98 | impl JobEngine { | |
| 99 | fn remove(&self) {} | |
| 100 | fn job_args(&self) -> Vec<String> { | |
| 101 | Vec::new() | |
| 102 | } | |
| 103 | } | |
| 104 | ||
| 105 | /// `docker`, at the runner's own engine when it has one. | |
| 106 | fn docker() -> Command { | |
| 107 | #[cfg(unix)] | |
| 108 | return super::engine::docker(); | |
| 109 | #[cfg(not(unix))] | |
| 110 | Command::new("docker") | |
| 111 | } | |
| 112 | ||
| 113 | fn quietly(command: &mut Command) { | |
| 114 | let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).status(); | |
| 115 | } | |
| 116 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 117 | impl Running { |
| 118 | pub fn overdue(&self) -> bool { | |
| 119 | Instant::now() > self.deadline | |
| 120 | } | |
| 121 | ||
| 122 | /// Its exit code once it has ended. | |
| 123 | pub fn ended(&mut self) -> Option<i32> { | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 124 | if self.exit.is_some() { |
| 125 | return self.exit; | |
| 126 | } | |
| 127 | self.exit = match self.child.try_wait() { | |
| 128 | Ok(Some(_)) if self.native.is_some() => { | |
| 129 | use std::io::Read; | |
| 130 | let mut text = String::new(); | |
| 131 | if let Some(out) = self.child.stdout.as_mut() { | |
| 132 | let _ = out.read_to_string(&mut text); | |
| 133 | } | |
| 134 | Some(native::parse_exit_code(&text).unwrap_or(1)) | |
| 135 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 136 | Ok(Some(status)) => Some(status.code().unwrap_or(1)), |
| 137 | Ok(None) => None, | |
| 138 | Err(_) => Some(1), | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 139 | }; |
| 140 | self.exit | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 141 | } |
| 142 | ||
| 143 | /// Stops it: the container, then the process and everything it started. | |
| 144 | pub fn stop(&mut self) { | |
| 145 | if let Some(container) = &self.container { | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 146 | quietly(docker().args(["kill", container])); |
| 147 | } | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 148 | #[cfg(unix)] |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 149 | if let Some(engine) = &self.engine { |
| 150 | quietly(docker().args(["kill", &engine.container])); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 151 | } |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 152 | if let Some(job) = &self.native { |
| 153 | job.stop(); | |
| 154 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 155 | let pid = self.child.id().to_string(); |
| 156 | if cfg!(windows) { | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 157 | quietly(Command::new("taskkill").args(["/T", "/F", "/PID", &pid])); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 158 | } else { |
| 159 | // Its own process group (see `directly`): the steps' processes too. | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 160 | quietly(Command::new("kill").args(["-TERM", &format!("-{pid}")])); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 161 | } |
| 162 | let _ = self.child.kill(); | |
| 163 | let _ = self.child.wait(); | |
| 164 | } | |
| 165 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 166 | /// Removes what it left: its container, its engine and that engine's |
| 167 | /// data, its network, its work volume, its folder. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 168 | pub fn clean(&mut self) { |
| 169 | if let Some(container) = &self.container { | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 170 | quietly(docker().args(["rm", "-f", "-v", container])); |
| 171 | } | |
| 172 | if let Some(engine) = self.engine.take() { | |
| 173 | engine.remove(); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 174 | } |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 175 | if let Some(network) = &self.network { |
| 176 | quietly(docker().args(["network", "rm", network])); | |
| 177 | } | |
| 178 | if let Some(volume) = &self.volume { | |
| 179 | quietly(docker().args(["volume", "rm", "-f", volume])); | |
| 180 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 181 | if let Some(folder) = &self.folder { |
| 182 | let _ = std::fs::remove_dir_all(folder); | |
| 183 | } | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 184 | if let Some(job) = self.native.take() { |
| 185 | job.clean(); | |
| 186 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 187 | } |
| 188 | } | |
| 189 | ||
| 190 | /// A name Docker takes, from a job's id. | |
| 191 | fn container_name(id: &str) -> String { | |
| 192 | let clean: String = id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' { c } else { '-' }).collect(); | |
| 193 | format!("g1t-{clean}") | |
| 194 | } | |
| 195 | ||
| 196 | /// The environment the harness gets for this work. | |
| 197 | fn harness_env(config: &Config, work: &Assignment) -> Result<BTreeMap<String, String>> { | |
| 198 | let mut env = BTreeMap::new(); | |
| 199 | match work.kind.as_str() { | |
| 200 | "workflow" => { | |
| 201 | let Some(token) = &work.token else { bail!("the job came without its token") }; | |
| 202 | env.insert("MODE".into(), "actions".into()); | |
| 203 | env.insert("G1T_API".into(), config.api.clone()); | |
| 204 | env.insert("ACTIONS_JOB".into(), work.id.clone()); | |
| 205 | env.insert("ACTIONS_TOKEN".into(), token.clone()); | |
| 206 | } | |
| 207 | "agent" => { | |
| 208 | for (name, value) in work.env.clone().unwrap_or_default() { | |
| 209 | let value = match value { | |
| 210 | serde_json::Value::String(text) => text, | |
| 211 | other => other.to_string(), | |
| 212 | }; | |
| 213 | env.insert(name, value); | |
| 214 | } | |
| 215 | if !env.contains_key("MODE") && !env.contains_key("PROMPT") { | |
| 216 | bail!("the agent work came without its environment"); | |
| 217 | } | |
| 218 | // Where its API calls go: this installation's. | |
| 219 | env.insert("G1T_API".into(), config.api.clone()); | |
| 220 | } | |
| 221 | other => bail!("this runner does not know work of kind {other}; update it"), | |
| 222 | } | |
| 223 | // The machine is the workspace's own: no mining watch. | |
| 224 | env.insert("G1T_ABUSE".into(), "off".into()); | |
| 225 | Ok(env) | |
| 226 | } | |
| 227 | ||
| 228 | /// Starts work. The harness reports a workflow job's steps and log itself; | |
| 229 | /// the runner tells g1t how the process ended. | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 230 | pub fn start(config: &Config, isolation: Isolation, folder: &Path, work: &Assignment) -> Result<Running> { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 231 | let env = harness_env(config, work)?; |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 232 | match isolation { |
| 233 | Isolation::Host => { | |
| 234 | refuse_public_on_host(work)?; | |
| 235 | directly(config, work, env) | |
| 236 | } | |
| 237 | Isolation::Vm => in_native_vm(config, folder, work, env), | |
| 238 | _ => in_docker(config, isolation, folder, work, env), | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 239 | } |
| 240 | } | |
| 241 | ||
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 242 | /// A memory size as `--memory` takes it (`8g`, `4096m`), in MiB. |
| 243 | pub fn mib(text: &str) -> Option<u64> { | |
| 244 | let text = text.trim().to_ascii_lowercase(); | |
| 245 | let (number, unit) = text.split_at(text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len())); | |
| 246 | let number: u64 = number.parse().ok()?; | |
| 247 | match unit.trim_end_matches('b').trim_end_matches('i') { | |
| 248 | "" | "m" => Some(number), | |
| 249 | "g" => Some(number * 1024), | |
| 250 | "k" => Some(number / 1024), | |
| 251 | _ => None, | |
| 252 | } | |
| 253 | } | |
| 254 | ||
| 255 | /// A job native to this OS, in a throwaway VM of its own (native.rs): | |
| 256 | /// Windows Sandbox on Windows. Agent work needs g1t's Linux sandbox | |
| 257 | /// layout and runs on a Linux runner. | |
| 258 | fn in_native_vm(config: &Config, folder: &Path, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> { | |
| 259 | if work.kind != "workflow" { | |
| 260 | bail!("agent work runs on Linux runners: this one runs jobs native to {}", std::env::consts::OS); | |
| 261 | } | |
| 262 | if !cfg!(windows) { | |
| 263 | bail!("this runner has no sandbox for jobs native to {}", std::env::consts::OS); | |
| 264 | } | |
| 265 | let memory = config.memory.as_deref().and_then(mib).unwrap_or(4096); | |
| 266 | let drop = folder.join("native").join(container_name(&work.id)); | |
| 267 | let job = WsbJob::start(drop, &env, memory)?; | |
| 268 | let child = match job.exec().spawn() { | |
| 269 | Ok(child) => child, | |
| 270 | Err(error) => { | |
| 271 | job.clean(); | |
| 272 | return Err(error).context("could not run wsb exec"); | |
| 273 | } | |
| 274 | }; | |
| 275 | log(&format!( | |
| 276 | "Running {} {} in a Windows Sandbox of its own ({memory} MiB): no folder of this machine's but its read-only harness, discarded after", | |
| 277 | work.kind, work.name | |
| 278 | )); | |
| 279 | Ok(Running { | |
| 280 | id: work.id.clone(), | |
| 281 | repo: work.repo.clone(), | |
| 282 | kind: work.kind.clone(), | |
| 283 | name: work.name.clone(), | |
| 284 | child, | |
| 285 | container: None, | |
| 286 | network: None, | |
| 287 | volume: None, | |
| 288 | engine: None, | |
| 289 | folder: None, | |
| 290 | native: Some(job), | |
| 291 | exit: None, | |
| 292 | deadline: deadline(work), | |
| 293 | }) | |
| 294 | } | |
| 295 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 296 | /// Host mode does not take a public repository's work: anyone can propose |
| 297 | /// code to one, and here it would run on the machine itself. | |
| 298 | fn refuse_public_on_host(work: &Assignment) -> Result<()> { | |
| 299 | match work.visibility.as_deref() { | |
| 300 | Some("public") => bail!("{}", sandbox::host_mode_refuses(&work.repo)), | |
| 301 | Some(_) => Ok(()), | |
| 302 | None => { | |
| 303 | 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)); | |
| 304 | Ok(()) | |
| 305 | } | |
| 306 | } | |
| 307 | } | |
| 308 | ||
| 309 | /// Each job container's limits. | |
| 310 | #[derive(Debug, Clone, PartialEq)] | |
| 311 | pub struct Limits { | |
| 312 | pub memory: String, | |
| 313 | pub cpus: String, | |
| 314 | pub pids: u32, | |
| 315 | } | |
| 316 | ||
| 317 | /// Three quarters of `MemTotal` in `/proc/meminfo`, in MiB, which under | |
| 318 | /// Sysbox is the runner container's own memory. | |
| 319 | fn default_memory(meminfo: &str) -> Option<String> { | |
| 320 | let kb: u64 = meminfo.lines().find_map(|l| l.strip_prefix("MemTotal:"))?.trim().trim_end_matches("kB").trim().parse().ok()?; | |
| 321 | let mib = kb / 1024 * 3 / 4; | |
| 322 | (mib >= 256).then(|| format!("{mib}m")) | |
| 323 | } | |
| 324 | ||
| 325 | pub fn limits(config: &Config) -> Limits { | |
| 326 | let memory = config | |
| 327 | .memory | |
| 328 | .clone() | |
| 329 | .or_else(|| default_memory(&std::fs::read_to_string("/proc/meminfo").unwrap_or_default())) | |
| 330 | .unwrap_or_else(|| "4g".into()); | |
| 331 | let cpus = config.cpus.clone().unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(2).to_string()); | |
| 332 | Limits { memory, cpus, pids: config.pids_limit.unwrap_or(DEFAULT_PIDS_LIMIT) } | |
| 333 | } | |
| 334 | ||
| 335 | /// Everything a job's `docker run` is made from. | |
| 336 | pub struct Plan<'a> { | |
| 337 | pub kind: &'a str, | |
| 338 | pub name: &'a str, | |
| 339 | pub runner: &'a str, | |
| 340 | pub image: &'a str, | |
| 341 | pub harness: &'a Path, | |
| 342 | pub network: &'a str, | |
| 343 | /// The job's work volume, for a workflow job's `/home/runner`. | |
| 344 | pub volume: Option<&'a str>, | |
| 345 | pub limits: &'a Limits, | |
| 346 | /// The names of the harness's variables, whose values come from the | |
| 347 | /// runner's process, never its command line. | |
| 348 | pub env_names: Vec<&'a str>, | |
| 349 | /// What the job's own engine adds (`JobEngine::job_args`). | |
| 350 | pub engine_args: Vec<String>, | |
| 351 | } | |
| 352 | ||
| 353 | /// The arguments of `docker` that start a job's container. | |
| 354 | pub fn run_args(plan: &Plan) -> Vec<String> { | |
| 355 | let mut args: Vec<String> = Vec::new(); | |
| 356 | fn add(args: &mut Vec<String>, items: &[&str]) { | |
| 357 | args.extend(items.iter().map(|s| s.to_string())); | |
| 358 | } | |
| 359 | add(&mut args, &["run", "--rm", "--name", plan.name, "--label", &format!("sh.g1t.runner={}", plan.runner)]); | |
| 360 | add(&mut args, &["--pull", "missing", "--init"]); | |
| 361 | // The walls. | |
| 362 | add(&mut args, &["--cap-drop", "ALL"]); | |
| 363 | let mut caps: Vec<&str> = JOB_CAPS.to_vec(); | |
| 364 | if plan.kind == "agent" { | |
| 365 | caps.extend(AGENT_EXTRA_CAPS); | |
| 366 | } else { | |
| 367 | add(&mut args, &["--security-opt", "no-new-privileges"]); | |
| 368 | } | |
| 369 | for cap in caps { | |
| 370 | add(&mut args, &["--cap-add", cap]); | |
| 371 | } | |
| 372 | let pids = plan.limits.pids.to_string(); | |
| 373 | add(&mut args, &["--pids-limit", &pids, "--memory", &plan.limits.memory, "--memory-swap", &plan.limits.memory, "--cpus", &plan.limits.cpus]); | |
| 374 | add(&mut args, &["--network", plan.network, "--ipc", "private"]); | |
| 375 | // The job is in its `container:` image already: the harness inside | |
| 376 | // does not start it again (actions/containers.rs). | |
| 377 | add(&mut args, &["-e", "G1T_JOB_CONTAINER=1"]); | |
| 378 | for name in &plan.env_names { | |
| 379 | add(&mut args, &["-e", name]); | |
| 380 | } | |
| 381 | // Its only mounts: the harness, read-only, and its own volume. | |
| 382 | add(&mut args, &["-v", &format!("{}:/opt/g1t/g1t-runner:ro", plan.harness.display())]); | |
| 383 | if let Some(volume) = plan.volume { | |
| 384 | add(&mut args, &["-v", &format!("{volume}:/home/runner")]); | |
| 385 | } | |
| 386 | args.extend(plan.engine_args.iter().cloned()); | |
| 387 | add(&mut args, &["--entrypoint", "/opt/g1t/g1t-runner", plan.image]); | |
| 388 | args | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 389 | } |
| 390 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 391 | fn 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 incidents | 392 | let image = match work.kind.as_str() { |
| 393 | "agent" => match config.agent_image.clone().or_else(|| update::agent_image(config)) { | |
| 394 | Some(image) => image, | |
| 395 | None => bail!( | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 396 | "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 incidents | 397 | ), |
| 398 | }, | |
| 399 | _ => work.image.clone().or_else(|| config.image.clone()).unwrap_or_else(|| DEFAULT_IMAGE.to_owned()), | |
| 400 | }; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 401 | #[cfg(unix)] |
| 402 | if super::engine::in_use() { | |
| 403 | super::engine::ensure_running()?; | |
| 404 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 405 | let harness = update::linux_harness(config, folder)?; |
| 406 | let name = container_name(&work.id); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 407 | // What an earlier run of this job may have left. |
| 408 | quietly(docker().args(["rm", "-f", "-v", &name])); | |
| 409 | quietly(docker().args(["network", "rm", &name])); | |
| 410 | let output = docker() | |
| 411 | .args(["network", "create", "--label", &format!("sh.g1t.runner={}", config.runner), &name]) | |
| 412 | .output() | |
| 413 | .context("could not run docker: is Docker installed and running?")?; | |
| 414 | if !output.status.success() { | |
| 415 | 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 incidents | 416 | } |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 417 | let volume = (work.kind == "workflow").then(|| format!("{name}-work")); |
| 418 | if let Some(volume) = &volume { | |
| 419 | quietly(docker().args(["volume", "rm", "-f", volume])); | |
| 420 | quietly(docker().args(["volume", "create", "--label", &format!("sh.g1t.runner={}", config.runner), volume])); | |
| 421 | } | |
| 422 | // A job's own Docker, only inside the runner's own sandbox: on a | |
| 423 | // shared engine it would be the machine's. | |
| 424 | let engine: Option<JobEngine> = job_engine(config, isolation, &name); | |
| 425 | let limits = limits(config); | |
| 426 | let plan = Plan { | |
| 427 | kind: &work.kind, | |
| 428 | name: &name, | |
| 429 | runner: &config.runner, | |
| 430 | image: &image, | |
| 431 | harness: &harness, | |
| 432 | network: &name, | |
| 433 | volume: volume.as_deref(), | |
| 434 | limits: &limits, | |
| 435 | env_names: env.keys().map(String::as_str).collect(), | |
| 436 | engine_args: engine.as_ref().map(JobEngine::job_args).unwrap_or_default(), | |
| 437 | }; | |
| 438 | let mut command = docker(); | |
| 439 | command.args(run_args(&plan)); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 440 | command.envs(&env); |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 441 | log(&format!( |
| 442 | "Running {} {} in {image}: its own container and network, capabilities dropped, {} memory, {} CPUs, {} processes{}", | |
| 443 | work.kind, | |
| 444 | work.name, | |
| 445 | limits.memory, | |
| 446 | limits.cpus, | |
| 447 | limits.pids, | |
| 448 | if engine.is_some() { ", Docker of its own when it uses it" } else { "" } | |
| 449 | )); | |
| 450 | let child = match command.spawn() { | |
| 451 | Ok(child) => child, | |
| 452 | Err(error) => { | |
| 453 | if let Some(engine) = &engine { | |
| 454 | engine.remove(); | |
| 455 | } | |
| 456 | quietly(docker().args(["network", "rm", &name])); | |
| 457 | return Err(error).context("could not run docker: is Docker installed and running?"); | |
| 458 | } | |
| 459 | }; | |
| 460 | Ok(Running { | |
| 461 | id: work.id.clone(), | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 462 | repo: work.repo.clone(), |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 463 | kind: work.kind.clone(), |
| 464 | name: work.name.clone(), | |
| 465 | child, | |
| 466 | container: Some(name.clone()), | |
| 467 | network: Some(name), | |
| 468 | volume, | |
| 469 | engine, | |
| 470 | folder: None, | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 471 | native: None, |
| 472 | exit: None, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 473 | deadline: deadline(work), |
| 474 | }) | |
| 475 | } | |
| 476 | ||
| 477 | #[cfg(unix)] | |
| 478 | fn job_engine(config: &Config, isolation: Isolation, name: &str) -> Option<JobEngine> { | |
| 479 | if isolation != Isolation::NestedContainer || !config.job_docker { | |
| 480 | return None; | |
| 481 | } | |
| 482 | let image = config.job_docker_image.as_deref().unwrap_or(super::engine::DEFAULT_JOB_ENGINE_IMAGE); | |
| 483 | match JobEngine::prepare(name, &config.runner, image) { | |
| 484 | Ok(engine) => Some(engine), | |
| 485 | Err(error) => { | |
| 486 | log(&format!("This job has no Docker of its own: {error:#}")); | |
| 487 | None | |
| 488 | } | |
| 489 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 490 | } |
| 491 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 492 | #[cfg(not(unix))] |
| 493 | fn job_engine(_config: &Config, _isolation: Isolation, _name: &str) -> Option<JobEngine> { | |
| 494 | None | |
| 495 | } | |
| 496 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 497 | fn directly(config: &Config, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> { |
| 498 | let me = std::env::current_exe().context("could not find this program to run the job")?; | |
| 499 | let mut command = Command::new(me); | |
| 500 | let mut folder = None; | |
| 501 | if work.kind == "workflow" { | |
| 502 | // GitHub's layout, in a folder of its own. | |
| 503 | let root = config.work_dir.join(container_name(&work.id)); | |
| 504 | let _ = std::fs::remove_dir_all(&root); | |
| 505 | std::fs::create_dir_all(&root).with_context(|| format!("could not make {}", root.display()))?; | |
| 506 | command.env("G1T_RUNNER_ROOT", &root); | |
| 507 | folder = Some(root); | |
| 508 | } else if !cfg!(target_os = "linux") || std::fs::create_dir_all("/work").is_err() { | |
| 509 | // Agent work expects g1t's sandbox layout (/work), which only a | |
| 510 | // Linux machine (or a container) set aside for it has. | |
| 511 | bail!("agent work runs in Docker, or directly on Linux where /work can be written; this runner can do neither"); | |
| 512 | } | |
| 513 | // Its own variables, less anything of the runner's. | |
| 514 | for (name, _) in std::env::vars_os() { | |
| 515 | if name.to_string_lossy().starts_with("G1T_RUNNER_") && name != "G1T_RUNNER_ROOT" { | |
| 516 | command.env_remove(&name); | |
| 517 | } | |
| 518 | } | |
| 519 | command.envs(&env); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 520 | // The job's log says it ran unsandboxed (actions/mod.rs). |
| 521 | command.env(JOB_NOTICE, sandbox::HOST_MODE_IN_JOB); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 522 | // A group of its own, so stopping it stops what its steps started. |
| 523 | #[cfg(unix)] | |
| 524 | std::os::unix::process::CommandExt::process_group(&mut command, 0); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 525 | 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 incidents | 526 | let child = command.spawn().context("could not start the job's process")?; |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 527 | Ok(Running { |
| 528 | id: work.id.clone(), | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 529 | repo: work.repo.clone(), |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 530 | kind: work.kind.clone(), |
| 531 | name: work.name.clone(), | |
| 532 | child, | |
| 533 | container: None, | |
| 534 | network: None, | |
| 535 | volume: None, | |
| 536 | engine: None, | |
| 537 | folder, | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 538 | native: None, |
| 539 | exit: None, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 540 | deadline: deadline(work), |
| 541 | }) | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 542 | } |
| 543 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 544 | /// A warning the harness puts at the top of the job's log. |
| 545 | pub const JOB_NOTICE: &str = "G1T_JOB_NOTICE"; | |
| 546 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 547 | fn deadline(work: &Assignment) -> Instant { |
| 548 | Instant::now() + Duration::from_secs(u64::from(work.timeout_minutes.max(1)) * 60 + 10 * 60) | |
| 549 | } | |
| 550 | ||
| 551 | /// Whether Docker answers. | |
| 552 | pub fn docker_ready() -> bool { | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 553 | docker() |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 554 | .args(["version", "--format", "{{.Server.Version}}"]) |
| 555 | .stdout(Stdio::null()) | |
| 556 | .stderr(Stdio::null()) | |
| 557 | .status() | |
| 558 | .is_ok_and(|status| status.success()) | |
| 559 | } | |
| 560 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 561 | /// Removes what jobs of this runner's left behind, if it stopped while one ran. |
| 562 | pub fn sweep(config: &Config) { | |
| 563 | let label = format!("label=sh.g1t.runner={}", config.runner); | |
| 564 | for (list, remove) in [ | |
| 565 | (vec!["ps", "-aq", "--filter", &label], vec!["rm", "-f", "-v"]), | |
| 566 | (vec!["network", "ls", "-q", "--filter", &label], vec!["network", "rm"]), | |
| 567 | (vec!["volume", "ls", "-q", "--filter", &label], vec!["volume", "rm", "-f"]), | |
| 568 | ] { | |
| 569 | let Ok(output) = docker().args(&list).output() else { return }; | |
| 570 | let ids: Vec<String> = String::from_utf8_lossy(&output.stdout).split_whitespace().map(str::to_owned).collect(); | |
| 571 | if !ids.is_empty() { | |
| 572 | quietly(docker().args(&remove).args(&ids)); | |
| 573 | } | |
| 574 | } | |
| 575 | } | |
| 576 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 577 | #[cfg(test)] |
| 578 | mod tests { | |
| 579 | use super::*; | |
| 580 | use serde_json::json; | |
| 581 | ||
| 582 | fn config() -> Config { | |
| 583 | Config { | |
| 584 | url: "https://g1t.sh".into(), | |
| 585 | api: "https://api.g1t.sh".into(), | |
| 586 | runner: "rnr_1".into(), | |
| 587 | name: "a".into(), | |
| 588 | credential: "g1tr_secret".into(), | |
| 589 | workspace: "acme".into(), | |
| 590 | repo: None, | |
| 591 | group: None, | |
| 592 | labels: vec![], | |
| 593 | ephemeral: false, | |
| 594 | work_dir: PathBuf::from("/tmp/w"), | |
| 595 | docker: true, | |
| 596 | image: None, | |
| 597 | agent_image: None, | |
| 598 | harness: None, | |
| 599 | auto_update: false, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 600 | memory: None, |
| 601 | cpus: None, | |
| 602 | pids_limit: None, | |
| 603 | job_docker: true, | |
| 604 | job_docker_image: None, | |
| Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop. | 605 | native: false, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 606 | } |
| 607 | } | |
| 608 | ||
| 609 | fn assignment(kind: &str) -> Assignment { | |
| 610 | serde_json::from_value(json!({ | |
| 611 | "kind": kind, "id": "job_1", "name": "build", "repo": "acme/web", "timeout_minutes": 60, | |
| 612 | "token": "jobtoken", "env": { "MODE": "checks", "G1T_TOKEN": "g1t_run", "G1T_API": "https://elsewhere" } | |
| 613 | })) | |
| 614 | .unwrap() | |
| 615 | } | |
| 616 | ||
| 617 | #[test] | |
| 618 | fn a_workflow_job_gets_its_own_token_and_never_the_runners() { | |
| 619 | let env = harness_env(&config(), &assignment("workflow")).unwrap(); | |
| 620 | assert_eq!(env["MODE"], "actions"); | |
| 621 | assert_eq!(env["ACTIONS_JOB"], "job_1"); | |
| 622 | assert_eq!(env["ACTIONS_TOKEN"], "jobtoken"); | |
| 623 | assert_eq!(env["G1T_API"], "https://api.g1t.sh"); | |
| 624 | assert!(env.values().all(|value| !value.contains("g1tr_"))); | |
| 625 | } | |
| 626 | ||
| 627 | #[test] | |
| 628 | fn agent_work_gets_the_sandboxs_environment_and_this_api() { | |
| 629 | let env = harness_env(&config(), &assignment("agent")).unwrap(); | |
| 630 | assert_eq!(env["MODE"], "checks"); | |
| 631 | assert_eq!(env["G1T_TOKEN"], "g1t_run"); | |
| 632 | assert_eq!(env["G1T_API"], "https://api.g1t.sh"); | |
| 633 | assert_eq!(env["G1T_ABUSE"], "off"); | |
| 634 | } | |
| 635 | ||
| 636 | #[test] | |
| 637 | fn unknown_work_is_refused() { | |
| 638 | assert!(harness_env(&config(), &assignment("deploy")).is_err()); | |
| 639 | } | |
| 640 | ||
| 641 | #[test] | |
| 642 | fn container_names_are_dockers() { | |
| 643 | assert_eq!(container_name("job_01ab"), "g1t-job_01ab"); | |
| 644 | assert_eq!(container_name("a/b c"), "g1t-a-b-c"); | |
| 645 | } | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 646 | |
| 647 | fn args_for(kind: &str, engine_args: Vec<String>) -> (Vec<String>, BTreeMap<String, String>) { | |
| 648 | let config = config(); | |
| 649 | let env = harness_env(&config, &assignment(kind)).unwrap(); | |
| 650 | let limits = Limits { memory: "6144m".into(), cpus: "4".into(), pids: 4096 }; | |
| 651 | let harness = PathBuf::from("/usr/local/bin/g1t-runner"); | |
| 652 | let plan = Plan { | |
| 653 | kind, | |
| 654 | name: "g1t-job_1", | |
| 655 | runner: "rnr_1", | |
| 656 | image: DEFAULT_IMAGE, | |
| 657 | harness: &harness, | |
| 658 | network: "g1t-job_1", | |
| 659 | volume: (kind == "workflow").then_some("g1t-job_1-work"), | |
| 660 | limits: &limits, | |
| 661 | env_names: env.keys().map(String::as_str).collect(), | |
| 662 | engine_args, | |
| 663 | }; | |
| 664 | (run_args(&plan), env.clone()) | |
| 665 | } | |
| 666 | ||
| 667 | fn value_of<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> { | |
| 668 | args.windows(2).filter(|w| w[0] == flag).map(|w| w[1].as_str()).collect() | |
| 669 | } | |
| 670 | ||
| 671 | #[test] | |
| 672 | fn a_jobs_container_is_walled_in() { | |
| 673 | let (args, _) = args_for("workflow", Vec::new()); | |
| 674 | assert_eq!(args[0], "run"); | |
| 675 | assert!(args.contains(&"--rm".to_owned())); | |
| 676 | assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]); | |
| 677 | assert_eq!(value_of(&args, "--cap-add"), JOB_CAPS.to_vec()); | |
| 678 | assert_eq!(value_of(&args, "--security-opt"), vec!["no-new-privileges"]); | |
| 679 | assert_eq!(value_of(&args, "--pids-limit"), vec!["4096"]); | |
| 680 | assert_eq!(value_of(&args, "--memory"), vec!["6144m"]); | |
| 681 | assert_eq!(value_of(&args, "--memory-swap"), vec!["6144m"]); | |
| 682 | assert_eq!(value_of(&args, "--cpus"), vec!["4"]); | |
| 683 | assert_eq!(value_of(&args, "--network"), vec!["g1t-job_1"]); | |
| 684 | for forbidden in ["--privileged", "--pid", "--userns", "--device", "host"] { | |
| 685 | assert!(!args.iter().any(|a| a == forbidden), "{forbidden} in {args:?}"); | |
| 686 | } | |
| 687 | assert!(!JOB_CAPS.iter().any(|c| c.starts_with("SYS_") || *c == "NET_ADMIN" || *c == "NET_RAW")); | |
| 688 | } | |
| 689 | ||
| 690 | #[test] | |
| 691 | fn a_jobs_only_mounts_are_the_harness_and_its_own_volume() { | |
| 692 | let (args, _) = args_for("workflow", Vec::new()); | |
| 693 | let mounts = value_of(&args, "-v"); | |
| 694 | assert_eq!(mounts, vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro", "g1t-job_1-work:/home/runner"]); | |
| 695 | assert!(!args.iter().any(|a| a.contains("docker.sock")), "no engine socket: {args:?}"); | |
| 696 | assert!(!args.iter().any(|a| a == "--mount")); | |
| 697 | } | |
| 698 | ||
| 699 | #[test] | |
| 700 | fn credentials_go_by_name_and_the_runners_never() { | |
| 701 | let (args, env) = args_for("workflow", Vec::new()); | |
| 702 | let passed = value_of(&args, "-e"); | |
| 703 | assert!(passed.contains(&"ACTIONS_TOKEN")); | |
| 704 | // Never a value on the command line, and never the runner's credential. | |
| 705 | assert!(!args.iter().any(|a| a.contains("jobtoken"))); | |
| 706 | assert!(!args.iter().any(|a| a.contains("g1tr_"))); | |
| 707 | assert!(env.values().all(|v| !v.contains("g1tr_secret"))); | |
| 708 | assert!(!env.contains_key("G1T_RUNNER_TOKEN")); | |
| 709 | } | |
| 710 | ||
| 711 | #[test] | |
| 712 | fn agent_work_keeps_sudo_for_its_guardrails_and_every_other_wall() { | |
| 713 | let (args, _) = args_for("agent", Vec::new()); | |
| 714 | assert!(value_of(&args, "--security-opt").is_empty()); | |
| 715 | assert!(value_of(&args, "--cap-add").contains(&"AUDIT_WRITE")); | |
| 716 | assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]); | |
| 717 | assert_eq!(value_of(&args, "-v"), vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro"]); | |
| 718 | } | |
| 719 | ||
| 720 | #[test] | |
| 721 | fn a_job_with_docker_gets_a_socket_of_its_own_not_the_runners() { | |
| 722 | 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()]; | |
| 723 | let (args, _) = args_for("workflow", engine_args); | |
| 724 | let mounts = value_of(&args, "-v"); | |
| 725 | assert!(mounts.contains(&"/run/g1t-runner/jobs/g1t-job_1/proxy:/run/g1t-docker:ro")); | |
| 726 | assert!(!mounts.iter().any(|m| m.starts_with("/var/run/docker.sock") || m.starts_with("/run/g1t-runner/docker.sock"))); | |
| 727 | // The entrypoint and image stay last. | |
| 728 | assert_eq!(&args[args.len() - 3..], ["--entrypoint", "/opt/g1t/g1t-runner", DEFAULT_IMAGE]); | |
| 729 | } | |
| 730 | ||
| 731 | #[test] | |
| 732 | fn limits_default_sensibly() { | |
| 733 | assert_eq!(default_memory("MemTotal: 16384000 kB\nMemFree: 1 kB\n"), Some("12000m".into())); | |
| 734 | assert_eq!(default_memory(""), None); | |
| 735 | let mut config = config(); | |
| 736 | config.memory = Some("2g".into()); | |
| 737 | config.pids_limit = Some(100); | |
| 738 | let limits = limits(&config); | |
| 739 | assert_eq!(limits.memory, "2g"); | |
| 740 | assert_eq!(limits.pids, 100); | |
| 741 | assert!(limits.cpus.parse::<f64>().unwrap() >= 1.0); | |
| 742 | } | |
| 743 | ||
| 744 | #[test] | |
| 745 | fn host_mode_refuses_a_public_repositorys_work() { | |
| 746 | let mut work = assignment("workflow"); | |
| 747 | work.visibility = Some("public".into()); | |
| 748 | let refused = refuse_public_on_host(&work).unwrap_err().to_string(); | |
| 749 | assert!(refused.contains("acme/web is public"), "{refused}"); | |
| 750 | work.visibility = Some("private".into()); | |
| 751 | assert!(refuse_public_on_host(&work).is_ok()); | |
| 752 | work.visibility = None; | |
| 753 | assert!(refuse_public_on_host(&work).is_ok()); | |
| 754 | // And refuses it before anything runs. | |
| 755 | work.visibility = Some("public".into()); | |
| 756 | assert!(start(&config(), Isolation::Host, Path::new("/tmp"), &work).is_err()); | |
| 757 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 758 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.