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.
| Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships | 1 | //! What a self-hosted runner calls: registering with a registration token, |
| 2 | //! then polling for work, saying how work ended and removing itself, with | |
| 3 | //! its own credential. Neither is a g1t access token, so these are served | |
| 4 | //! before anything reads one, and no other endpoint accepts either. | |
| 5 | //! | |
| 6 | //! | Route | Body | | |
| 7 | //! | --- | --- | | |
| A computer whose runner is registered again takes its own runner's name back instead of being refused. When the desktop app's sandbox was made again (a new VM, its registration gone), its runners registered under the names the old sandbox's still held on g1t, were refused with a 409 asking for --replace, and tried again forever. A runner now says which computer it is when it registers: a fingerprint g1t-runner works out from the operating system's machine id (Windows' MachineGuid, macOS's platform UUID, Linux's /etc/machine-id), hashed so the id never leaves the computer, or from --machine / G1T_RUNNER_MACHINE; a container sends none, since its machine id is its image's, and g1t-runner sandbox passes the computer's to the runner in the VM. The actions service keeps it hashed again with the workspace (migration 0022 adds runners.machine_hash), and a registration of a name already taken replaces that runner when it was registered from the same computer, or, for a runner from before fingerprints, when the same person made both tokens and the old runner has been offline for 90 seconds; a runner of the same name from another computer is still refused without --replace, and the refusal says so. The self-hosted runners guide covers registering again and --machine. | 8 | //! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace`, `isolation`, `machine` | |
| Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships | 9 | //! | `POST /runners/{id}/poll` | `version`, `running`, `wait_ms` | |
| 10 | //! | `POST /runners/{id}/finished` | `id`, `exit_code`, `reason` | | |
| 11 | //! | `POST /runners/{id}/remove` | nothing | | |
| 12 | //! | |
| 13 | //! The last three send `Authorization: Bearer g1tr_…`. See | |
| 14 | //! `g1t_contracts::runners` for what each answers. | |
| 15 | ||
| 16 | use g1t_contracts::Outcome; | |
| 17 | use g1t_kit::wire; | |
| 18 | use serde_json::{Value, json}; | |
| 19 | use worker::{Request, Response, Result}; | |
| 20 | ||
| 21 | use crate::operations::Services; | |
| 22 | use crate::{fail, failure, json_body}; | |
| 23 | ||
| 24 | /// What is passed through as given: an agent task's environment. | |
| 25 | const AS_GIVEN: &[&str] = &["env"]; | |
| 26 | ||
| 27 | fn credential(request: &Request) -> Result<String> { | |
| 28 | let header = request.headers().get("authorization")?.unwrap_or_default(); | |
| 29 | Ok(match header.split_once(' ') { | |
| 30 | Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") => token.trim().to_owned(), | |
| 31 | _ => String::new(), | |
| 32 | }) | |
| 33 | } | |
| 34 | ||
| 35 | async fn answer(services: &Services, method: &str, args: &Value) -> Result<Response> { | |
| 36 | let answered: Outcome<Value> = g1t_kit::call(&services.actions, method, args).await?; | |
| 37 | match answered { | |
| 38 | Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, AS_GIVEN)), | |
| 39 | Outcome::Fail(refused) => failure(&refused), | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | /// Serves `path` if it is one of the runner's routes. | |
| 44 | pub async fn handle(request: &mut Request, services: &Services, path: &str) -> Result<Option<Response>> { | |
| 45 | let Some(rest) = path.strip_prefix("/runners/") else { return Ok(None) }; | |
| 46 | let body = json_body(request).await; | |
| 47 | let body = if body.is_object() { body } else { json!({}) }; | |
| 48 | if rest == "register" { | |
| 49 | let args = json!({ | |
| 50 | "token": body["token"].as_str().unwrap_or_default(), | |
| 51 | "name": body["name"].as_str().unwrap_or_default(), | |
| 52 | "labels": body["labels"].as_array().cloned().unwrap_or_default(), | |
| 53 | "os": body["os"].as_str().unwrap_or_default(), | |
| 54 | "arch": body["arch"].as_str().unwrap_or_default(), | |
| 55 | "version": body["version"].as_str().unwrap_or_default(), | |
| 56 | "ephemeral": body["ephemeral"].as_bool().unwrap_or(false), | |
| 57 | "group": body["group"].as_str(), | |
| 58 | "replace": body["replace"].as_bool().unwrap_or(false), | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 59 | "isolation": body["isolation"].as_str(), |
| A computer whose runner is registered again takes its own runner's name back instead of being refused. When the desktop app's sandbox was made again (a new VM, its registration gone), its runners registered under the names the old sandbox's still held on g1t, were refused with a 409 asking for --replace, and tried again forever. A runner now says which computer it is when it registers: a fingerprint g1t-runner works out from the operating system's machine id (Windows' MachineGuid, macOS's platform UUID, Linux's /etc/machine-id), hashed so the id never leaves the computer, or from --machine / G1T_RUNNER_MACHINE; a container sends none, since its machine id is its image's, and g1t-runner sandbox passes the computer's to the runner in the VM. The actions service keeps it hashed again with the workspace (migration 0022 adds runners.machine_hash), and a registration of a name already taken replaces that runner when it was registered from the same computer, or, for a runner from before fingerprints, when the same person made both tokens and the old runner has been offline for 90 seconds; a runner of the same name from another computer is still refused without --replace, and the refusal says so. The self-hosted runners guide covers registering again and --machine. | 60 | "machine": body["machine"].as_str(), |
| Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships | 61 | }); |
| 62 | return Ok(Some(answer(services, "runner_register", &args).await?)); | |
| 63 | } | |
| 64 | let Some((runner, action)) = rest.split_once('/') else { | |
| 65 | return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)); | |
| 66 | }; | |
| 67 | let auth = json!({ "runner": runner, "credential": credential(request)? }); | |
| 68 | let mut args = auth; | |
| 69 | let method = match action { | |
| 70 | "poll" => { | |
| 71 | args["version"] = json!(body["version"].as_str().unwrap_or_default()); | |
| 72 | args["running"] = json!(body["running"].as_array().cloned().unwrap_or_default()); | |
| 73 | args["wait_ms"] = json!(body["wait_ms"].as_u64().unwrap_or(0)); | |
| 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. | 74 | args["isolation"] = json!(body["isolation"].as_str()); |
| Artifacts: an hourly view of operations and errors, and SSH must meter its git before it ships | 75 | "runner_poll" |
| 76 | } | |
| 77 | "finished" => { | |
| 78 | args["id"] = json!(body["id"].as_str().unwrap_or_default()); | |
| 79 | args["exit_code"] = json!(body["exit_code"].as_i64().unwrap_or(1)); | |
| 80 | args["reason"] = body["reason"].clone(); | |
| 81 | "runner_finished" | |
| 82 | } | |
| 83 | "remove" => "runner_remove_self", | |
| 84 | _ => return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)), | |
| 85 | }; | |
| 86 | Ok(Some(answer(services, method, &args).await?)) | |
| 87 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.