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.
| Deploy scripts live in the repository | 1 | //! Runs a coding agent on one pull request and reports back to g1t. |
| 2 | //! | |
| 3 | //! This is the program a hosted sandbox starts. It clones the pull | |
| 4 | //! request's fork, runs the agent harness headless, streams what the agent | |
| 5 | //! does into the pull request's session as it happens, pushes the result | |
| 6 | //! and marks the pull request ready for review. It talks to g1t only through the public API and git, exactly | |
| 7 | //! as an agent on someone's own machine would. | |
| 8 | //! | |
| 9 | //! `MODE` selects another job instead: `checks` runs acceptance checks, | |
| 10 | //! `update` brings a pull request up to date with its target branch, | |
| 11 | //! `review` has an agent review one, and `revise` sends the author back to | |
| 12 | //! address what the checks or a review found, `plan` turns an outcome | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 13 | //! into issues, `queue` builds and checks a state of the merge queue, |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 14 | //! `mergecheck` finds out whether a pull request merges cleanly, |
| Merge branch 'worktree-agent-ac5b181a013e54348' | 15 | //! `actions` runs one job of a GitHub Actions workflow, `backup` cuts a |
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 16 | //! repository's nightly backup bundle, `bump` makes a |
| 17 | //! security update: one package raised in its lockfiles, pushed as g1t, | |
| 18 | //! and `computer` is a workspace agent's own computer, served for as long | |
| 19 | //! as it is awake. See the modules of those names. | |
| Deploy scripts live in the repository | 20 | //! |
| 21 | //! Configuration comes from the environment: | |
| 22 | //! | |
| 23 | //! - `G1T_API`, `G1T_TOKEN`, `G1T_USER`: where and who to report as. | |
| 24 | //! - `G1T_REPO`, `PULL_NUMBER`, `GIT_REMOTE`: the pull request and its fork. | |
| 25 | //! - `PROMPT`: what the agent is asked to do. | |
| 26 | //! - `COMMIT_MESSAGE`: used if the agent leaves changes uncommitted. | |
| 27 | //! - `ANTHROPIC_API_KEY`: read by the harness itself. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 28 | //! |
| 29 | //! Every mode runs with the mining watch in `abuse`: a sandbox that looks | |
| 30 | //! like it is mining stops itself and exits with `abuse::EXIT_CODE`. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 31 | //! |
| 32 | //! Given a command instead (`register`, `run`, `service`, `remove`, | |
| 33 | //! `update`, `version`), it is a self-hosted runner on someone's own | |
| 34 | //! machine, which runs work in these modes: see `selfhosted`. | |
| Deploy scripts live in the repository | 35 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 36 | mod abuse; |
| Deploy scripts live in the repository | 37 | mod actions; |
| Merge branch 'worktree-agent-ac5b181a013e54348' | 38 | mod backup; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 39 | mod bump; |
| Deploy scripts live in the repository | 40 | mod checks; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 41 | mod clone; |
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 42 | mod computer; |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 43 | mod confidence; |
| Deploy scripts live in the repository | 44 | mod deploy; |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 45 | mod docker; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 46 | mod guard; |
| Deploy scripts live in the repository | 47 | mod harness; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 48 | mod learned; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 49 | mod mergecheck; |
| Deploy scripts live in the repository | 50 | mod plan; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 51 | mod progress; |
| Deploy scripts live in the repository | 52 | mod queue; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 53 | mod reply; |
| Deploy scripts live in the repository | 54 | mod report; |
| 55 | mod review; | |
| 56 | mod revise; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 57 | mod selfhosted; |
| Deploy scripts live in the repository | 58 | mod steer; |
| 59 | mod update; | |
| 60 | ||
| 61 | use std::path::Path; | |
| 62 | use std::process::Command; | |
| 63 | ||
| 64 | use anyhow::{Context, Result, bail}; | |
| 65 | use base64::Engine; | |
| 66 | use base64::engine::general_purpose::STANDARD; | |
| 67 | ||
| 68 | use report::{Entry, Reporter}; | |
| 69 | ||
| 70 | pub(crate) const WORKDIR: &str = "/work/repo"; | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 71 | /// The name and address on every commit g1t makes here, whether its agent |
| 72 | /// or g1t itself: `g1t_contracts::system::{USERNAME, EMAIL}`. | |
| 73 | pub(crate) const AUTHOR_NAME: &str = "g1t"; | |
| 74 | pub(crate) const AUTHOR_EMAIL: &str = "g1t@users.noreply.g1t.sh"; | |
| Deploy scripts live in the repository | 75 | |
| 76 | pub(crate) fn env(name: &str) -> Result<String> { | |
| 77 | std::env::var(name).with_context(|| format!("{name} is not set")) | |
| 78 | } | |
| 79 | ||
| 80 | /// Runs git and returns its trimmed output, failing on a non-zero exit. | |
| 81 | pub(crate) fn git(dir: &Path, args: &[&str]) -> Result<String> { | |
| 82 | let output = Command::new("git") | |
| 83 | .current_dir(dir) | |
| 84 | .args(args) | |
| 85 | .output() | |
| 86 | .context("could not run git")?; | |
| 87 | if !output.status.success() { | |
| 88 | bail!( | |
| 89 | "git {} failed: {}", | |
| 90 | args.first().unwrap_or(&""), | |
| 91 | String::from_utf8_lossy(&output.stderr).trim() | |
| 92 | ); | |
| 93 | } | |
| 94 | Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) | |
| 95 | } | |
| 96 | ||
| 97 | /// A git option that authenticates one command. The credential is passed | |
| 98 | /// per command and never written to the clone's config or its remote URL, | |
| 99 | /// where the agent would find it. | |
| 100 | pub(crate) fn auth_option(user: &str, token: &str) -> String { | |
| 101 | let credentials = STANDARD.encode(format!("{user}:{token}")); | |
| 102 | format!("http.extraHeader=Authorization: Basic {credentials}") | |
| 103 | } | |
| 104 | ||
| 105 | /// Clones the fork, runs the agent on `PROMPT`, commits and pushes what it | |
| 106 | /// did, and returns its closing summary. | |
| 107 | pub(crate) fn run(reporter: &mut Reporter) -> Result<String> { | |
| 108 | let mut prompt = env("PROMPT")?; | |
| 109 | let remote = env("GIT_REMOTE")?; | |
| 110 | let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?); | |
| 111 | let workdir = Path::new(WORKDIR); | |
| 112 | ||
| Merge branch 'model-routing' | 113 | // Which model, and why: the router's one line names both. |
| 114 | let reason = std::env::var("AGENT_MODEL_REASON").unwrap_or_default(); | |
| 115 | if !reason.trim().is_empty() { | |
| 116 | reporter.record(Entry::new("note", reason.trim())); | |
| 117 | } else if let Ok(model) = std::env::var("AGENT_MODEL_NAME") { | |
| Deploy scripts live in the repository | 118 | reporter.record(Entry::new("note", &format!("Running on {model}."))); |
| 119 | } | |
| 120 | reporter.record(Entry::new("prompt", &prompt)); | |
| 121 | reporter.flush(); | |
| 122 | ||
| 123 | std::fs::create_dir_all("/work")?; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 124 | clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR).context("could not clone the pull request's fork")?; |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 125 | git(workdir, &["config", "user.name", crate::AUTHOR_NAME])?; |
| 126 | git(workdir, &["config", "user.email", crate::AUTHOR_EMAIL])?; | |
| Deploy scripts live in the repository | 127 | let branch = git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"])?; |
| 128 | let start = git(workdir, &["rev-parse", "HEAD"]).unwrap_or_default(); | |
| 129 | ||
| 130 | // Sent back to work that is already open: start from where the branch it | |
| 131 | // will land on is now, so what passes here passes there too. | |
| 132 | if let (Ok(upstream), Ok(upstream_branch)) = (env("UPSTREAM_REMOTE"), env("UPSTREAM_BRANCH")) { | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 133 | clone::fetch(workdir, &auth, &upstream, &upstream_branch).context("could not fetch the branch this will land on")?; |
| 134 | // Shallow: deep enough to tell whether it is behind, and to merge. | |
| 135 | clone::share_history(workdir, &auth, &[("origin", branch.as_str()), (upstream.as_str(), upstream_branch.as_str())], "HEAD", "FETCH_HEAD")?; | |
| Deploy scripts live in the repository | 136 | let behind = Command::new("git") |
| 137 | .current_dir(workdir) | |
| 138 | .args(["merge-base", "--is-ancestor", "FETCH_HEAD", "HEAD"]) | |
| 139 | .status() | |
| 140 | .is_ok_and(|status| !status.success()); | |
| 141 | if behind { | |
| 142 | let message = format!("Catch up with {upstream_branch}"); | |
| 143 | let merged = Command::new("git") | |
| 144 | .current_dir(workdir) | |
| 145 | .args(["merge", "--quiet", "--no-edit", "-m", &message, "FETCH_HEAD"]) | |
| 146 | .status() | |
| 147 | .is_ok_and(|status| status.success()); | |
| 148 | if merged { | |
| 149 | reporter.record(Entry::new( | |
| 150 | "note", | |
| 151 | &format!("Merged in the latest {upstream_branch} before starting."), | |
| 152 | )); | |
| 153 | } else { | |
| 154 | let files = git(workdir, &["diff", "--name-only", "--diff-filter=U"])?; | |
| 155 | let files: Vec<&str> = files.lines().collect(); | |
| 156 | reporter.record(Entry::new( | |
| 157 | "note", | |
| 158 | &format!( | |
| 159 | "Merged in the latest {upstream_branch} before starting; {} conflict.", | |
| 160 | files.join(", ") | |
| 161 | ), | |
| 162 | )); | |
| 163 | prompt.push_str(&format!( | |
| 164 | "\n\nBefore you started, the latest {upstream_branch} was merged into this branch, and these files conflict: {}. Resolve the conflicts first, keeping what both sides meant, then address the points above. Leave no conflict markers.", | |
| 165 | files.join(", ") | |
| 166 | )); | |
| 167 | } | |
| 168 | reporter.flush(); | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 172 | // The agent is asked what it learned, which goes to memory, and how sure |
| 173 | // it is of its change, which g1t weighs with what it observes. Neither | |
| 174 | // stays in the summary. | |
| 175 | let asked = confidence::ask(&learned::ask(&prompt)); | |
| 176 | let summary = confidence::finish(learned::finish(harness::run_claude(workdir, &asked, reporter)?)); | |
| Deploy scripts live in the repository | 177 | |
| 178 | // Commit whatever the agent left in the working tree. | |
| 179 | if !git(workdir, &["status", "--porcelain"])?.is_empty() { | |
| 180 | let message = std::env::var("COMMIT_MESSAGE").unwrap_or_else(|_| "Agent changes".into()); | |
| 181 | git(workdir, &["add", "--all"])?; | |
| 182 | git(workdir, &["commit", "--quiet", "--message", &message])?; | |
| 183 | } | |
| 184 | let head = git(workdir, &["rev-parse", "HEAD"])?; | |
| 185 | if head == start { | |
| 186 | // An agent woken to answer usually only answers. | |
| 187 | if std::env::var("MODE").as_deref() == Ok("answer") { | |
| 188 | return Ok(summary); | |
| 189 | } | |
| 190 | bail!("the agent finished without changing anything"); | |
| 191 | } | |
| 192 | git( | |
| 193 | workdir, | |
| 194 | &[ | |
| 195 | "-c", | |
| 196 | &auth, | |
| 197 | "push", | |
| 198 | "--quiet", | |
| 199 | "origin", | |
| 200 | &format!("HEAD:{branch}"), | |
| 201 | ], | |
| 202 | ) | |
| 203 | .context("could not push the pull request's commits")?; | |
| 204 | reporter.record(Entry::new( | |
| 205 | "note", | |
| 206 | &format!("Pushed {}.", &head[..head.len().min(12)]), | |
| 207 | )); | |
| 208 | Ok(summary) | |
| 209 | } | |
| 210 | ||
| 211 | fn main() { | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 212 | // The runc the job's Docker Engine starts containers with (docker/oci.rs). |
| 213 | if docker::oci::invoked_as_runc() { | |
| 214 | docker::oci::main(); | |
| 215 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 216 | // A self-hosted runner's commands; the modes below are what it, and |
| 217 | // g1t's sandboxes, run work with. | |
| 218 | let args: Vec<String> = std::env::args().skip(1).collect(); | |
| 219 | if selfhosted::is_command(&args) { | |
| 220 | std::process::exit(selfhosted::main(args)); | |
| 221 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 222 | // A guarded sandbox's HTTPS is re-signed on its way out: trust that |
| 223 | // before anything is fetched. The guard hook runs before every tool | |
| 224 | // call, so it skips this. | |
| 225 | if std::env::var("MODE").as_deref() == Ok("guard") { | |
| 226 | std::process::exit(guard::hook_main()); | |
| 227 | } | |
| 228 | guard::trust_egress_ca(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 229 | // Watches for mining for as long as the sandbox runs (abuse.rs). Not in |
| 230 | // the hooks the harness runs after every tool call. | |
| 231 | if std::env::var("MODE").as_deref() != Ok("steer") { | |
| 232 | abuse::watch(); | |
| 233 | } | |
| Deploy scripts live in the repository | 234 | // The same image does the other jobs a sandbox is started for. |
| 235 | match std::env::var("MODE").as_deref() { | |
| 236 | Ok("actions") => std::process::exit(actions::main()), | |
| Merge branch 'worktree-agent-ac5b181a013e54348' | 237 | Ok("backup") => std::process::exit(backup::main()), |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 238 | Ok("bump") => std::process::exit(bump::main()), |
| Deploy scripts live in the repository | 239 | Ok("checks") => std::process::exit(checks::main()), |
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 240 | // An agent's own computer: serves until it is put to sleep. |
| 241 | Ok("computer") => std::process::exit(computer::main()), | |
| Deploy scripts live in the repository | 242 | Ok("deploy") => std::process::exit(deploy::main()), |
| 243 | Ok("update") => std::process::exit(update::main()), | |
| 244 | Ok("review") => std::process::exit(review::main()), | |
| 245 | Ok("revise") => std::process::exit(revise::main()), | |
| 246 | Ok("answer") => std::process::exit(revise::answer()), | |
| 247 | Ok("plan") => std::process::exit(plan::main()), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 248 | Ok("reply") => std::process::exit(reply::main()), |
| Deploy scripts live in the repository | 249 | Ok("queue") => std::process::exit(queue::main()), |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 250 | Ok("mergecheck") => std::process::exit(mergecheck::main()), |
| Deploy scripts live in the repository | 251 | Ok("steer") => std::process::exit(steer::main()), |
| 252 | _ => {} | |
| 253 | } | |
| 254 | let mut reporter = match Reporter::from_env() { | |
| 255 | Ok(reporter) => reporter, | |
| 256 | Err(error) => { | |
| 257 | eprintln!("g1t-runner: {error:#}"); | |
| 258 | std::process::exit(2); | |
| 259 | } | |
| 260 | }; | |
| 261 | match run(&mut reporter) { | |
| 262 | Ok(summary) => { | |
| 263 | reporter.flush(); | |
| 264 | if let Err(error) = reporter.ready(&summary) { | |
| 265 | eprintln!("g1t-runner: could not mark the pull request ready: {error:#}"); | |
| 266 | std::process::exit(1); | |
| 267 | } | |
| 268 | } | |
| 269 | Err(error) => { | |
| 270 | eprintln!("g1t-runner: {error:#}"); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 271 | // Stopped at a cap: like a person's stop, the pull request is |
| 272 | // left open for a person, not closed. | |
| 273 | if guard::is_halt(&error) { | |
| 274 | reporter.record(Entry::new("note", &format!("g1t stopped the agent: {error:#}."))); | |
| 275 | reporter.flush(); | |
| 276 | std::process::exit(1); | |
| 277 | } | |
| Deploy scripts live in the repository | 278 | reporter.record(Entry::new("note", &format!("The run failed: {error:#}"))); |
| 279 | reporter.flush(); | |
| 280 | let _ = reporter.close(); | |
| 281 | std::process::exit(1); | |
| 282 | } | |
| 283 | } | |
| 284 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.