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 | //! `g1t-runner` on your own machine: a self-hosted runner for g1t. |
| 2 | //! | |
| 3 | //! ```text | |
| 4 | //! g1t-runner register --url https://g1t.sh --token g1trt_… [--name build-01] | |
| 5 | //! [--labels gpu,cuda] [--group Default] [--ephemeral] [--work-dir DIR] | |
| 6 | //! [--no-docker] [--image IMAGE] [--agent-image IMAGE] [--replace] | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 7 | //! [--memory 8g] [--cpus 4] [--pids-limit 4096] [--no-job-docker] |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 8 | //! g1t-runner run [--once] |
| 9 | //! g1t-runner service install|start|stop|status|uninstall | |
| 10 | //! g1t-runner remove | |
| 11 | //! g1t-runner update | |
| 12 | //! g1t-runner version | |
| 13 | //! ``` | |
| 14 | //! | |
| 15 | //! It only ever calls out, to `api.g1t.sh` over HTTPS: it polls for work | |
| 16 | //! (a poll waits up to 20 seconds for some, and is the runner's heartbeat), | |
| 17 | //! runs what it is given with the same harness g1t's sandboxes run (this | |
| 18 | //! program in another mode, see `exec`), and says how it ended. Nothing | |
| 19 | //! listens on the machine. Every command takes `--dir` (default | |
| 20 | //! `~/.g1t-runner`, or `G1T_RUNNER_DIR`), where its configuration is kept. | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 21 | //! |
| 22 | //! How it keeps jobs from the machine is `sandbox`: in its image under | |
| 23 | //! Sysbox (`G1T_RUNNER_SANDBOX=nested`) it runs its own Docker Engine | |
| 24 | //! (`engine`) and refuses to start if it can reach the machine's. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 25 | |
| 26 | mod api; | |
| 27 | mod config; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 28 | #[cfg(unix)] |
| 29 | mod engine; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 30 | mod exec; |
| 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. | 31 | mod launcher; |
| 32 | mod native; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 33 | mod sandbox; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 34 | mod service; |
| 35 | mod update; | |
| 36 | ||
| 37 | use std::path::{Path, PathBuf}; | |
| 38 | use std::sync::atomic::{AtomicBool, Ordering}; | |
| 39 | use std::time::{Duration, Instant}; | |
| 40 | ||
| 41 | use anyhow::{Context, Result, bail}; | |
| 42 | use serde_json::json; | |
| 43 | ||
| 44 | use api::{Api, Failure}; | |
| 45 | use config::Config; | |
| 46 | ||
| 47 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); | |
| 48 | ||
| 49 | /// Set to stop the loop between polls: a Windows service's stop request. | |
| 50 | pub static STOP: AtomicBool = AtomicBool::new(false); | |
| 51 | ||
| 52 | /// How long a busy runner waits between heartbeats. | |
| 53 | const HEARTBEAT: Duration = Duration::from_secs(10); | |
| 54 | /// How long an idle poll waits for work. | |
| 55 | const IDLE_WAIT_MS: u64 = 20_000; | |
| 56 | /// How often an idle runner checks for a new version. | |
| 57 | const UPDATE_EVERY: Duration = Duration::from_secs(6 * 60 * 60); | |
| 58 | ||
| 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. | 59 | const COMMANDS: &[&str] = &["register", "register-and-run", "run", "service", "sandbox", "remove", "update", "version", "--version", "help", "--help", "-h"]; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 60 | |
| 61 | /// Whether `args` (without the program) are a command of this one, rather | |
| 62 | /// than the harness's `MODE`. | |
| 63 | pub fn is_command(args: &[String]) -> bool { | |
| 64 | args.first().is_some_and(|first| COMMANDS.contains(&first.as_str())) | |
| 65 | } | |
| 66 | ||
| 67 | pub fn log(message: &str) { | |
| 68 | let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0); | |
| 69 | println!("{} {message}", g1t_now(now)); | |
| 70 | } | |
| 71 | ||
| 72 | /// RFC 3339 for the log, without pulling in a date library. | |
| 73 | fn g1t_now(ms: u64) -> String { | |
| 74 | let seconds = ms / 1000; | |
| 75 | let (days, rest) = (seconds / 86_400, seconds % 86_400); | |
| 76 | let z = days as i64 + 719_468; | |
| 77 | let era = z.div_euclid(146_097); | |
| 78 | let doe = z.rem_euclid(146_097); | |
| 79 | let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; | |
| 80 | let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); | |
| 81 | let mp = (5 * doy + 2) / 153; | |
| 82 | let day = doy - (153 * mp + 2) / 5 + 1; | |
| 83 | let month = if mp < 10 { mp + 3 } else { mp - 9 }; | |
| 84 | let year = yoe + era * 400 + i64::from(month <= 2); | |
| 85 | format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", rest / 3600, rest % 3600 / 60, rest % 60) | |
| 86 | } | |
| 87 | ||
| 88 | /// `--name value` and `--flag` options. | |
| 89 | struct Options { | |
| 90 | values: Vec<(String, String)>, | |
| 91 | flags: Vec<String>, | |
| 92 | rest: Vec<String>, | |
| 93 | } | |
| 94 | ||
| 95 | impl Options { | |
| 96 | fn parse(args: &[String], takes_value: &[&str]) -> Result<Options> { | |
| 97 | let mut options = Options { values: Vec::new(), flags: Vec::new(), rest: Vec::new() }; | |
| 98 | let mut i = 0; | |
| 99 | while i < args.len() { | |
| 100 | let arg = &args[i]; | |
| 101 | if let Some(name) = arg.strip_prefix("--") { | |
| 102 | if let Some((name, value)) = name.split_once('=') { | |
| 103 | options.values.push((name.to_owned(), value.to_owned())); | |
| 104 | } else if takes_value.contains(&name) { | |
| 105 | let Some(value) = args.get(i + 1) else { bail!("--{name} needs a value") }; | |
| 106 | options.values.push((name.to_owned(), value.clone())); | |
| 107 | i += 1; | |
| 108 | } else { | |
| 109 | options.flags.push(name.to_owned()); | |
| 110 | } | |
| 111 | } else { | |
| 112 | options.rest.push(arg.clone()); | |
| 113 | } | |
| 114 | i += 1; | |
| 115 | } | |
| 116 | Ok(options) | |
| 117 | } | |
| 118 | ||
| 119 | fn value(&self, name: &str) -> Option<&str> { | |
| 120 | self.values.iter().rev().find(|(n, _)| n == name).map(|(_, v)| v.as_str()) | |
| 121 | } | |
| 122 | ||
| 123 | fn flag(&self, name: &str) -> bool { | |
| 124 | self.flags.iter().any(|f| f == name) | |
| 125 | } | |
| 126 | ||
| 127 | /// For `register-and-run` in a container: each option may come from | |
| 128 | /// `G1T_RUNNER_<NAME>` instead (`G1T_RUNNER_TOKEN`, `G1T_RUNNER_LABELS`, | |
| 129 | /// `G1T_RUNNER_EPHEMERAL=1`), so a Kubernetes secret can hold the token. | |
| 130 | fn with_environment(mut self) -> Options { | |
| 131 | let env_name = |name: &str| format!("G1T_RUNNER_{}", name.to_ascii_uppercase().replace('-', "_")); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 132 | for name in VALUE_OPTIONS { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 133 | if self.value(name).is_none() |
| 134 | && let Ok(value) = std::env::var(env_name(name)) | |
| 135 | && !value.is_empty() | |
| 136 | { | |
| 137 | self.values.push((name.to_owned(), value)); | |
| 138 | } | |
| 139 | } | |
| 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. | 140 | for name in ["ephemeral", "no-docker", "replace", "no-auto-update", "no-job-docker", "native"] { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 141 | if !self.flag(name) && std::env::var(env_name(name)).is_ok_and(|v| matches!(v.as_str(), "1" | "true" | "yes")) { |
| 142 | self.flags.push(name.to_owned()); | |
| 143 | } | |
| 144 | } | |
| 145 | self | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 149 | /// The options that take a value. |
| 150 | const VALUE_OPTIONS: [&str; 14] = | |
| 151 | ["url", "api", "token", "name", "labels", "group", "work-dir", "image", "agent-image", "harness", "memory", "cpus", "pids-limit", "job-docker-image"]; | |
| 152 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 153 | const HELP: &str = "g1t-runner: a self-hosted runner for g1t. |
| 154 | ||
| 155 | g1t-runner register --url https://g1t.sh --token <registration token> | |
| 156 | [--name NAME] [--labels a,b] [--group GROUP] [--ephemeral] | |
| 157 | [--work-dir DIR] [--no-docker] [--image IMAGE] [--agent-image IMAGE] | |
| 158 | [--api URL] [--harness PATH] [--replace] [--no-auto-update] | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 159 | [--memory 8g] [--cpus 4] [--pids-limit 4096] [--no-job-docker] |
| 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. | 160 | [--job-docker-image IMAGE] [--native] |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 161 | g1t-runner run [--once] poll for work and run it |
| 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. | 162 | g1t-runner sandbox up|down|status|update [--json] |
| 163 | on your own computer: a Linux VM of the | |
| 164 | runner's own for Linux jobs, and Windows | |
| 165 | Sandbox for Windows jobs; see | |
| 166 | `g1t-runner sandbox help` | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 167 | g1t-runner register-and-run ... both, for containers; options may be |
| 168 | G1T_RUNNER_URL, _TOKEN, _LABELS, _EPHEMERAL… | |
| 169 | g1t-runner service install|start|stop|status|uninstall | |
| 170 | g1t-runner remove unregister this runner | |
| 171 | g1t-runner update update to the newest release | |
| 172 | g1t-runner version | |
| 173 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 174 | In a workflow step (the g1t/deploy and g1t/detect actions use these): |
| 175 | g1t-runner deploy build this checkout and put it on g1t.page | |
| 176 | g1t-runner detect [--root-dir DIR] what that build would find | |
| 177 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 178 | Every command takes --dir DIR (default ~/.g1t-runner, or G1T_RUNNER_DIR). |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 179 | Run it as a container under Sysbox so neither it nor its jobs reach the |
| 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. | 180 | machine: docker run --runtime=sysbox-runc … g1t.sh/flagon-io/g1t-runner, |
| 181 | or on a desktop, g1t-runner sandbox up. --native runs jobs native to this | |
| 182 | OS, each in a throwaway VM (Windows Sandbox). --no-docker (host mode) is | |
| 183 | deprecated. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 184 | Make a registration token under Settings, Runners on g1t.sh. |
| 185 | Docs: https://docs.g1t.sh/guides/self-hosted-runners/"; | |
| 186 | ||
| 187 | /// Runs a command. Returns the process's exit code. | |
| 188 | pub fn main(args: Vec<String>) -> 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. | 189 | // Options of its own, and output the desktop app reads. |
| 190 | if args[0] == "sandbox" { | |
| 191 | return launcher::main(&args[1..]); | |
| 192 | } | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 193 | let mut takes = VALUE_OPTIONS.to_vec(); |
| 194 | takes.push("dir"); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 195 | let options = match Options::parse(&args[1..], &takes) { |
| 196 | Ok(options) => options, | |
| 197 | Err(error) => { | |
| 198 | eprintln!("g1t-runner: {error:#}"); | |
| 199 | return 2; | |
| 200 | } | |
| 201 | }; | |
| 202 | let folder = config::folder(options.value("dir")); | |
| 203 | let outcome = match args[0].as_str() { | |
| 204 | "register" => register(&options, &folder), | |
| 205 | // A container's one command: register unless it already is, then run. | |
| 206 | "register-and-run" => { | |
| 207 | let options = options.with_environment(); | |
| 208 | let ready = if config::load(&folder).is_ok() { Ok(()) } else { register(&options, &folder) }; | |
| 209 | ready.and_then(|()| run(&folder, options.flag("once"))) | |
| 210 | } | |
| 211 | "run" => run(&folder, options.flag("once")), | |
| 212 | "service" => match options.rest.first().map(String::as_str) { | |
| 213 | Some(action) => config::load(&folder).and_then(|config| service::main(action, &config, &folder)), | |
| 214 | None => Err(anyhow::anyhow!("service takes install, uninstall, start, stop or status")), | |
| 215 | }, | |
| 216 | "remove" => remove(&folder), | |
| 217 | "update" => config::load(&folder).and_then(|config| match update::update(&config)? { | |
| 218 | Some(version) => { | |
| 219 | log(&format!("Updated to {version}. Restart the runner (or its service) to use it.")); | |
| 220 | Ok(()) | |
| 221 | } | |
| 222 | None => { | |
| 223 | log(&format!("{VERSION} is the newest release.")); | |
| 224 | Ok(()) | |
| 225 | } | |
| 226 | }), | |
| 227 | "version" | "--version" => { | |
| 228 | println!("g1t-runner {VERSION} ({})", update::platform()); | |
| 229 | Ok(()) | |
| 230 | } | |
| 231 | _ => { | |
| 232 | println!("{HELP}"); | |
| 233 | Ok(()) | |
| 234 | } | |
| 235 | }; | |
| 236 | match outcome { | |
| 237 | Ok(()) => 0, | |
| 238 | Err(error) => { | |
| 239 | eprintln!("g1t-runner: {error:#}"); | |
| 240 | 1 | |
| 241 | } | |
| 242 | } | |
| 243 | } | |
| 244 | ||
| 245 | fn hostname() -> String { | |
| 246 | for name in ["COMPUTERNAME", "HOSTNAME"] { | |
| 247 | if let Ok(value) = std::env::var(name).map(|v| v.trim().to_owned()) | |
| 248 | && !value.is_empty() | |
| 249 | { | |
| 250 | return value; | |
| 251 | } | |
| 252 | } | |
| 253 | std::process::Command::new("hostname") | |
| 254 | .output() | |
| 255 | .ok() | |
| 256 | .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned()) | |
| 257 | .filter(|name| !name.is_empty()) | |
| 258 | .unwrap_or_else(|| "runner".to_owned()) | |
| 259 | } | |
| 260 | ||
| 261 | /// A runner's name from a machine's: what g1t accepts. | |
| 262 | fn runner_name(given: &str) -> String { | |
| 263 | let name: String = given | |
| 264 | .chars() | |
| 265 | .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') { c } else { '-' }) | |
| 266 | .take(64) | |
| 267 | .collect(); | |
| 268 | if name.is_empty() { "runner".into() } else { name } | |
| 269 | } | |
| 270 | ||
| 271 | fn register(options: &Options, folder: &Path) -> Result<()> { | |
| 272 | let Some(url) = options.value("url") else { bail!("--url is needed: the g1t you register with, such as https://g1t.sh") }; | |
| 273 | let Some(token) = options.value("token") else { bail!("--token is needed: make a registration token under Settings, Runners") }; | |
| 274 | let url = url.trim_end_matches('/').to_owned(); | |
| 275 | let api = options.value("api").map(|a| a.trim_end_matches('/').to_owned()).unwrap_or_else(|| config::api_for(&url)); | |
| 276 | let name = runner_name(options.value("name").map(str::to_owned).unwrap_or_else(hostname).as_str()); | |
| 277 | let labels: Vec<String> = options | |
| 278 | .value("labels") | |
| 279 | .unwrap_or_default() | |
| 280 | .split(',') | |
| 281 | .map(|l| l.trim().to_owned()) | |
| 282 | .filter(|l| !l.is_empty()) | |
| 283 | .collect(); | |
| 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. | 284 | let native = options.flag("native"); |
| 285 | if native && options.flag("no-docker") { | |
| 286 | bail!("--native runs each job in a VM of its own, and --no-docker runs it on this machine: pick one"); | |
| 287 | } | |
| 288 | let docker = !options.flag("no-docker") && !native; | |
| 289 | let isolation = isolation(docker, native)?; | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 290 | if isolation == sandbox::Isolation::Host { |
| 291 | log(sandbox::HOST_MODE_AT_START); | |
| 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. | 292 | } else if !native && !sandbox::nested_requested() && !exec::docker_ready() { |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 293 | log("Docker does not answer here. Jobs will fail until it does."); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 294 | } |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 295 | let pids_limit = match options.value("pids-limit") { |
| 296 | Some(text) => Some(text.parse::<u32>().ok().filter(|n| *n > 0).with_context(|| format!("--pids-limit takes a number of processes, not {text}"))?), | |
| 297 | None => None, | |
| 298 | }; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 299 | let existing = config::load(folder).ok(); |
| 300 | if existing.is_some() && !options.flag("replace") { | |
| 301 | bail!("{} already holds a registered runner: run `g1t-runner remove` first, or register with --replace or another --dir", folder.display()); | |
| 302 | } | |
| 303 | // The OS jobs run on: in Docker that is Linux, whatever the machine | |
| 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. | 304 | // (Docker Desktop runs Linux containers on macOS and Windows). A |
| 305 | // runner says it runs jobs native to this OS only when it sandboxes | |
| 306 | // them (--native), or when the person chose host mode (--no-docker). | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 307 | let os = match std::env::consts::OS { |
| 308 | _ if docker => "linux", | |
| 309 | "macos" => "macos", | |
| 310 | "windows" => "windows", | |
| 311 | _ => "linux", | |
| 312 | }; | |
| 313 | let arch = if std::env::consts::ARCH == "aarch64" { "arm64" } else { "x64" }; | |
| 314 | let registered = Api::register( | |
| 315 | &api, | |
| 316 | &json!({ | |
| 317 | "token": token, | |
| 318 | "name": name, | |
| 319 | "labels": labels, | |
| 320 | "os": os, | |
| 321 | "arch": arch, | |
| 322 | "version": VERSION, | |
| 323 | "ephemeral": options.flag("ephemeral"), | |
| 324 | "group": options.value("group"), | |
| 325 | "replace": options.flag("replace"), | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 326 | "isolation": isolation.as_str(), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 327 | }), |
| 328 | )?; | |
| 329 | let work_dir = options.value("work-dir").map(PathBuf::from).unwrap_or_else(|| folder.join("work")); | |
| 330 | let config = Config { | |
| 331 | url, | |
| 332 | api, | |
| 333 | runner: registered.runner.id.clone(), | |
| 334 | name: registered.runner.name.clone(), | |
| 335 | credential: registered.credential, | |
| 336 | workspace: registered.runner.workspace.clone(), | |
| 337 | repo: registered.runner.repo.clone(), | |
| 338 | group: registered.runner.group.clone(), | |
| 339 | labels: registered.runner.labels.clone(), | |
| 340 | ephemeral: options.flag("ephemeral"), | |
| 341 | work_dir, | |
| 342 | docker, | |
| 343 | image: options.value("image").map(str::to_owned), | |
| 344 | agent_image: options.value("agent-image").map(str::to_owned), | |
| 345 | harness: options.value("harness").map(PathBuf::from), | |
| 346 | auto_update: !options.flag("no-auto-update"), | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 347 | memory: options.value("memory").map(str::to_owned), |
| 348 | cpus: options.value("cpus").map(str::to_owned), | |
| 349 | pids_limit, | |
| 350 | job_docker: !options.flag("no-job-docker"), | |
| 351 | job_docker_image: options.value("job-docker-image").map(str::to_owned), | |
| 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. | 352 | native, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 353 | }; |
| 354 | config::save(folder, &config)?; | |
| 355 | let place = match &config.repo { | |
| 356 | Some(repo) => format!("for {repo}"), | |
| 357 | None => format!("in {}{}", config.workspace, config.group.as_ref().map(|g| format!(", group {g}")).unwrap_or_default()), | |
| 358 | }; | |
| 359 | log(&format!("Registered {} {place}, with labels {}.", config.name, config.labels.join(", "))); | |
| 360 | log("Start it with `g1t-runner run`, or `g1t-runner service install` to keep it running."); | |
| 361 | Ok(()) | |
| 362 | } | |
| 363 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 364 | /// How this runner keeps work from its machine; refuses a nested runner |
| 365 | /// that can reach the machine (sandbox.rs). | |
| 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. | 366 | fn isolation(docker: bool, native: bool) -> Result<sandbox::Isolation> { |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 367 | use sandbox::{Isolation, SandboxDriver}; |
| 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. | 368 | if native { |
| 369 | let Some(driver) = sandbox::native_driver() else { | |
| 370 | bail!("--native: there is no sandbox for jobs native to {}; Linux jobs run in containers", std::env::consts::OS); | |
| 371 | }; | |
| 372 | return driver.preflight().with_context(|| format!("--native needs {}", driver.name())); | |
| 373 | } | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 374 | if !docker { |
| 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. | 375 | // What would sandbox jobs native to this OS instead. |
| 376 | if let Some(driver) = sandbox::native_driver() { | |
| 377 | match driver.preflight() { | |
| 378 | Ok(_) => log(&format!("This machine can run {} jobs sandboxed, each in {}: register with --native instead of --no-docker.", std::env::consts::OS, driver.name())), | |
| 379 | Err(why) => log(&format!("Sandboxed {} jobs, in {}: {why:#}", std::env::consts::OS, driver.name())), | |
| 380 | } | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 381 | } |
| 382 | return Ok(Isolation::Host); | |
| 383 | } | |
| 384 | if !sandbox::nested_requested() { | |
| 385 | return sandbox::SharedDocker.preflight(); | |
| 386 | } | |
| 387 | if !cfg!(target_os = "linux") { | |
| 388 | bail!("{}=nested is for the runner's Linux image", sandbox::SANDBOX_ENV); | |
| 389 | } | |
| 390 | let isolation = sandbox::NestedDocker { allow_unsafe: sandbox::unsafe_allowed() }.preflight()?; | |
| 391 | if isolation != Isolation::NestedContainer { | |
| 392 | for problem in sandbox::Findings::read().problems() { | |
| 393 | log(&format!("UNSAFE: {problem}")); | |
| 394 | } | |
| 395 | log(&format!("UNSAFE: {}=1 is set, so the runner starts anyway, and its jobs can reach this machine.", sandbox::UNSAFE_ENV)); | |
| 396 | } | |
| 397 | Ok(isolation) | |
| 398 | } | |
| 399 | ||
| 400 | /// A nested runner's own Docker Engine, which it uses whatever else it | |
| 401 | /// can see (with the override, it starts despite that). | |
| 402 | fn prepare(docker: bool) -> Result<()> { | |
| 403 | if !docker || !sandbox::nested_requested() { | |
| 404 | return Ok(()); | |
| 405 | } | |
| 406 | #[cfg(unix)] | |
| 407 | { | |
| 408 | let version = engine::start()?; | |
| 409 | log(&format!("Started this runner's own Docker Engine {version}, inside its container.")); | |
| 410 | } | |
| 411 | Ok(()) | |
| 412 | } | |
| 413 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 414 | fn remove(folder: &Path) -> Result<()> { |
| 415 | let config = config::load(folder)?; | |
| 416 | let api = Api { base: config.api.clone(), runner: config.runner.clone(), credential: config.credential.clone() }; | |
| 417 | match api.remove() { | |
| 418 | Ok(()) | Err(Failure::Refused(401, _)) | Err(Failure::Refused(404, _)) => {} | |
| 419 | Err(failure) => bail!("{failure}; the runner is still registered"), | |
| 420 | } | |
| 421 | config::forget(folder); | |
| 422 | log(&format!("Removed {}.", config.name)); | |
| 423 | Ok(()) | |
| 424 | } | |
| 425 | ||
| 426 | /// The loop, for a Windows service: its exit code. | |
| 427 | #[cfg_attr(not(windows), allow(dead_code))] | |
| 428 | pub fn serve_from_service() -> i32 { | |
| 429 | let folder = config::folder(None); | |
| 430 | let folder = std::env::args() | |
| 431 | .collect::<Vec<_>>() | |
| 432 | .windows(2) | |
| 433 | .find(|pair| pair[0] == "--dir") | |
| 434 | .map(|pair| PathBuf::from(&pair[1])) | |
| 435 | .unwrap_or(folder); | |
| 436 | match run(&folder, false) { | |
| 437 | Ok(()) => 0, | |
| 438 | Err(error) => { | |
| 439 | eprintln!("g1t-runner: {error:#}"); | |
| 440 | 1 | |
| 441 | } | |
| 442 | } | |
| 443 | } | |
| 444 | ||
| 445 | /// Sleeps up to `duration`, waking early when `stop` says so. | |
| 446 | fn nap(duration: Duration, mut stop: impl FnMut() -> bool) { | |
| 447 | let until = Instant::now() + duration; | |
| 448 | while Instant::now() < until { | |
| 449 | if STOP.load(Ordering::SeqCst) || stop() { | |
| 450 | return; | |
| 451 | } | |
| 452 | std::thread::sleep(Duration::from_millis(250)); | |
| 453 | } | |
| 454 | } | |
| 455 | ||
| 456 | fn run(folder: &Path, once: bool) -> Result<()> { | |
| 457 | let mut config = config::load(folder)?; | |
| 458 | let mut api = Api { base: config.api.clone(), runner: config.runner.clone(), credential: config.credential.clone() }; | |
| 459 | std::fs::create_dir_all(&config.work_dir).with_context(|| format!("could not make {}", config.work_dir.display()))?; | |
| 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. | 460 | let isolation = isolation(config.docker, config.native)?; |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 461 | prepare(config.docker)?; |
| 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 | match isolation { |
| 463 | sandbox::Isolation::Host => {} | |
| 464 | sandbox::Isolation::Vm => native::sweep(folder), | |
| 465 | _ => exec::sweep(&config), | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 466 | } |
| 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. | 467 | // A drain or stop asked for before this start is done with. |
| 468 | let _ = std::fs::remove_file(drain_path(folder)); | |
| 469 | let _ = std::fs::remove_file(folder.join("stop")); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 470 | log(&format!( |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 471 | "g1t-runner {VERSION}: {} ({}) is listening for work from {}. Isolation: {}.", |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 472 | config.name, |
| 473 | config.labels.join(", "), | |
| 474 | config.url, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 475 | isolation.describe() |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 476 | )); |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 477 | if isolation == sandbox::Isolation::Host { |
| 478 | log(sandbox::HOST_MODE_AT_START); | |
| 479 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 480 | let mut current: Option<exec::Running> = None; |
| 481 | let mut ran_one = false; | |
| 482 | let mut backoff = Duration::from_secs(2); | |
| 483 | let mut last_update_check = Instant::now().checked_sub(UPDATE_EVERY).unwrap_or_else(Instant::now); | |
| 484 | let mut said_no_key = false; | |
| 485 | loop { | |
| 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. | 486 | // Asked to stop now (`g1t-runner sandbox down`): as a service's stop. |
| 487 | if std::fs::remove_file(folder.join("stop")).is_ok() { | |
| 488 | STOP.store(true, Ordering::SeqCst); | |
| 489 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 490 | if STOP.load(Ordering::SeqCst) { |
| 491 | if let Some(mut running) = current.take() { | |
| 492 | log(&format!("Stopping: {} is cut short.", running.name)); | |
| 493 | running.stop(); | |
| 494 | let _ = api.finished(&running.id, 1, Some("The self-hosted runner was stopped.")); | |
| 495 | running.clean(); | |
| 496 | } | |
| 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. | 497 | say_status(folder, isolation, None, "stopped"); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 498 | return Ok(()); |
| 499 | } | |
| 500 | // What was running has ended: say how. | |
| 501 | if let Some(running) = current.as_mut() | |
| 502 | && let Some(code) = running.ended() | |
| 503 | { | |
| 504 | let mut running = current.take().expect("checked above"); | |
| 505 | log(&format!("{} {} ended ({code}).", running.kind, running.name)); | |
| 506 | if let Err(failure) = api.finished(&running.id, code, None) { | |
| 507 | log(&format!("Could not say how it ended: {failure}")); | |
| 508 | } | |
| 509 | running.clean(); | |
| 510 | ran_one = true; | |
| 511 | if config.ephemeral || once { | |
| 512 | if config.ephemeral { | |
| 513 | let _ = api.remove(); | |
| 514 | config::forget(folder); | |
| 515 | log("The ephemeral runner ran its job and removed itself."); | |
| 516 | } | |
| 517 | return Ok(()); | |
| 518 | } | |
| 519 | } | |
| 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. | 520 | // Asked to drain (`g1t-runner sandbox update`, before it swaps the |
| 521 | // sandbox's image): no new work, and once idle, stop. | |
| 522 | let draining = drain_path(folder).exists(); | |
| 523 | if draining && current.is_none() { | |
| 524 | let _ = std::fs::remove_file(drain_path(folder)); | |
| 525 | say_status(folder, isolation, None, "stopped"); | |
| 526 | log("Drained: no job is running, so the runner stops here."); | |
| 527 | return Ok(()); | |
| 528 | } | |
| 529 | let state = match (¤t, draining) { | |
| 530 | (Some(_), true) => "draining", | |
| 531 | (Some(_), false) => "busy", | |
| 532 | (None, _) => "idle", | |
| 533 | }; | |
| 534 | say_status(folder, isolation, current.as_ref(), state); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 535 | // Idle: a newer release, now and then. |
| 536 | if current.is_none() && config.auto_update && last_update_check.elapsed() >= UPDATE_EVERY { | |
| 537 | last_update_check = Instant::now(); | |
| 538 | if update::can_update() { | |
| 539 | match update::update(&config) { | |
| 540 | Ok(Some(version)) => { | |
| 541 | log(&format!("Updated to {version}; starting it.")); | |
| 542 | return restart(); | |
| 543 | } | |
| 544 | Ok(None) => {} | |
| 545 | Err(error) => log(&format!("Could not check for a new version: {error:#}")), | |
| 546 | } | |
| 547 | } else if !said_no_key { | |
| 548 | said_no_key = true; | |
| 549 | log("This build cannot check releases, so it will not update itself."); | |
| 550 | } | |
| 551 | } | |
| 552 | let running: Vec<String> = current.iter().map(|r| r.id.clone()).collect(); | |
| 553 | let wait = if current.is_some() { 0 } else { IDLE_WAIT_MS }; | |
| Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's. | 554 | let poll = match api.poll(&running, wait, isolation.as_str()) { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 555 | Ok(poll) => { |
| 556 | backoff = Duration::from_secs(2); | |
| 557 | poll | |
| 558 | } | |
| 559 | Err(Failure::Refused(401, message)) => { | |
| 560 | if let Some(mut running) = current.take() { | |
| 561 | running.stop(); | |
| 562 | running.clean(); | |
| 563 | } | |
| 564 | bail!("g1t no longer knows this runner ({message}). Register it again."); | |
| 565 | } | |
| 566 | Err(failure) => { | |
| 567 | log(&format!("{failure}; trying again in {}s", backoff.as_secs())); | |
| 568 | nap(backoff, || false); | |
| 569 | backoff = (backoff * 2).min(Duration::from_secs(60)); | |
| 570 | continue; | |
| 571 | } | |
| 572 | }; | |
| 573 | if let Some(fresh) = poll.credential { | |
| 574 | config.credential = fresh.clone(); | |
| 575 | api.credential = fresh; | |
| 576 | config::save(folder, &config)?; | |
| 577 | } | |
| 578 | if poll.removed { | |
| 579 | config::forget(folder); | |
| 580 | log("This runner was removed from g1t; stopping."); | |
| 581 | return Ok(()); | |
| 582 | } | |
| 583 | if let Some(running) = current.as_mut() | |
| 584 | && poll.cancel.contains(&running.id) | |
| 585 | { | |
| 586 | log(&format!("{} was cancelled; stopping it.", running.name)); | |
| 587 | running.stop(); | |
| 588 | } | |
| 589 | if let Some(running) = current.as_mut() | |
| 590 | && running.overdue() | |
| 591 | { | |
| 592 | log(&format!("{} ran past its time limit; stopping it.", running.name)); | |
| 593 | running.stop(); | |
| 594 | } | |
| 595 | if let Some(work) = poll.assignment { | |
| 596 | log(&format!("Took {} {} ({}).", work.kind, work.name, work.repo)); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 597 | match exec::start(&config, isolation, folder, &work) { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 598 | Ok(running) => current = Some(running), |
| 599 | Err(error) => { | |
| 600 | log(&format!("Could not start it: {error:#}")); | |
| 601 | let _ = api.finished(&work.id, 1, Some(&format!("The self-hosted runner {} could not start it: {error:#}", config.name))); | |
| 602 | if config.ephemeral || once { | |
| 603 | return Ok(()); | |
| 604 | } | |
| 605 | } | |
| 606 | } | |
| 607 | continue; | |
| 608 | } | |
| 609 | if once && ran_one { | |
| 610 | return Ok(()); | |
| 611 | } | |
| 612 | // Busy: a heartbeat every few seconds until it ends. | |
| 613 | if current.is_some() { | |
| 614 | let mut check = || current.as_mut().is_some_and(|r| r.ended().is_some()); | |
| 615 | nap(HEARTBEAT, &mut check); | |
| 616 | } | |
| 617 | } | |
| 618 | } | |
| 619 | ||
| 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. | 620 | /// The file whose presence asks a runner to drain: take no new work, and |
| 621 | /// stop once idle. | |
| 622 | pub fn drain_path(folder: &Path) -> PathBuf { | |
| 623 | folder.join("drain") | |
| 624 | } | |
| 625 | ||
| 626 | /// The file a runner keeps saying what it is doing, for `g1t-runner | |
| 627 | /// sandbox status` (which reads it from inside the sandbox). | |
| 628 | pub fn status_path(folder: &Path) -> PathBuf { | |
| 629 | folder.join("status.json") | |
| 630 | } | |
| 631 | ||
| 632 | /// What a runner says it is doing, in its status file. | |
| 633 | #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq)] | |
| 634 | pub struct RunnerStatus { | |
| 635 | pub version: String, | |
| 636 | pub isolation: String, | |
| 637 | /// `idle`, `busy`, `draining` or `stopped`. | |
| 638 | pub state: String, | |
| 639 | pub job: Option<JobStatus>, | |
| 640 | pub updated_at: String, | |
| 641 | } | |
| 642 | ||
| 643 | #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq)] | |
| 644 | pub struct JobStatus { | |
| 645 | /// `workflow` or `agent`. | |
| 646 | pub kind: String, | |
| 647 | pub id: String, | |
| 648 | pub name: String, | |
| 649 | pub repo: String, | |
| 650 | } | |
| 651 | ||
| 652 | fn say_status(folder: &Path, isolation: sandbox::Isolation, running: Option<&exec::Running>, state: &str) { | |
| 653 | let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0); | |
| 654 | let status = RunnerStatus { | |
| 655 | version: VERSION.to_owned(), | |
| 656 | isolation: isolation.as_str().to_owned(), | |
| 657 | state: state.to_owned(), | |
| 658 | job: running.map(|r| JobStatus { kind: r.kind.clone(), id: r.id.clone(), name: r.name.clone(), repo: r.repo.clone() }), | |
| 659 | updated_at: g1t_now(now), | |
| 660 | }; | |
| 661 | let temp = folder.join("status.json.new"); | |
| 662 | if serde_json::to_vec_pretty(&status).ok().is_some_and(|bytes| std::fs::write(&temp, bytes).is_ok()) { | |
| 663 | let _ = std::fs::rename(&temp, status_path(folder)); | |
| 664 | } | |
| 665 | } | |
| 666 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 667 | /// Runs the updated program in this one's place, with the same arguments. |
| 668 | fn restart() -> Result<()> { | |
| 669 | let me = std::env::current_exe()?; | |
| 670 | let status = std::process::Command::new(me).args(std::env::args().skip(1)).status()?; | |
| 671 | std::process::exit(status.code().unwrap_or(1)); | |
| 672 | } | |
| 673 | ||
| 674 | #[cfg(test)] | |
| 675 | mod tests { | |
| 676 | use super::*; | |
| 677 | ||
| 678 | fn args(items: &[&str]) -> Vec<String> { | |
| 679 | items.iter().map(|s| s.to_string()).collect() | |
| 680 | } | |
| 681 | ||
| 682 | #[test] | |
| 683 | fn commands_are_told_apart_from_the_harness() { | |
| 684 | assert!(is_command(&args(&["register", "--url", "x"]))); | |
| 685 | assert!(is_command(&args(&["run"]))); | |
| 686 | assert!(!is_command(&args(&[]))); | |
| 687 | assert!(!is_command(&args(&["something"]))); | |
| 688 | } | |
| 689 | ||
| 690 | #[test] | |
| 691 | fn options_read_both_ways() { | |
| 692 | let options = Options::parse(&args(&["--url", "https://g1t.sh", "--labels=gpu,cuda", "--ephemeral", "install"]), &["url", "labels"]).unwrap(); | |
| 693 | assert_eq!(options.value("url"), Some("https://g1t.sh")); | |
| 694 | assert_eq!(options.value("labels"), Some("gpu,cuda")); | |
| 695 | assert!(options.flag("ephemeral")); | |
| 696 | assert_eq!(options.rest, vec!["install".to_owned()]); | |
| 697 | assert!(Options::parse(&args(&["--url"]), &["url"]).is_err()); | |
| 698 | } | |
| 699 | ||
| 700 | #[test] | |
| 701 | fn a_machines_name_becomes_a_runners() { | |
| 702 | assert_eq!(runner_name("Build Box #1"), "Build-Box--1"); | |
| 703 | assert_eq!(runner_name(""), "runner"); | |
| 704 | assert_eq!(runner_name(&"x".repeat(80)).len(), 64); | |
| 705 | } | |
| 706 | ||
| 707 | #[test] | |
| 708 | fn the_log_says_when() { | |
| 709 | assert_eq!(g1t_now(1_790_918_179_123), "2026-10-02T05:16:19Z"); | |
| 710 | } | |
| 711 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.