Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | //! 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 | //! | --- | --- | | |
| The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so. | 8 | //! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace`, `isolation` | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 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), | |
| The self-hosted runner runs as a container under Sysbox, with a Docker Engine of its own inside, and each job runs in a fresh container on that engine, so neither the runner nor its jobs reach the machine: `docker run --runtime=sysbox-runc -v g1t-runner:/data -e G1T_RUNNER_URL=… -e G1T_RUNNER_TOKEN=… g1t.sh/flagon-io/g1t-runner`, with docker-init as PID 1 supervising g1t-runner and g1t-runner supervising dockerd. The image refuses to start when it can see the machine's Docker socket mounted in, an engine socket it did not make, DOCKER_HOST, or a root that is the machine's (no Sysbox, or --privileged), unless G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1 is set, and its log says which isolation it has: nested-container, container or host, which it now also tells g1t when it registers (kept on the runner, shown nowhere yet). Every job container drops all capabilities but eight it needs, has no-new-privileges (agent work keeps sudo for its guardrails), a process, memory and CPU limit (--pids-limit, --memory, --cpus, with defaults), a network and a /home/runner volume of its own removed after it, and no mount but the read-only harness; a job that uses Docker gets an engine of its own beside it, started the first time it is used and removed after, unless the runner is registered with --no-job-docker. --no-docker keeps working but is deprecated: the runner says so when it starts, every job's log starts with a warning that it ran on the host unsandboxed, and it refuses a public repository's work now that g1t says a job's repository visibility. Windows and macOS sandboxes (Hyper-V-isolated containers, ephemeral macOS VMs) are designed and not built. The runner is 0.2.0; the self-hosted runners guide, the limitations page and the New runner command say so. | 59 | "isolation": body["isolation"].as_str(), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 60 | }); |
| 61 | return Ok(Some(answer(services, "runner_register", &args).await?)); | |
| 62 | } | |
| 63 | let Some((runner, action)) = rest.split_once('/') else { | |
| 64 | return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)); | |
| 65 | }; | |
| 66 | let auth = json!({ "runner": runner, "credential": credential(request)? }); | |
| 67 | let mut args = auth; | |
| 68 | let method = match action { | |
| 69 | "poll" => { | |
| 70 | args["version"] = json!(body["version"].as_str().unwrap_or_default()); | |
| 71 | args["running"] = json!(body["running"].as_array().cloned().unwrap_or_default()); | |
| 72 | args["wait_ms"] = json!(body["wait_ms"].as_u64().unwrap_or(0)); | |
| 73 | "runner_poll" | |
| 74 | } | |
| 75 | "finished" => { | |
| 76 | args["id"] = json!(body["id"].as_str().unwrap_or_default()); | |
| 77 | args["exit_code"] = json!(body["exit_code"].as_i64().unwrap_or(1)); | |
| 78 | args["reason"] = body["reason"].clone(); | |
| 79 | "runner_finished" | |
| 80 | } | |
| 81 | "remove" => "runner_remove_self", | |
| 82 | _ => return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)), | |
| 83 | }; | |
| 84 | Ok(Some(answer(services, method, &args).await?)) | |
| 85 | } |