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