Skip to content
85 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.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1//! 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 incidents9//! | `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
16use g1t_contracts::Outcome;
17use g1t_kit::wire;
18use serde_json::{Value, json};
19use worker::{Request, Response, Result};
20
21use crate::operations::Services;
22use crate::{fail, failure, json_body};
23
24/// What is passed through as given: an agent task's environment.
25const AS_GIVEN: &[&str] = &["env"];
26
27fn 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
35async 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.
44pub 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 incidents60 });
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}