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
87 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 plan says what g1t is for without measuring it against other products1//! 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` |
The plan says what g1t is for without measuring it against other products9//! | `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),
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r959 "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(),
The plan says what g1t is for without measuring it against other products61 });
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());
The plan says what g1t is for without measuring it against other products75 "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.