Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
764 linesCodeBlameRaw

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

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.1//! Running what g1t hands the runner: a workflow job, or g1t's own work (a
2//! merge check or a state of the merge queue), with the same harness g1t's
3//! sandboxes run (this program, in another mode).
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push4//!
5//! - **In Docker** (the default): one container per job, removed when it
6//! ends, from the job's `container:` image or the runner's `--image`. A
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r97//! Linux build of the harness is mounted into it read-only; that and the
8//! job's own work volume, removed after, are its only mounts. It runs
9//! with every capability dropped but the few a build needs, no new
10//! privileges, a process, memory and CPU limit, and a network of its
11//! own. With nested-container isolation (sandbox.rs), all of this is on
12//! the runner's own Docker Engine, inside the runner's container, and a
13//! job that uses Docker gets an engine of its own (engine.rs).
14//! - **Directly** (`--no-docker`, deprecated): the harness runs as a child
15//! process, in a fresh folder under the runner's work folder that is
16//! removed after. Unsandboxed: the job's log says so, and a public
17//! repository's work is refused.
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push18//!
19//! The runner's own credential never reaches either: a job only ever has
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r920//! its own short-lived token, as it would in g1t's sandbox. Credentials are
21//! passed by name (`-e NAME`), never on the command line.
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push22
23use std::collections::BTreeMap;
24use std::path::{Path, PathBuf};
25use std::process::{Child, Command, Stdio};
26use std::time::{Duration, Instant};
27
28use anyhow::{Context, Result, bail};
29
30use super::api::Assignment;
31use super::config::Config;
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r932use super::sandbox::{self, Isolation};
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push33use super::{log, update};
34
35/// The image workflow jobs run in when they name none: Debian with Node,
36/// git and Python, as g1t's sandbox has.
37pub const DEFAULT_IMAGE: &str = "node:24-bookworm";
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r938/// The most processes in a job's container, unless the runner says.
39pub const DEFAULT_PIDS_LIMIT: u32 = 4096;
40
41/// The capabilities a job's container keeps; Docker's others, and
42/// everything else, are dropped. A workflow job runs as its image's user,
43/// root for most CI images, and these are what installing packages and
44/// building need:
45///
46/// - `CHOWN`, `FOWNER`, `FSETID`, `DAC_OVERRIDE`: a package manager (apt,
47/// npm as root) writing files it then hands to another owner, keeping
48/// setgid bits, and root writing files it does not own.
49/// - `SETUID`, `SETGID`: apt and others drop to their own user (`_apt`) to
50/// download; with no new privileges, nothing can gain a user back.
51/// - `KILL`: a step stopping processes another user started.
52/// - `NET_BIND_SERVICE`: a test server on port 80 or 443.
53///
54/// Not kept, among others: `NET_RAW` (raw sockets, `ping`), `MKNOD`,
55/// `SYS_CHROOT`, `SETFCAP`, `AUDIT_WRITE`, and every `SYS_*` and `NET_ADMIN`.
56pub const JOB_CAPS: &[&str] = &["CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "SETUID", "SETGID", "KILL", "NET_BIND_SERVICE"];
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push57
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.58/// The work kind g1t's own tasks come as: a merge check or a state of the
59/// merge queue, with the environment g1t's sandbox would have had. Older
60/// g1t sent agents' runs as this kind too; those are refused.
61pub const TASK_KIND: &str = "agent";
62
63/// The modes of g1t's own tasks this runner runs (main.rs).
64pub const TASK_MODES: &[&str] = &["queue", "mergecheck", "checks"];
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r965
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push66/// Work in progress.
67pub struct Running {
68 pub id: String,
69 pub kind: String,
70 pub name: String,
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.71 pub repo: String,
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push72 child: Child,
73 container: Option<String>,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r974 /// The job's network and work volume, removed after.
75 network: Option<String>,
76 volume: Option<String>,
77 /// The job's own Docker Engine, when it may have one.
78 engine: Option<JobEngine>,
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push79 folder: Option<PathBuf>,
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.80 /// The job's own VM, for a job native to this OS (native.rs); `child`
81 /// is then `wsb exec`, whose output carries the job's exit code.
82 native: Option<WsbJob>,
83 /// How it ended, once read: a native job's code is read once from
84 /// `wsb exec`'s output.
85 exit: Option<i32>,
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push86 /// Past its time limit and a grace for g1t to say so: stopped here.
87 deadline: Instant,
88}
89
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.90use super::native::{self, WsbJob};
91
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r992#[cfg(unix)]
93use super::engine::JobEngine;
94
95/// No job has an engine of its own off Unix.
96#[cfg(not(unix))]
97pub struct JobEngine;
98
99#[cfg(not(unix))]
100impl JobEngine {
101 fn remove(&self) {}
102 fn job_args(&self) -> Vec<String> {
103 Vec::new()
104 }
105}
106
107/// `docker`, at the runner's own engine when it has one.
108fn docker() -> Command {
109 #[cfg(unix)]
110 return super::engine::docker();
111 #[cfg(not(unix))]
112 Command::new("docker")
113}
114
115fn quietly(command: &mut Command) {
116 let _ = command.stdout(Stdio::null()).stderr(Stdio::null()).status();
117}
118
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push119impl Running {
120 pub fn overdue(&self) -> bool {
121 Instant::now() > self.deadline
122 }
123
124 /// Its exit code once it has ended.
125 pub fn ended(&mut self) -> Option<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.126 if self.exit.is_some() {
127 return self.exit;
128 }
129 self.exit = match self.child.try_wait() {
130 Ok(Some(_)) if self.native.is_some() => {
131 use std::io::Read;
132 let mut text = String::new();
133 if let Some(out) = self.child.stdout.as_mut() {
134 let _ = out.read_to_string(&mut text);
135 }
136 Some(native::parse_exit_code(&text).unwrap_or(1))
137 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push138 Ok(Some(status)) => Some(status.code().unwrap_or(1)),
139 Ok(None) => None,
140 Err(_) => Some(1),
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 };
142 self.exit
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push143 }
144
145 /// Stops it: the container, then the process and everything it started.
146 pub fn stop(&mut self) {
147 if let Some(container) = &self.container {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9148 quietly(docker().args(["kill", container]));
149 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9150 #[cfg(unix)]
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9151 if let Some(engine) = &self.engine {
152 quietly(docker().args(["kill", &engine.container]));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push153 }
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.154 if let Some(job) = &self.native {
155 job.stop();
156 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push157 let pid = self.child.id().to_string();
158 if cfg!(windows) {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9159 quietly(Command::new("taskkill").args(["/T", "/F", "/PID", &pid]));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push160 } else {
161 // Its own process group (see `directly`): the steps' processes too.
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9162 quietly(Command::new("kill").args(["-TERM", &format!("-{pid}")]));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push163 }
164 let _ = self.child.kill();
165 let _ = self.child.wait();
166 }
167
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9168 /// Removes what it left: its container, its engine and that engine's
169 /// data, its network, its work volume, its folder.
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push170 pub fn clean(&mut self) {
171 if let Some(container) = &self.container {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9172 quietly(docker().args(["rm", "-f", "-v", container]));
173 }
174 if let Some(engine) = self.engine.take() {
175 engine.remove();
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push176 }
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9177 if let Some(network) = &self.network {
178 quietly(docker().args(["network", "rm", network]));
179 }
180 if let Some(volume) = &self.volume {
181 quietly(docker().args(["volume", "rm", "-f", volume]));
182 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push183 if let Some(folder) = &self.folder {
184 let _ = std::fs::remove_dir_all(folder);
185 }
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.186 if let Some(job) = self.native.take() {
187 job.clean();
188 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push189 }
190}
191
192/// A name Docker takes, from a job's id.
193fn container_name(id: &str) -> String {
194 let clean: String = id.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' { c } else { '-' }).collect();
195 format!("g1t-{clean}")
196}
197
198/// The environment the harness gets for this work.
199fn harness_env(config: &Config, work: &Assignment) -> Result<BTreeMap<String, String>> {
200 let mut env = BTreeMap::new();
201 match work.kind.as_str() {
202 "workflow" => {
203 let Some(token) = &work.token else { bail!("the job came without its token") };
204 env.insert("MODE".into(), "actions".into());
205 env.insert("G1T_API".into(), config.api.clone());
206 env.insert("ACTIONS_JOB".into(), work.id.clone());
207 env.insert("ACTIONS_TOKEN".into(), token.clone());
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.208 // That token is where this computer can see it (a Windows job's
209 // script in the job's folder, a container's environment): the
210 // harness spends it on fetching the job and works on with a new
211 // one only it has.
212 env.insert(crate::actions::ROTATE_TOKEN.into(), "1".into());
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push213 }
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.214 TASK_KIND => {
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push215 for (name, value) in work.env.clone().unwrap_or_default() {
216 let value = match value {
217 serde_json::Value::String(text) => text,
218 other => other.to_string(),
219 };
220 env.insert(name, value);
221 }
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.222 match env.get("MODE").map(String::as_str) {
223 Some(mode) if TASK_MODES.contains(&mode) => {}
224 Some(mode) => bail!("this runner does not run g1t work of mode {mode}"),
225 None => bail!("this runner does not run agents' work"),
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push226 }
227 // Where its API calls go: this installation's.
228 env.insert("G1T_API".into(), config.api.clone());
229 }
230 other => bail!("this runner does not know work of kind {other}; update it"),
231 }
232 // The machine is the workspace's own: no mining watch.
233 env.insert("G1T_ABUSE".into(), "off".into());
234 Ok(env)
235}
236
237/// Starts work. The harness reports a workflow job's steps and log itself;
238/// the runner tells g1t how the process ended.
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9239pub fn start(config: &Config, isolation: Isolation, folder: &Path, work: &Assignment) -> Result<Running> {
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push240 let env = harness_env(config, work)?;
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.241 match isolation {
242 Isolation::Host => {
243 refuse_public_on_host(work)?;
244 directly(config, work, env)
245 }
246 Isolation::Vm => in_native_vm(config, folder, work, env),
247 _ => in_docker(config, isolation, folder, work, env),
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9248 }
249}
250
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.251/// A memory size as `--memory` takes it (`8g`, `4096m`), in MiB.
252pub fn mib(text: &str) -> Option<u64> {
253 let text = text.trim().to_ascii_lowercase();
254 let (number, unit) = text.split_at(text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len()));
255 let number: u64 = number.parse().ok()?;
256 match unit.trim_end_matches('b').trim_end_matches('i') {
257 "" | "m" => Some(number),
258 "g" => Some(number * 1024),
259 "k" => Some(number / 1024),
260 _ => None,
261 }
262}
263
264/// A job native to this OS, in a throwaway VM of its own (native.rs):
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.265/// Windows Sandbox on Windows. g1t's own work needs its Linux sandbox
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.266/// layout and runs on a Linux runner.
267fn in_native_vm(config: &Config, folder: &Path, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> {
268 if work.kind != "workflow" {
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.269 bail!("g1t's own work runs on Linux runners: this one runs jobs native to {}", std::env::consts::OS);
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.270 }
271 if !cfg!(windows) {
272 bail!("this runner has no sandbox for jobs native to {}", std::env::consts::OS);
273 }
274 let memory = config.memory.as_deref().and_then(mib).unwrap_or(4096);
275 let drop = folder.join("native").join(container_name(&work.id));
276 let job = WsbJob::start(drop, &env, memory)?;
277 let child = match job.exec().spawn() {
278 Ok(child) => child,
279 Err(error) => {
280 job.clean();
281 return Err(error).context("could not run wsb exec");
282 }
283 };
284 log(&format!(
285 "Running {} {} in a Windows Sandbox of its own ({memory} MiB): no folder of this machine's but its read-only harness, discarded after",
286 work.kind, work.name
287 ));
288 Ok(Running {
289 id: work.id.clone(),
290 repo: work.repo.clone(),
291 kind: work.kind.clone(),
292 name: work.name.clone(),
293 child,
294 container: None,
295 network: None,
296 volume: None,
297 engine: None,
298 folder: None,
299 native: Some(job),
300 exit: None,
301 deadline: deadline(work),
302 })
303}
304
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9305/// Host mode does not take a public repository's work: anyone can propose
306/// code to one, and here it would run on the machine itself.
307fn refuse_public_on_host(work: &Assignment) -> Result<()> {
308 match work.visibility.as_deref() {
309 Some("public") => bail!("{}", sandbox::host_mode_refuses(&work.repo)),
310 Some(_) => Ok(()),
311 None => {
312 log(&format!("g1t did not say whether {} is public (an older g1t), so it runs; host mode refuses public repositories' work once it does.", work.repo));
313 Ok(())
314 }
315 }
316}
317
318/// Each job container's limits.
319#[derive(Debug, Clone, PartialEq)]
320pub struct Limits {
321 pub memory: String,
322 pub cpus: String,
323 pub pids: u32,
324}
325
326/// Three quarters of `MemTotal` in `/proc/meminfo`, in MiB, which under
327/// Sysbox is the runner container's own memory.
328fn default_memory(meminfo: &str) -> Option<String> {
329 let kb: u64 = meminfo.lines().find_map(|l| l.strip_prefix("MemTotal:"))?.trim().trim_end_matches("kB").trim().parse().ok()?;
330 let mib = kb / 1024 * 3 / 4;
331 (mib >= 256).then(|| format!("{mib}m"))
332}
333
334pub fn limits(config: &Config) -> Limits {
335 let memory = config
336 .memory
337 .clone()
338 .or_else(|| default_memory(&std::fs::read_to_string("/proc/meminfo").unwrap_or_default()))
339 .unwrap_or_else(|| "4g".into());
340 let cpus = config.cpus.clone().unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(2).to_string());
341 Limits { memory, cpus, pids: config.pids_limit.unwrap_or(DEFAULT_PIDS_LIMIT) }
342}
343
344/// Everything a job's `docker run` is made from.
345pub struct Plan<'a> {
346 pub name: &'a str,
347 pub runner: &'a str,
348 pub image: &'a str,
349 pub harness: &'a Path,
350 pub network: &'a str,
351 /// The job's work volume, for a workflow job's `/home/runner`.
352 pub volume: Option<&'a str>,
353 pub limits: &'a Limits,
354 /// The names of the harness's variables, whose values come from the
355 /// runner's process, never its command line.
356 pub env_names: Vec<&'a str>,
357 /// What the job's own engine adds (`JobEngine::job_args`).
358 pub engine_args: Vec<String>,
359}
360
361/// The arguments of `docker` that start a job's container.
362pub fn run_args(plan: &Plan) -> Vec<String> {
363 let mut args: Vec<String> = Vec::new();
364 fn add(args: &mut Vec<String>, items: &[&str]) {
365 args.extend(items.iter().map(|s| s.to_string()));
366 }
367 add(&mut args, &["run", "--rm", "--name", plan.name, "--label", &format!("sh.g1t.runner={}", plan.runner)]);
368 add(&mut args, &["--pull", "missing", "--init"]);
369 // The walls.
370 add(&mut args, &["--cap-drop", "ALL"]);
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.371 add(&mut args, &["--security-opt", "no-new-privileges"]);
372 for cap in JOB_CAPS {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9373 add(&mut args, &["--cap-add", cap]);
374 }
375 let pids = plan.limits.pids.to_string();
376 add(&mut args, &["--pids-limit", &pids, "--memory", &plan.limits.memory, "--memory-swap", &plan.limits.memory, "--cpus", &plan.limits.cpus]);
377 add(&mut args, &["--network", plan.network, "--ipc", "private"]);
378 // The job is in its `container:` image already: the harness inside
379 // does not start it again (actions/containers.rs).
380 add(&mut args, &["-e", "G1T_JOB_CONTAINER=1"]);
381 for name in &plan.env_names {
382 add(&mut args, &["-e", name]);
383 }
384 // Its only mounts: the harness, read-only, and its own volume.
385 add(&mut args, &["-v", &format!("{}:/opt/g1t/g1t-runner:ro", plan.harness.display())]);
386 if let Some(volume) = plan.volume {
387 add(&mut args, &["-v", &format!("{volume}:/home/runner")]);
388 }
389 args.extend(plan.engine_args.iter().cloned());
390 add(&mut args, &["--entrypoint", "/opt/g1t/g1t-runner", plan.image]);
391 args
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push392}
393
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9394fn in_docker(config: &Config, isolation: Isolation, folder: &Path, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> {
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.395 // g1t's own work needs only git, which the default image has.
396 let image = work.image.clone().or_else(|| config.image.clone()).unwrap_or_else(|| DEFAULT_IMAGE.to_owned());
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9397 #[cfg(unix)]
398 if super::engine::in_use() {
399 super::engine::ensure_running()?;
400 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push401 let harness = update::linux_harness(config, folder)?;
402 let name = container_name(&work.id);
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9403 // What an earlier run of this job may have left.
404 quietly(docker().args(["rm", "-f", "-v", &name]));
405 quietly(docker().args(["network", "rm", &name]));
406 let output = docker()
407 .args(["network", "create", "--label", &format!("sh.g1t.runner={}", config.runner), &name])
408 .output()
409 .context("could not run docker: is Docker installed and running?")?;
410 if !output.status.success() {
411 bail!("could not make the job's network: {}", String::from_utf8_lossy(&output.stderr).trim());
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push412 }
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9413 let volume = (work.kind == "workflow").then(|| format!("{name}-work"));
414 if let Some(volume) = &volume {
415 quietly(docker().args(["volume", "rm", "-f", volume]));
416 quietly(docker().args(["volume", "create", "--label", &format!("sh.g1t.runner={}", config.runner), volume]));
417 }
418 // A job's own Docker, only inside the runner's own sandbox: on a
419 // shared engine it would be the machine's.
420 let engine: Option<JobEngine> = job_engine(config, isolation, &name);
421 let limits = limits(config);
422 let plan = Plan {
423 name: &name,
424 runner: &config.runner,
425 image: &image,
426 harness: &harness,
427 network: &name,
428 volume: volume.as_deref(),
429 limits: &limits,
430 env_names: env.keys().map(String::as_str).collect(),
431 engine_args: engine.as_ref().map(JobEngine::job_args).unwrap_or_default(),
432 };
433 let mut command = docker();
434 command.args(run_args(&plan));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push435 command.envs(&env);
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9436 log(&format!(
437 "Running {} {} in {image}: its own container and network, capabilities dropped, {} memory, {} CPUs, {} processes{}",
438 work.kind,
439 work.name,
440 limits.memory,
441 limits.cpus,
442 limits.pids,
443 if engine.is_some() { ", Docker of its own when it uses it" } else { "" }
444 ));
445 let child = match command.spawn() {
446 Ok(child) => child,
447 Err(error) => {
448 if let Some(engine) = &engine {
449 engine.remove();
450 }
451 quietly(docker().args(["network", "rm", &name]));
452 return Err(error).context("could not run docker: is Docker installed and running?");
453 }
454 };
455 Ok(Running {
456 id: work.id.clone(),
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.457 repo: work.repo.clone(),
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9458 kind: work.kind.clone(),
459 name: work.name.clone(),
460 child,
461 container: Some(name.clone()),
462 network: Some(name),
463 volume,
464 engine,
465 folder: None,
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.466 native: None,
467 exit: None,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9468 deadline: deadline(work),
469 })
470}
471
472#[cfg(unix)]
473fn job_engine(config: &Config, isolation: Isolation, name: &str) -> Option<JobEngine> {
474 if isolation != Isolation::NestedContainer || !config.job_docker {
475 return None;
476 }
477 let image = config.job_docker_image.as_deref().unwrap_or(super::engine::DEFAULT_JOB_ENGINE_IMAGE);
478 match JobEngine::prepare(name, &config.runner, image) {
479 Ok(engine) => Some(engine),
480 Err(error) => {
481 log(&format!("This job has no Docker of its own: {error:#}"));
482 None
483 }
484 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push485}
486
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9487#[cfg(not(unix))]
488fn job_engine(_config: &Config, _isolation: Isolation, _name: &str) -> Option<JobEngine> {
489 None
490}
491
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push492fn directly(config: &Config, work: &Assignment, env: BTreeMap<String, String>) -> Result<Running> {
493 let me = std::env::current_exe().context("could not find this program to run the job")?;
494 let mut command = Command::new(me);
495 let mut folder = None;
496 if work.kind == "workflow" {
497 // GitHub's layout, in a folder of its own.
498 let root = config.work_dir.join(container_name(&work.id));
499 let _ = std::fs::remove_dir_all(&root);
500 std::fs::create_dir_all(&root).with_context(|| format!("could not make {}", root.display()))?;
501 command.env("G1T_RUNNER_ROOT", &root);
502 folder = Some(root);
503 } else if !cfg!(target_os = "linux") || std::fs::create_dir_all("/work").is_err() {
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.504 // g1t's own work expects its sandbox layout (/work), which only a
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push505 // Linux machine (or a container) set aside for it has.
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.506 bail!("g1t's own work runs in Docker, or directly on Linux where /work can be written; this runner can do neither");
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push507 }
508 // Its own variables, less anything of the runner's.
509 for (name, _) in std::env::vars_os() {
510 if name.to_string_lossy().starts_with("G1T_RUNNER_") && name != "G1T_RUNNER_ROOT" {
511 command.env_remove(&name);
512 }
513 }
514 command.envs(&env);
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9515 // The job's log says it ran unsandboxed (actions/mod.rs).
516 command.env(JOB_NOTICE, sandbox::HOST_MODE_IN_JOB);
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push517 // A group of its own, so stopping it stops what its steps started.
518 #[cfg(unix)]
519 std::os::unix::process::CommandExt::process_group(&mut command, 0);
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9520 log(&format!("Running {} {} on this machine, unsandboxed (host mode, deprecated)", work.kind, work.name));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push521 let child = command.spawn().context("could not start the job's process")?;
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9522 Ok(Running {
523 id: work.id.clone(),
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.524 repo: work.repo.clone(),
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9525 kind: work.kind.clone(),
526 name: work.name.clone(),
527 child,
528 container: None,
529 network: None,
530 volume: None,
531 engine: None,
532 folder,
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.533 native: None,
534 exit: None,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9535 deadline: deadline(work),
536 })
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push537}
538
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9539/// A warning the harness puts at the top of the job's log.
540pub const JOB_NOTICE: &str = "G1T_JOB_NOTICE";
541
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push542fn deadline(work: &Assignment) -> Instant {
543 Instant::now() + Duration::from_secs(u64::from(work.timeout_minutes.max(1)) * 60 + 10 * 60)
544}
545
546/// Whether Docker answers.
547pub fn docker_ready() -> bool {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9548 docker()
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push549 .args(["version", "--format", "{{.Server.Version}}"])
550 .stdout(Stdio::null())
551 .stderr(Stdio::null())
552 .status()
553 .is_ok_and(|status| status.success())
554}
555
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9556/// Removes what jobs of this runner's left behind, if it stopped while one ran.
557pub fn sweep(config: &Config) {
558 let label = format!("label=sh.g1t.runner={}", config.runner);
559 for (list, remove) in [
560 (vec!["ps", "-aq", "--filter", &label], vec!["rm", "-f", "-v"]),
561 (vec!["network", "ls", "-q", "--filter", &label], vec!["network", "rm"]),
562 (vec!["volume", "ls", "-q", "--filter", &label], vec!["volume", "rm", "-f"]),
563 ] {
564 let Ok(output) = docker().args(&list).output() else { return };
565 let ids: Vec<String> = String::from_utf8_lossy(&output.stdout).split_whitespace().map(str::to_owned).collect();
566 if !ids.is_empty() {
567 quietly(docker().args(&remove).args(&ids));
568 }
569 }
570}
571
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push572#[cfg(test)]
573mod tests {
574 use super::*;
575 use serde_json::json;
576
577 fn config() -> Config {
578 Config {
579 url: "https://g1t.sh".into(),
580 api: "https://api.g1t.sh".into(),
581 runner: "rnr_1".into(),
582 name: "a".into(),
583 credential: "g1tr_secret".into(),
584 workspace: "acme".into(),
585 repo: None,
586 group: None,
587 labels: vec![],
588 ephemeral: false,
589 work_dir: PathBuf::from("/tmp/w"),
590 docker: true,
591 image: None,
592 harness: None,
593 auto_update: false,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9594 memory: None,
595 cpus: None,
596 pids_limit: None,
597 job_docker: true,
598 job_docker_image: None,
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.599 native: false,
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push600 }
601 }
602
603 fn assignment(kind: &str) -> Assignment {
604 serde_json::from_value(json!({
605 "kind": kind, "id": "job_1", "name": "build", "repo": "acme/web", "timeout_minutes": 60,
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.606 "token": "jobtoken", "env": { "MODE": "queue", "G1T_TOKEN": "g1t_run", "G1T_API": "https://elsewhere" }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push607 }))
608 .unwrap()
609 }
610
611 #[test]
612 fn a_workflow_job_gets_its_own_token_and_never_the_runners() {
613 let env = harness_env(&config(), &assignment("workflow")).unwrap();
614 assert_eq!(env["MODE"], "actions");
615 assert_eq!(env["ACTIONS_JOB"], "job_1");
616 assert_eq!(env["ACTIONS_TOKEN"], "jobtoken");
617 assert_eq!(env["G1T_API"], "https://api.g1t.sh");
618 assert!(env.values().all(|value| !value.contains("g1tr_")));
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.619 // That token is spent on fetching the job.
620 assert_eq!(env[crate::actions::ROTATE_TOKEN], "1");
621 assert!(!harness_env(&config(), &assignment("agent")).unwrap().contains_key(crate::actions::ROTATE_TOKEN));
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push622 }
623
624 #[test]
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.625 fn g1ts_own_work_gets_the_sandboxs_environment_and_this_api() {
626 let env = harness_env(&config(), &assignment(TASK_KIND)).unwrap();
627 assert_eq!(env["MODE"], "queue");
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push628 assert_eq!(env["G1T_TOKEN"], "g1t_run");
629 assert_eq!(env["G1T_API"], "https://api.g1t.sh");
630 assert_eq!(env["G1T_ABUSE"], "off");
631 }
632
633 #[test]
634 fn unknown_work_is_refused() {
635 assert!(harness_env(&config(), &assignment("deploy")).is_err());
636 }
637
638 #[test]
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.639 fn an_agents_run_from_an_older_g1t_is_refused_before_it_starts() {
640 for env in [json!({ "MODE": "revise", "G1T_TOKEN": "g1t_run" }), json!({ "PROMPT": "Fix it", "G1T_TOKEN": "g1t_run" })] {
641 let mut work = assignment(TASK_KIND);
642 work.env = serde_json::from_value(env).unwrap();
643 assert!(harness_env(&config(), &work).is_err());
644 assert!(start(&config(), Isolation::Host, Path::new("/tmp"), &work).is_err());
645 }
646 }
647
648 #[test]
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push649 fn container_names_are_dockers() {
650 assert_eq!(container_name("job_01ab"), "g1t-job_01ab");
651 assert_eq!(container_name("a/b c"), "g1t-a-b-c");
652 }
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9653
654 fn args_for(kind: &str, engine_args: Vec<String>) -> (Vec<String>, BTreeMap<String, String>) {
655 let config = config();
656 let env = harness_env(&config, &assignment(kind)).unwrap();
657 let limits = Limits { memory: "6144m".into(), cpus: "4".into(), pids: 4096 };
658 let harness = PathBuf::from("/usr/local/bin/g1t-runner");
659 let plan = Plan {
660 name: "g1t-job_1",
661 runner: "rnr_1",
662 image: DEFAULT_IMAGE,
663 harness: &harness,
664 network: "g1t-job_1",
665 volume: (kind == "workflow").then_some("g1t-job_1-work"),
666 limits: &limits,
667 env_names: env.keys().map(String::as_str).collect(),
668 engine_args,
669 };
670 (run_args(&plan), env.clone())
671 }
672
673 fn value_of<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> {
674 args.windows(2).filter(|w| w[0] == flag).map(|w| w[1].as_str()).collect()
675 }
676
677 #[test]
678 fn a_jobs_container_is_walled_in() {
679 let (args, _) = args_for("workflow", Vec::new());
680 assert_eq!(args[0], "run");
681 assert!(args.contains(&"--rm".to_owned()));
682 assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]);
683 assert_eq!(value_of(&args, "--cap-add"), JOB_CAPS.to_vec());
684 assert_eq!(value_of(&args, "--security-opt"), vec!["no-new-privileges"]);
685 assert_eq!(value_of(&args, "--pids-limit"), vec!["4096"]);
686 assert_eq!(value_of(&args, "--memory"), vec!["6144m"]);
687 assert_eq!(value_of(&args, "--memory-swap"), vec!["6144m"]);
688 assert_eq!(value_of(&args, "--cpus"), vec!["4"]);
689 assert_eq!(value_of(&args, "--network"), vec!["g1t-job_1"]);
690 for forbidden in ["--privileged", "--pid", "--userns", "--device", "host"] {
691 assert!(!args.iter().any(|a| a == forbidden), "{forbidden} in {args:?}");
692 }
693 assert!(!JOB_CAPS.iter().any(|c| c.starts_with("SYS_") || *c == "NET_ADMIN" || *c == "NET_RAW"));
694 }
695
696 #[test]
697 fn a_jobs_only_mounts_are_the_harness_and_its_own_volume() {
698 let (args, _) = args_for("workflow", Vec::new());
699 let mounts = value_of(&args, "-v");
700 assert_eq!(mounts, vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro", "g1t-job_1-work:/home/runner"]);
701 assert!(!args.iter().any(|a| a.contains("docker.sock")), "no engine socket: {args:?}");
702 assert!(!args.iter().any(|a| a == "--mount"));
703 }
704
705 #[test]
706 fn credentials_go_by_name_and_the_runners_never() {
707 let (args, env) = args_for("workflow", Vec::new());
708 let passed = value_of(&args, "-e");
709 assert!(passed.contains(&"ACTIONS_TOKEN"));
710 // Never a value on the command line, and never the runner's credential.
711 assert!(!args.iter().any(|a| a.contains("jobtoken")));
712 assert!(!args.iter().any(|a| a.contains("g1tr_")));
713 assert!(env.values().all(|v| !v.contains("g1tr_secret")));
714 assert!(!env.contains_key("G1T_RUNNER_TOKEN"));
715 }
716
717 #[test]
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.718 fn g1ts_own_work_has_every_wall_a_job_has() {
719 let (args, _) = args_for(TASK_KIND, Vec::new());
720 assert_eq!(value_of(&args, "--security-opt"), vec!["no-new-privileges"]);
721 assert_eq!(value_of(&args, "--cap-add"), JOB_CAPS.to_vec());
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9722 assert_eq!(value_of(&args, "--cap-drop"), vec!["ALL"]);
723 assert_eq!(value_of(&args, "-v"), vec!["/usr/local/bin/g1t-runner:/opt/g1t/g1t-runner:ro"]);
724 }
725
726 #[test]
727 fn a_job_with_docker_gets_a_socket_of_its_own_not_the_runners() {
728 let engine_args = vec!["-v".into(), "/run/g1t-runner/jobs/g1t-job_1/proxy:/run/g1t-docker:ro".into(), "-e".into(), "DOCKER_HOST=unix:///run/g1t-docker/docker.sock".into()];
729 let (args, _) = args_for("workflow", engine_args);
730 let mounts = value_of(&args, "-v");
731 assert!(mounts.contains(&"/run/g1t-runner/jobs/g1t-job_1/proxy:/run/g1t-docker:ro"));
732 assert!(!mounts.iter().any(|m| m.starts_with("/var/run/docker.sock") || m.starts_with("/run/g1t-runner/docker.sock")));
733 // The entrypoint and image stay last.
734 assert_eq!(&args[args.len() - 3..], ["--entrypoint", "/opt/g1t/g1t-runner", DEFAULT_IMAGE]);
735 }
736
737 #[test]
738 fn limits_default_sensibly() {
739 assert_eq!(default_memory("MemTotal: 16384000 kB\nMemFree: 1 kB\n"), Some("12000m".into()));
740 assert_eq!(default_memory(""), None);
741 let mut config = config();
742 config.memory = Some("2g".into());
743 config.pids_limit = Some(100);
744 let limits = limits(&config);
745 assert_eq!(limits.memory, "2g");
746 assert_eq!(limits.pids, 100);
747 assert!(limits.cpus.parse::<f64>().unwrap() >= 1.0);
748 }
749
750 #[test]
751 fn host_mode_refuses_a_public_repositorys_work() {
752 let mut work = assignment("workflow");
753 work.visibility = Some("public".into());
754 let refused = refuse_public_on_host(&work).unwrap_err().to_string();
755 assert!(refused.contains("acme/web is public"), "{refused}");
756 work.visibility = Some("private".into());
757 assert!(refuse_public_on_host(&work).is_ok());
758 work.visibility = None;
759 assert!(refuse_public_on_host(&work).is_ok());
760 // And refuses it before anything runs.
761 work.visibility = Some("public".into());
762 assert!(start(&config(), Isolation::Host, Path::new("/tmp"), &work).is_err());
763 }
Agents no longer count not having run CI as a doubt: the workflows run as checks after the push764}

This file's history is long; its oldest lines are credited to the oldest commit read.