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
538 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 incidents1import { Bot, Boxes, Cpu, Plus, ServerCog, ShieldAlert, Trash2 } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { useEffect, useState } from "react";
3import { Form, Link } from "react-router";
Fast pages, required checks on the branch, self-hosted runners, honest incidents4
Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop.5import { type RegistrationToken, RUNNER_DOWNLOADS, RUNNER_FILES, RUNNER_IMAGE, type Runner, type RunnerGroup, type RunnerIsolation } from "@g1t/contracts";
Fast pages, required checks on the branch, self-hosted runners, honest incidents6
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.7import { agentRunHref, workflowRunHref } from "../lib/runners";
Fast pages, required checks on the branch, self-hosted runners, honest incidents8import type { RunnersAction, RunnersData } from "../lib/runners.server";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.9import { CopyLine, EmptyState, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop.10import { Badge, type BadgeTone } from "./ui/badge";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.11import { Button } from "./ui/button";
12import { Card } from "./ui/card";
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.13import { Checkbox } from "./ui/checkbox";
14import { RadioGroup, RadioOption } from "./ui/radio-group";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project15import { SelectField } from "./ui/select";
Fast pages, required checks on the branch, self-hosted runners, honest incidents16import { Switch } from "./ui/switch";
17import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop.18import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Fast pages, required checks on the branch, self-hosted runners, honest incidents19
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.20/** Online and waiting for work reads as Idle; online and working, Busy. */
21export const STATUS: Record<Runner["status"], { dot: string; text: string; label: string }> = {
22 online: { dot: "bg-success", text: "text-success", label: "Idle" },
23 busy: { dot: "bg-info", text: "text-info", label: "Busy" },
24 offline: { dot: "bg-faint", text: "text-muted", label: "Offline" },
Fast pages, required checks on the branch, self-hosted runners, honest incidents25};
26
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.27/** Where a runner's current work is: its workflow run, or the agent run it is. */
28function workHref(work: NonNullable<Runner["work"]>): string | null {
29 if (!work.runId || !work.repo) return null;
30 return work.kind === "workflow" ? workflowRunHref(work.repo, work.runId) : agentRunHref(work.repo, work.runId);
31}
32
33export const OS_NAMES: Record<Runner["os"], string> = { linux: "Linux", macos: "macOS", windows: "Windows" };
Fast pages, required checks on the branch, self-hosted runners, honest incidents34
Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop.35/** How a runner keeps work from its machine, as a badge and what it means. */
36export const ISOLATION: Record<RunnerIsolation, { label: string; tone: BadgeTone; about: string }> = {
37 "nested-container": {
38 label: "Sandboxed",
39 tone: "success",
40 about: "Each job runs in a fresh container inside the runner's own sandboxed container. Neither reaches the machine.",
41 },
42 vm: {
43 label: "VM per job",
44 tone: "success",
45 about: "Each job runs in a throwaway virtual machine of its own, such as Windows Sandbox, with no folder of the machine's.",
46 },
47 container: {
48 label: "Shared Docker",
49 tone: "warn",
50 about: "Each job runs in a container, but the runner shares the machine's Docker Engine, so anything that takes it over controls the machine.",
51 },
52 host: {
53 label: "Unsandboxed",
54 tone: "danger",
55 about: "Jobs run directly on the machine (host mode). It is not given public repositories' work.",
56 },
57};
58
59function IsolationBadge({ isolation }: { isolation: Runner["isolation"] }) {
60 if (!isolation) return null;
61 const level = ISOLATION[isolation];
62 if (!level) return null;
63 return (
64 <Tooltip>
65 <TooltipTrigger asChild>
66 <Badge size="md" tone={level.tone} tabIndex={0}>
67 {level.label}
68 </Badge>
69 </TooltipTrigger>
70 <TooltipContent className="max-w-72">{level.about}</TooltipContent>
71 </Tooltip>
72 );
73}
74
Fast pages, required checks on the branch, self-hosted runners, honest incidents75function Section({ icon, title, about, children }: { icon: React.ReactNode; title: string; about: React.ReactNode; children: React.ReactNode }) {
76 return (
77 <section className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]">
78 <div>
79 <h2 className="flex items-center gap-2 font-medium">
80 {icon}
81 {title}
82 </h2>
83 <div className="mt-1 text-sm text-muted">{about}</div>
84 </div>
85 <div className="min-w-0">{children}</div>
86 </section>
87 );
88}
89
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.90export function RunnerRow({ runner, manage }: { runner: Runner; manage: boolean }) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents91 const status = STATUS[runner.status];
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.92 const href = runner.work ? workHref(runner.work) : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents93 return (
94 <li className="flex flex-wrap items-start gap-x-4 gap-y-2 px-4 py-3">
95 <span className={`mt-1.5 size-2 shrink-0 rounded-full ${status.dot}`} aria-hidden />
96 <div className="min-w-0 grow basis-60">
97 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
98 <span className="font-medium">{runner.name}</span>
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.99 <span className={`text-xs font-medium ${status.text}`}>{status.label}</span>
Self-hosted runners on a person's own computer are sandboxes that nothing a job runs can get out of. `g1t-runner sandbox up|down|status|update [--json]` gives a Windows, macOS or Linux computer up to two capabilities, each a runner of its own: Linux jobs in a Linux VM the runner owns, where the runner's image runs under Sysbox and each job is a fresh container inside it (nested-container, three walls from the computer); and jobs native to Windows, each in a Windows Sandbox of its own, discarded after, with one read-only folder holding the harness (a new `vm` isolation level, also `g1t-runner register --native`). On Windows the VM is a WSL 2 distribution imported from the release's signed root filesystem: its wsl.conf turns off drives, Windows programs and Windows' PATH, and the supervisor inside unmounts what WSL shares with every distribution anyway (/mnt/wsl, where Docker Desktop keeps its sockets to Windows, WSLg, the GPU driver folders), refuses to run while anything of the computer's is reachable, and drops containers' traffic to the computer's address but for DNS. It needs no administrator once WSL is installed, and works on Windows 11 Home. On macOS the VM runs under vfkit and on Linux without Sysbox under QEMU with KVM, with no shared folders and nothing forwarded in (built and tested as commands, not yet run on those machines); on Linux with Sysbox no VM is needed. `status --json` reports the state, versions, isolation, the current job and a sentence when something is wrong, and each capability's availability with its cause and fix, for the desktop app. `update` drains the runner, swaps in the newest signed image and starts again without registering again. The sandbox's images are built from deploy/sandbox/Dockerfile by scripts/sandbox-image.mjs and listed with their SHA-256 in the release's signed manifest, beside the runner image's digest. The release workflow now builds its images on g1t's own machines (g1t-4core) rather than waiting forever for a self-hosted runner, registering QEMU for arm64 where the machine allows it and building amd64 alone with a warning where it does not, and signs and publishes once the images are built. The Runners page shows each runner's isolation (Sandboxed, VM per job, Shared Docker, Unsandboxed), which the API now returns as `isolation`. The Self-hosted runners guide has a new "Sandbox on your computer" section, with what it cannot stop.100 <IsolationBadge isolation={runner.isolation} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.101 {runner.ephemeral && <Badge size="md">ephemeral</Badge>}
Fast pages, required checks on the branch, self-hosted runners, honest incidents102 {runner.group && <span className="text-xs text-faint">in {runner.group}</span>}
103 </div>
104 <div className="mt-1.5 flex flex-wrap gap-1">
105 {runner.labels.map((label) => (
106 <span key={label} className="rounded-md bg-raised px-1.5 py-0.5 font-mono text-[0.6875rem] text-fg-soft">
107 {label}
108 </span>
109 ))}
110 </div>
111 {runner.work && (
112 <p className="mt-1.5 truncate text-xs text-muted">
113 {runner.work.kind === "workflow" ? "Running " : "Agent work: "}
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.114 {href ? (
115 <Link to={href} className="text-fg hover:underline">
Fast pages, required checks on the branch, self-hosted runners, honest incidents116 {runner.work.name}
117 </Link>
118 ) : (
119 <span className="text-fg">{runner.work.name}</span>
120 )}
121 {runner.work.repo && <> in {runner.work.repo}</>}
122 </p>
123 )}
124 </div>
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.125 <div className="ml-6 grow text-xs text-faint sm:ml-0 sm:shrink-0 sm:grow-0 sm:text-right">
Fast pages, required checks on the branch, self-hosted runners, honest incidents126 <div>
127 {OS_NAMES[runner.os]} {runner.arch}
128 {runner.version && <> · {runner.version}</>}
129 </div>
130 <div className="mt-0.5">{runner.lastSeenAt ? <>Seen <TimeAgo at={runner.lastSeenAt} /></> : "Never seen"}</div>
131 </div>
132 {manage && (
133 <Form
134 method="post"
135 className="shrink-0"
136 onSubmit={(event) => {
137 if (!confirm(`Remove ${runner.name}? Its credential stops working at once${runner.work ? ", and the job it is running fails" : ""}.`)) event.preventDefault();
138 }}
139 >
140 <input type="hidden" name="intent" value="remove" />
141 <input type="hidden" name="id" value={runner.id} />
142 <input type="hidden" name="name" value={runner.name} />
Merge branch 'worktree-agent-a985c01f43538af01'143 <SubmitButton
144 icon
145 match={{ intent: "remove", id: runner.id }}
Fast pages, required checks on the branch, self-hosted runners, honest incidents146 aria-label={`Remove ${runner.name}`}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.147 variant="ghost"
148 size="inline"
149 className="p-1.5 text-faint hover:text-danger"
Fast pages, required checks on the branch, self-hosted runners, honest incidents150 >
151 <Trash2 size={15} />
Merge branch 'worktree-agent-a985c01f43538af01'152 </SubmitButton>
Fast pages, required checks on the branch, self-hosted runners, honest incidents153 </Form>
154 )}
155 </li>
156 );
157}
158
159type Platform = "linux" | "macos" | "windows" | "docker";
160
161/** The copy-paste steps for one platform. */
162export function installSteps(platform: Platform, token: RegistrationToken | null, arch: "x64" | "arm64" = "x64"): { title: string; lines: string[] }[] {
163 const url = token?.url ?? "https://g1t.sh";
164 const value = token?.token ?? "<registration token>";
165 const register = (exe: string) => `${exe} register --url ${url} --token ${value}`;
166 switch (platform) {
167 case "linux": {
168 const file = arch === "arm64" ? RUNNER_FILES["linux-arm64"] : RUNNER_FILES["linux-x64"];
169 return [
170 { title: "Download", lines: [`curl -fsSLo g1t-runner ${RUNNER_DOWNLOADS}/latest/${file} && chmod +x g1t-runner`] },
171 { title: "Register", lines: [register("./g1t-runner")] },
172 { title: "Run it, or install it as a service", lines: ["./g1t-runner run", "sudo ./g1t-runner service install"] },
173 ];
174 }
175 case "macos": {
176 const file = arch === "x64" ? RUNNER_FILES["macos-x64"] : RUNNER_FILES["macos-arm64"];
177 return [
178 { title: "Download", lines: [`curl -fsSLo g1t-runner ${RUNNER_DOWNLOADS}/latest/${file} && chmod +x g1t-runner`] },
179 { title: "Register", lines: [register("./g1t-runner")] },
180 { title: "Run it, or install it as a launch agent", lines: ["./g1t-runner run", "./g1t-runner service install"] },
181 ];
182 }
183 case "windows":
184 return [
185 {
186 title: "Download (PowerShell)",
187 lines: [`Invoke-WebRequest ${RUNNER_DOWNLOADS}/latest/${RUNNER_FILES["windows-x64"]} -OutFile g1t-runner.exe`],
188 },
189 { title: "Register", lines: [register(".\\g1t-runner.exe")] },
190 { title: "Run it, or install it as a service (as Administrator)", lines: [".\\g1t-runner.exe run", ".\\g1t-runner.exe service install"] },
191 ];
192 case "docker":
193 return [
194 {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9195 title: "Register and run under Sysbox, each job in a container inside the runner's",
Fast pages, required checks on the branch, self-hosted runners, honest incidents196 lines: [
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9197 `docker run -d --name g1t-runner --restart unless-stopped --runtime=sysbox-runc -v g1t-runner:/data ${RUNNER_IMAGE} register-and-run --url ${url} --token ${value}`,
Fast pages, required checks on the branch, self-hosted runners, honest incidents198 ],
199 },
200 ];
201 }
202}
203
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.204export function NewRunner({ token, groups, repoScoped }: { token: RegistrationToken | null; groups: RunnerGroup[]; repoScoped: boolean }) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents205 const [platform, setPlatform] = useState<Platform>("linux");
206 const [arch, setArch] = useState<"x64" | "arm64">("x64");
207 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.208 <Card className="space-y-4 p-4">
Fast pages, required checks on the branch, self-hosted runners, honest incidents209 {!token ? (
210 <Form method="post" className="flex flex-wrap items-end gap-3">
211 <input type="hidden" name="intent" value="token" />
212 {!repoScoped && groups.length > 1 && (
213 <Field label="Group">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project214 <SelectField
215 name="group"
216 defaultValue={groups.find((g) => g.default)?.name ?? groups[0]?.name}
217 className="w-auto min-w-40"
218 options={groups.map((group) => ({ value: group.name, label: group.name }))}
219 />
Fast pages, required checks on the branch, self-hosted runners, honest incidents220 </Field>
221 )}
Merge branch 'worktree-agent-a985c01f43538af01'222 <SubmitButton match={{ intent: "token" }} pending="Making a token…">
Fast pages, required checks on the branch, self-hosted runners, honest incidents223 <Plus size={15} />
224 New runner
Merge branch 'worktree-agent-a985c01f43538af01'225 </SubmitButton>
Fast pages, required checks on the branch, self-hosted runners, honest incidents226 <p className="basis-full text-xs text-faint">Makes a registration token that lasts an hour and registers any number of runners. It is shown once.</p>
227 </Form>
228 ) : (
229 <p className="text-sm">
230 Registration token made. It expires <TimeAgoFuture at={token.expiresAt} />
231 {token.group ? <>; its runners join {token.group}</> : null}. Copy it now: it is not shown again.
232 </p>
233 )}
234 <Tabs value={platform} onValueChange={(value) => setPlatform(value as Platform)}>
235 <div className="flex flex-wrap items-center gap-3">
236 <TabsList aria-label="Where the runner runs">
237 <TabsTrigger value="linux">Linux</TabsTrigger>
238 <TabsTrigger value="macos">macOS</TabsTrigger>
239 <TabsTrigger value="windows">Windows</TabsTrigger>
240 <TabsTrigger value="docker">Docker</TabsTrigger>
241 </TabsList>
242 {(platform === "linux" || platform === "macos") && (
243 <TabsList aria-label="Architecture">
244 <button
245 type="button"
246 onClick={() => setArch("x64")}
247 className={`rounded-md px-2.5 py-1 text-xs font-medium ${arch === "x64" ? "bg-raised text-fg" : "text-muted hover:text-fg"}`}
248 >
249 {platform === "macos" ? "Intel" : "x64"}
250 </button>
251 <button
252 type="button"
253 onClick={() => setArch("arm64")}
254 className={`rounded-md px-2.5 py-1 text-xs font-medium ${arch === "arm64" ? "bg-raised text-fg" : "text-muted hover:text-fg"}`}
255 >
256 {platform === "macos" ? "Apple silicon" : "arm64"}
257 </button>
258 </TabsList>
259 )}
260 </div>
261 </Tabs>
262 <ol className="space-y-3">
263 {installSteps(platform, token, platform === "macos" && arch === "x64" ? "x64" : arch).map((step, at) => (
264 <li key={step.title}>
265 <p className="mb-1.5 text-xs text-muted">
266 {at + 1}. {step.title}
267 </p>
268 <div className="space-y-1.5">
269 {step.lines.map((line) => (
270 <CopyLine key={line} text={line} prompt />
271 ))}
272 </div>
273 </li>
274 ))}
275 </ol>
276 <p className="text-xs text-faint">
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9277 Each job runs in a container of its own; on Linux, run the Docker command under Sysbox so neither the runner nor its jobs reach the machine.{" "}
278 <code className="font-mono">--no-docker</code>, which runs jobs on the machine itself, is deprecated. Add{" "}
Fast pages, required checks on the branch, self-hosted runners, honest incidents279 <code className="font-mono">--labels gpu,cuda</code> for jobs to ask for, or <code className="font-mono">--ephemeral</code> for a runner that takes
280 one job and removes itself.{" "}
281 <a href="https://docs.g1t.sh/guides/self-hosted-runners/" className="underline underline-offset-2">
282 Self-hosted runners
283 </a>
284 </p>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.285 </Card>
Fast pages, required checks on the branch, self-hosted runners, honest incidents286 );
287}
288
289/** "in 59m", for a time still to come. */
290function TimeAgoFuture({ at }: { at: string }) {
291 const minutes = Math.max(0, Math.round((new Date(at).getTime() - Date.now()) / 60_000));
292 return (
293 <time dateTime={at} suppressHydrationWarning>
294 {minutes >= 60 ? "in an hour" : `in ${minutes}m`}
295 </time>
296 );
297}
298
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.299export function Groups({
Merge branch 'worktree-agent-a985c01f43538af01'300 groups,
301 repositories,
302 manage,
303 action,
304}: {
305 groups: RunnerGroup[];
306 repositories: string[];
307 manage: boolean;
308 action: RunnersAction | undefined;
309}) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents310 const [editing, setEditing] = useState<string | null>(null);
Merge branch 'worktree-agent-a985c01f43538af01'311 // A group saved closes its form: the list above now shows it as saved.
312 useEffect(() => {
313 if (action?.notice) setEditing(null);
314 }, [action]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents315 return (
316 <div className="space-y-3">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.317 <Card asChild divided>
318 <ul>
319 {groups.map((group) => (
320 <li key={group.id} className="px-4 py-3">
321 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
322 <span className="font-medium">{group.name}</span>
323 {group.default && <Badge size="md">default</Badge>}
324 <span className="text-xs text-muted">
325 {group.runners} {group.runners === 1 ? "runner" : "runners"} ·{" "}
326 {group.repositories.length === 0 ? "every repository" : group.repositories.join(", ")}
Fast pages, required checks on the branch, self-hosted runners, honest incidents327 </span>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.328 {manage && (
329 <span className="ml-auto flex items-center gap-2">
330 <Button type="button" variant="outline" onClick={() => setEditing(editing === group.id ? null : group.id)}>
331 {editing === group.id ? "Close" : "Edit"}
332 </Button>
333 {!group.default && (
334 <Form method="post" onSubmit={(event) => { if (!confirm(`Delete ${group.name}? Its runners join the default group.`)) event.preventDefault(); }}>
335 <input type="hidden" name="intent" value="delete-group" />
336 <input type="hidden" name="id" value={group.id} />
337 <SubmitButton variant="outline" match={{ intent: "delete-group", id: group.id }} pending="Deleting…">
338 Delete
339 </SubmitButton>
340 </Form>
341 )}
342 </span>
343 )}
344 </div>
345 {editing === group.id && <GroupForm group={group} repositories={repositories} />}
346 </li>
347 ))}
348 </ul>
349 </Card>
Fast pages, required checks on the branch, self-hosted runners, honest incidents350 {manage && (editing === "new" ? <GroupForm group={null} repositories={repositories} /> : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.351 <Button type="button" variant="outline" onClick={() => setEditing("new")}>
Fast pages, required checks on the branch, self-hosted runners, honest incidents352 <Plus size={15} />
353 New group
354 </Button>
355 ))}
356 </div>
357 );
358}
359
360function GroupForm({ group, repositories }: { group: RunnerGroup | null; repositories: string[] }) {
361 const [reach, setReach] = useState(group && group.repositories.length > 0 ? "some" : "all");
362 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.363 <Card asChild tone="bg" radius="lg" className="mt-3 space-y-3 p-3">
364 <Form method="post">
365 <input type="hidden" name="intent" value="group" />
366 {group && <input type="hidden" name="id" value={group.id} />}
367 <Field label="Name">
368 <Input name="name" defaultValue={group?.name ?? ""} placeholder="GPU machines" maxLength={64} required={!group} />
369 </Field>
370 <fieldset className="space-y-2 text-sm">
371 <legend className="mb-1 text-xs text-muted">Repositories that may use its runners</legend>
372 <RadioGroup name="reach" value={reach} onValueChange={setReach}>
373 <RadioOption value="all" label="Every repository in the workspace" />
374 <RadioOption value="some" label="Only these" />
375 </RadioGroup>
376 {reach === "some" && (
377 <div className="grid gap-1 pl-6 sm:grid-cols-2">
378 {repositories.map((name) => (
379 <label key={name} className="flex min-h-8 cursor-pointer items-center gap-2 font-mono text-xs">
380 <Checkbox name="repository" value={name} defaultChecked={group?.repositories.includes(name)} />
381 {name}
382 </label>
383 ))}
384 </div>
385 )}
386 </fieldset>
387 <SubmitButton match={{ intent: "group", id: group?.id }} pending="Saving…">
388 Save group
389 </SubmitButton>
390 </Form>
391 </Card>
Fast pages, required checks on the branch, self-hosted runners, honest incidents392 );
393}
394
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.395export function RunnerSettingsForm({ data, manage, scope }: { data: RunnersData; manage: boolean; scope: "workspace" | "project" }) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 const settings = data.settings;
397 const [agents, setAgents] = useState(settings?.agentsOnSelfHosted ?? false);
398 const [forks, setForks] = useState(settings?.forkPullRequests ?? false);
399 if (!settings) return null;
Merge branch 'worktree-agent-a985c01f43538af01'400 // The intent comes from the button pressed: a hidden one before it would
401 // win the form's first "intent", and Follow the workspace would only save.
Fast pages, required checks on the branch, self-hosted runners, honest incidents402 return (
403 <Form method="post" className="space-y-4">
404 {scope === "project" && settings.inherited && (
405 <p className="text-sm text-muted">These are the workspace&apos;s settings. Saving here gives this project its own.</p>
406 )}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.407 <Card asChild className="flex items-start justify-between gap-4 p-4">
408 <label>
409 <span className="min-w-0">
410 <span className="flex items-center gap-2 text-sm font-medium">
411 <Bot size={15} className="text-muted" />
412 Run g1t's work on self-hosted runners
413 </span>
414 <span className="mt-1 block text-sm text-muted">
415 Agent runs, checks, reviews and the merge queue run on your runners with these labels instead of g1t&apos;s sandboxes. The machine time is
416 free; the model is paid as before, through g1t&apos;s model proxy, or by your own provider if you connected one. Agent work needs Docker, or a
417 Linux runner.
418 </span>
419 <span className="mt-3 block max-w-sm">
420 <Input name="agentLabels" defaultValue={settings.agentLabels.join(", ")} disabled={!manage} aria-label="Labels for agent work" />
421 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents422 </span>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.423 <Switch name="agents" checked={agents} onCheckedChange={setAgents} disabled={!manage} aria-label="Run g1t's work on self-hosted runners" />
424 </label>
425 </Card>
Fast pages, required checks on the branch, self-hosted runners, honest incidents426 <label className={`flex items-start justify-between gap-4 rounded-xl border p-4 ${forks ? "border-danger/50 bg-danger/5" : "border-line bg-surface"}`}>
427 <span className="min-w-0">
428 <span className="flex items-center gap-2 text-sm font-medium">
429 <ShieldAlert size={15} className={forks ? "text-danger" : "text-muted"} />
430 Let pull requests from forks use self-hosted runners
431 </span>
432 <span className="mt-1 block text-sm text-muted">
433 Off by default, and best left off. Anyone who can open a pull request could then run any code on your machines, read what they can
434 reach, and leave something behind for the next job. Their jobs never get secrets either way.
435 </span>
436 </span>
437 <Switch name="forks" checked={forks} onCheckedChange={setForks} disabled={!manage} />
438 </label>
439 <p className="text-xs text-faint">
440 g1t&apos;s network guardrails are not enforced on your own machines: a job or an agent there reaches whatever the machine can.
441 </p>
442 {manage && (
443 <div className="flex flex-wrap gap-3">
Merge branch 'worktree-agent-a985c01f43538af01'444 <SubmitButton name="intent" value="settings" pending="Saving…">
Fast pages, required checks on the branch, self-hosted runners, honest incidents445 Save
Merge branch 'worktree-agent-a985c01f43538af01'446 </SubmitButton>
Fast pages, required checks on the branch, self-hosted runners, honest incidents447 {scope === "project" && !settings.inherited && (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.448 <SubmitButton name="intent" value="inherit" variant="outline" pending="Following…">
Fast pages, required checks on the branch, self-hosted runners, honest incidents449 Follow the workspace
Merge branch 'worktree-agent-a985c01f43538af01'450 </SubmitButton>
Fast pages, required checks on the branch, self-hosted runners, honest incidents451 )}
452 </div>
453 )}
454 </Form>
455 );
456}
457
458/** Settings, Runners: a workspace's, or a project's own. */
459export function RunnersPanel({
460 data,
461 action,
462 scope,
463 manage,
464}: {
465 data: RunnersData;
466 action: RunnersAction | undefined;
467 scope: "workspace" | "project";
468 manage: boolean;
469}) {
470 const online = data.runners.filter((runner) => runner.status !== "offline").length;
471 return (
472 <div className="max-w-5xl space-y-8">
473 <div className="min-h-6">
Merge branch 'worktree-agent-ad7c6d88d93adc817'474 {action?.notice && <p className="text-sm text-success">{action.notice}</p>}
Fast pages, required checks on the branch, self-hosted runners, honest incidents475 <ErrorText>{action?.error ?? data.error ?? null}</ErrorText>
476 </div>
477 <Section
478 icon={<ServerCog size={15} className="text-muted" />}
479 title="Runners"
480 about={
481 <>
482 <p>
483 Your own machines, for jobs with <code className="font-mono text-fg">runs-on: self-hosted</code>. Their time is free on every plan.
484 </p>
485 <p className="mt-2">
486 {data.runners.length === 0 ? "None yet." : `${online} of ${data.runners.length} online.`}
487 </p>
488 </>
489 }
490 >
491 {data.runners.length === 0 ? (
492 <EmptyState title="No self-hosted runners">
493 Add one below. It connects out to g1t; nothing needs to reach the machine.
494 </EmptyState>
495 ) : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.496 <Card asChild divided>
497 <ul>
498 {data.runners.map((runner) => (
499 <RunnerRow key={runner.id} runner={runner} manage={manage && (scope === "workspace" || runner.repo !== null)} />
500 ))}
501 </ul>
502 </Card>
Fast pages, required checks on the branch, self-hosted runners, honest incidents503 )}
504 </Section>
505 {manage && (
506 <Section
507 icon={<Plus size={15} className="text-muted" />}
508 title="New runner"
509 about="Download g1t-runner, register it with a token made here, and start it. Linux, macOS and Windows, x64 and arm64."
510 >
511 <NewRunner token={action?.token ?? null} groups={data.groups} repoScoped={scope === "project"} />
512 </Section>
513 )}
514 {scope === "workspace" && (
515 <Section
516 icon={<Boxes size={15} className="text-muted" />}
517 title="Groups"
518 about="Which repositories may use which runners. A runner joins the default group, every repository, unless its token names another."
519 >
Merge branch 'worktree-agent-a985c01f43538af01'520 <Groups groups={data.groups} repositories={data.repositories} manage={manage} action={action} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents521 </Section>
522 )}
523 <Section
524 icon={<Cpu size={15} className="text-muted" />}
525 title="Where work runs"
526 about={scope === "workspace" ? "For every project in the workspace, unless a project says otherwise." : "For this project."}
527 >
Merge branch 'worktree-agent-a985c01f43538af01'528 {/* Keyed to what is saved, so following the workspace again shows its settings, not the switches as they were. */}
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.529 <RunnerSettingsForm
Merge branch 'worktree-agent-a985c01f43538af01'530 key={data.settings ? `${data.settings.inherited}:${data.settings.agentsOnSelfHosted}:${data.settings.forkPullRequests}:${data.settings.agentLabels.join(",")}` : "none"}
531 data={data}
532 manage={manage}
533 scope={scope}
534 />
Fast pages, required checks on the branch, self-hosted runners, honest incidents535 </Section>
536 </div>
537 );
538}

This file's history is long; its oldest lines are credited to the oldest commit read.