Skip to content
1,398 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.

Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1import { Container, type StopParams } from "@cloudflare/containers";
2import { WorkerEntrypoint } from "cloudflare:workers";
3
4import {
5 type BumpArgs,
6 UPDATE_BRANCH_PREFIX,
7 type RunKind,
8 agentsClient,
9 type G1tEvent,
10 type QueueJob,
11 type RepoPath,
12 type Result,
13 type ServiceBinding,
14 type User,
15 type Viewer,
16 type ComputeEntitlements,
17 type ComputeKind,
18 ComputeGate,
19 actualMicros,
20 eventsClient,
21 refusalMessage,
22 sandboxEstimateMicros,
23 billingClient,
24 fail,
25 ok,
26 reposClient,
27 workClient,
28 type InstanceType,
29 STANDARD_INSTANCE,
30 instanceNamed,
Merge branch 'worktree-agent-a56293a6f836cd343'31 priceScale,
32 runnerMinutes,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged33} from "@g1t/contracts";
Merge branch 'main' into actions-toolkit-oidc-artifacts34
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.35import { BUMP_MINUTES, BUMP_TOKEN_TTL_SECONDS, bumpEnv, bumpProblem, bumpSandboxName, systemActor } from "./bump";
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged36import { BACKUP_MINUTES, backupEnv, backupPace, backupSandboxName } from "./backup";
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.37import { holdCredentials, revokeCredentials, runCredential } from "./credentials";
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged38import { cancelTask, enqueueTask, handedOverStep, selfHostedRoute, taskEnv, taskRepo } from "./self-hosted";
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.39import { answered, capReached, describeError, stoppedShort, tellStopped, withinTimeCap } from "./lifecycle";
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged40import {
41 ABUSE_EXIT_CODE,
42 ABUSE_HOST,
43 ABUSE_MESSAGE,
44 ALARM_GRACE_SECONDS,
45 type PlanLimits,
46 type RunGuard,
47 abuse,
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.48 buildGuard,
Merge branch 'main' into actions-toolkit-oidc-artifacts49 dockerFor,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged50 egress,
51 guardFor,
52 harnessEnv,
53 reportRun,
54 SANDBOX_BINDINGS,
55 sandboxNamespace,
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.56 throughWorker,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged57 timeCapMessage,
58 withPlanLimits,
59} from "./guard";
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.60import { newlyMisdirected } from "./broker";
Merge branch 'main' into rm-agents61import { relay, relayDeps } from "./broker-relay";
62import { forward } from "./forward";
Merge branch 'worktree-agent-a56293a6f836cd343'63import { EGRESS_ENV, jobNamespaceOf } from "./egress";
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged64
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.65// Outbound interception, which brokered secrets and abuse reports use,
66// needs this exported.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged67export { ContainerProxy } from "@cloudflare/containers";
68
69export interface RunnerEnv {
70 SANDBOX: DurableObjectNamespace<AttemptSandbox>;
71 /**
Merge branch 'worktree-agent-a56293a6f836cd343'72 * Workflow jobs that ask with `runs-on` for `g1t-2core` (the standard
73 * machine, kept as a name so the label keeps working) or `g1t-4core` (a
74 * larger instance type): the same image, in classes of their own.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged75 */
76 SANDBOX_2CORE?: DurableObjectNamespace<Sandbox2Core>;
77 SANDBOX_4CORE?: DurableObjectNamespace<Sandbox4Core>;
78 IDENTITY: ServiceBinding;
79 REPOS: ServiceBinding;
80 WORK: ServiceBinding;
81 BILLING: ServiceBinding;
82 /** GitHub Actions jobs: told when a job's sandbox dies without reporting. */
83 ACTIONS: ServiceBinding;
84 /** Told when a deploy sandbox dies without reporting. */
85 DEPLOYMENTS: ServiceBinding;
86 /** The event bus: `abuse.flagged`, for g1t's staff. */
87 EVENTS?: ServiceBinding;
88 CLOUDFLARE_ACCOUNT_ID: string;
89 /**
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.90 * `off` stops passing any sandbox's HTTP and HTTPS through the Worker: a
91 * switch for the operator, should it misbehave. Brokered secrets' stand-
92 * ins then go out as they are, unfilled. Anything else passes the
93 * requests of workflow jobs that may be given brokered secrets.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged94 */
95 EGRESS?: string;
96 /**
97 * `off` stops sandboxes watching themselves for mining (crates/runner
98 * abuse.rs): a switch for the operator, should it stop real work.
99 * Anything else leaves it on. Miners named in commands are refused
100 * either way.
101 */
102 ABUSE_WATCH?: string;
103 /**
Merge branch 'main' into actions-toolkit-oidc-artifacts104 * `off` leaves workflow jobs without a Docker Engine of their own
105 * (crates/runner docker/): a switch for the operator. Anything else
106 * gives each job one, started the first time it is used.
107 */
108 DOCKER?: string;
109 /**
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged110 * Nightly backups (backup.ts): how many queued backups one sweep starts
111 * (`0`: none, backups off here), and how many may run at once.
112 */
113 BACKUPS_PER_SWEEP?: string;
114 BACKUPS_RUNNING?: string;
115}
116
117/**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.118 * What a sandbox is doing. Runs of an issue's commands (`checks`) are from
119 * before checks were workflows; a sandbox that was started for work g1t no
120 * longer does is told apart by its kind and only cleaned up.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged121 */
122type Run =
123 | { kind: "checks"; runId: string; token: string }
124 /** One combined state of a merge queue, being built and checked. */
125 | { kind: "queue"; entryId: string; token: string }
126 /** Whether a pull request merges cleanly: two commits merged, nothing pushed. */
127 | { kind: "mergecheck"; pullId: string; token: string }
128 /** One job of a GitHub Actions workflow. */
129 | { kind: "actions"; jobId: string; token: string }
130 /** A build of one commit, deployed to g1t.page. */
131 | { kind: "deploy"; deployId: string; token: string }
132 /**
133 * A security update: one package raised in its lockfiles and pushed to
134 * its branch. The security service opens the pull request when it hears
135 * the push, so a failure has no one to tell.
136 */
137 | { kind: "bump"; repo: RepoPath; branch: string }
138 /**
139 * A repository's nightly backup: a bundle cut and sent to the repos
140 * service. g1t's own work, never charged to the workspace.
141 */
142 | { kind: "backup"; jobId: string; token: string };
143/**
144 * Whose sandbox time it is, reported when the sandbox stops, and the
145 * machine it ran on when it was not the standard one.
146 */
Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted.147type Meter = {
148 workspace: string;
149 repo: string;
150 description: string;
151 instance?: string | null;
152};
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged153/**
154 * What billing reserved for a sandbox's work (`ComputeGate.admit`), settled
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.155 * when it stops at what it cost: its seconds.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged156 */
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.157type Held = { id: string; workspace: string; microsPerSecond: number };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged158/**
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.159 * A sandbox that is not tracked as a run but still has a time cap: a
160 * workflow job, a deploy build or an update, for `minutes` at most.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged161 */
162type Build = {
163 kind: "actions" | "deploy" | "bump";
164 minutes: number;
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.165 /**
166 * A workflow job that may be given brokered secrets: its HTTP and HTTPS
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.167 * pass the Worker, which fills them in.
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.168 */
169 brokered?: boolean;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged170};
171/** Deploy builds are metered by the Deployments plan, not here. */
172type RunRequest = Run & {
173 envVars: Record<string, string>;
174 meter?: Meter;
175 track?: Track;
176 /** The workspace's plan's caps, applied under its guardrails' (lower of each). */
177 limits?: PlanLimits;
178 reservation?: Held | null;
179 build?: Build;
180 /** Whose sandbox it is, when it has no meter: for `abuse.flagged`. */
181 owner?: { workspace: string; repo: string };
182 /**
183 * The labels of the workspace's self-hosted runners this work goes to
184 * instead of a container (self-hosted.ts). Null or absent: a container.
185 */
186 selfHosted?: string[] | null;
187};
188
189/** What a sandbox is, as billing meters it. */
190function computeKindOf(kind: Run["kind"]): ComputeKind | null {
191 switch (kind) {
192 case "checks":
193 case "mergecheck":
194 // A security update resolves lockfiles, as cheap as a check.
195 case "bump":
196 return "check";
197 case "queue":
198 return "queue";
199 case "actions":
200 return "workflow";
201 case "deploy":
202 return "deploy";
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.203 // Not metered: a backup is g1t's own cost; nor is a sandbox of a kind
204 // g1t no longer starts, which ran before it stopped starting them.
205 default:
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged206 return null;
207 }
208}
209
210/** One gate per isolate, so entitlements and prices are kept between calls. */
211let gate: ComputeGate | null = null;
212function gateFor(env: { BILLING: ServiceBinding }): ComputeGate {
213 gate ??= new ComputeGate(env.BILLING);
214 return gate;
215}
216
217/**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.218 * What to record the sandbox as, so people can watch it: a run of the
219 * merge queue.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged220 */
221type Track = {
222 actor: User;
223 repo: RepoPath;
224 kind: RunKind;
225 number?: number | null;
226 pullId?: string | null;
227 title?: string | null;
228 startedBy?: string | null;
229};
230/** The run a sandbox reports to, kept so it can be closed when it stops. */
231type TrackedRun = { runId: string; token: string };
232
233function meter(repo: RepoPath, description: string): Meter {
234 return { workspace: repo.namespace, repo: `${repo.namespace}/${repo.name}`, description };
235}
236
237/** What the deployments service asks a sandbox to build. */
238type DeployJob = {
239 deployId: string;
240 /** The workspace the project is in, which pays. */
241 workspace?: string;
242 /** What the deployments service reserved for the build, settled when it stops. */
243 reservation?: string | null;
244 /** The price it reserved at, per second. */
245 microsPerSecond?: number | null;
246 /** The plan's longest run, in minutes; the build gets the lower of this and its own. */
247 maxRunMinutes?: number | null;
248 /** Lets the sandbox, and nothing else, report this build. */
249 token: string;
250 /** Whose access reads the commit. */
251 actor: User;
252 /** The repository the commit is in: the pull request's fork, or the repository. */
253 source: RepoPath;
254 /**
255 * The project's repository, whose guardrails the build runs under, and
256 * its id. A preview's `source` is its pull request's working copy, so
257 * the two differ. Older callers send only `source`.
258 */
259 repo?: RepoPath | null;
260 repoId?: string | null;
261 commit: string;
262 /** Where in the repository the project lives; empty for all of it. */
263 rootDir?: string;
264 buildCommand?: string | null;
265 outputDir?: string | null;
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.266 /** What it is built as (`auto`, `static`, `workers` or `docker`), and its Dockerfile's path when set. */
267 buildAs?: string | null;
268 dockerfile?: string | null;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged269 /** The repository's variables for deploy builds. */
270 buildEnv?: Record<string, string>;
271 /** Its secrets for deploy builds: set like variables, and redacted from the log. */
272 buildSecrets?: Record<string, string>;
273};
274
275/** Long enough to install and build; then the read token stops working. */
276const DEPLOY_TOKEN_TTL_SECONDS = 30 * 60;
277
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.278/** Long enough to clone, build and test a merge queue's state; then the token stops working. */
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged279const CHECKS_TOKEN_TTL_SECONDS = 45 * 60;
280
281/** Long enough to clone and merge two commits; then the read token stops working. */
282const MERGECHECK_TOKEN_TTL_SECONDS = 10 * 60;
283
284/**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.285 * One sandbox, for one piece of work: a workflow job, a build, a state of
286 * the merge queue, a merge check, an update or a backup. The image's entrypoint
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged287 * is the g1t runner, which does the work and exits; this class only starts
288 * it and cleans up if it dies without reporting.
289 */
290export class AttemptSandbox extends Container<RunnerEnv> {
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.291 // Past the longest default time cap (90 minutes) and its alarm. A run whose guardrails allow longer (up to 240 minutes) is kept
Merge remote-tracking branch 'origin/main' into workspace-chat292 // past it by `onActivityExpired`, so only its own cap ends it. A finished
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged293 // run's process exits and stops the sandbox well before this.
294 sleepAfter = "100m";
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.295 // HTTPS to an intercepted host goes through its handler too (guard.ts).
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged296 interceptHttps = true;
297 static {
298 // Assigned, not declared: a class field would hide the setter that
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.299 // registers the handler with the containers library. `ABUSE_HOST`,
300 // named up front, is the only host intercepted on its own, so every
301 // other request goes straight out.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged302 AttemptSandbox.outboundHandlers = { egress, abuse };
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.303 AttemptSandbox.outboundByHost = { [ABUSE_HOST]: abuse };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged304 }
305
306 async run(request: RunRequest): Promise<void> {
307 const { envVars, meter, track, limits, reservation, build, owner, selfHosted, ...run } = request;
308 // What billing reserved is settled however this ends, once.
309 if (reservation) await this.ctx.storage.put("reservation", reservation);
310 let guard: RunGuard | null;
311 try {
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.312 // A tracked run gets its project's guardrails; no sandbox for one
313 // starts without them. Workflow jobs, deploy builds and updates get
314 // the time cap they were given. The plan's caps apply under them:
315 // the lower of each.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged316 guard = track
317 ? withPlanLimits(await guardFor(this.env.WORK, track.repo, track.kind), limits)
318 : build
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.319 ? withPlanLimits(buildGuard(build.minutes), limits)
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged320 : null;
321 } catch (error) {
Merge remote-tracking branch 'origin/main' into workspace-chat322 // Thrown to the caller, which says why the work did not start; logged
323 // here too, so a sandbox that never started is traceable on its own.
324 console.error("sandbox not started", run.kind, describeError(error));
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged325 await this.settle(0);
326 throw error;
327 }
328 await this.ctx.storage.put("run", run);
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.329 // What an earlier attempt in this object left: its reason, its cap
330 // and its cap's alarm, which would otherwise stop this attempt when
331 // the earlier one's time ran out (re-runs use the same object).
332 await this.ctx.storage.delete(["abuse", "stopReason", "remote", "misdirected", "timeCap"]);
333 this.deleteSchedules("timeUp");
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged334 if (meter) await this.ctx.storage.put("meter", { ...meter, started: Date.now() });
335 await this.ctx.storage.put("started", Date.now());
336 const who = meter ? { workspace: meter.workspace, repo: meter.repo } : owner;
337 if (who) await this.ctx.storage.put("owner", { ...who, kind: track?.kind ?? run.kind });
338 const tracked = track ? await this.openRun(track, envVars, guard) : null;
339 // Its credentials are tied to the run, and revoked when it stops.
340 await holdCredentials(this.env.IDENTITY, this.ctx.storage, envVars, tracked?.runId ?? null);
341 try {
342 const vars = tracked ? { ...envVars, AGENT_RUN: tracked.runId, AGENT_RUN_TOKEN: tracked.token } : envVars;
343 // The workspace's own runner, not a container: the same environment,
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.344 // handed over as a task.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged345 const repo = selfHosted?.length ? taskRepo(track, meter, owner) : null;
346 if (selfHosted?.length && repo) {
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.347 const harness = guard ? harnessEnv(guard, vars) : {};
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged348 const minutes = guard?.minutes ?? limits?.minutes ?? 60;
349 await enqueueTask(this.env.ACTIONS, {
350 sandbox: this.ctx.id.toString(),
351 repo,
352 kind: track?.kind ?? run.kind,
353 title: track?.title ?? meter?.description ?? `${run.kind} in ${repo.namespace}/${repo.name}`,
354 labels: selfHosted,
355 env: taskEnv({ ...vars, ...harness }),
356 timeoutMinutes: minutes,
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.357 runId: tracked?.runId ?? null,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged358 });
359 await this.ctx.storage.put("remote", true);
360 if (tracked) await reportRun(this.env.WORK, tracked, { steps: [handedOverStep(selfHosted)] });
361 if (guard) {
362 await this.ctx.storage.put("timeCap", guard.minutes);
363 await this.schedule(guard.minutes * 60 + ALARM_GRACE_SECONDS, "timeUp");
364 }
365 return;
366 }
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.367 // Every sandbox reaches the internet. Its requests go straight out,
368 // and only `ABUSE_HOST` is intercepted (`outboundByHost`), unless
369 // it is a workflow job that may be given brokered secrets: then its
370 // HTTP and HTTPS pass `egress`, which fills them in from the job's
371 // stand-ins (asked for with the job's token) and sends every
372 // request on, to any host. Other protocols go straight out.
373 // Cloudflare Containers intercept either the hosts a class names
374 // up front or, once a handler is chosen at run time, everything, so
375 // the hosts of one job's secrets cannot be singled out.
376 const brokering = run.kind === "actions" && throughWorker(build, this.env.EGRESS);
377 this.enableInternet = true;
378 if (brokering && run.kind === "actions") {
379 await this.setOutboundHandler("egress", { job: { id: run.jobId, token: run.token } });
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged380 }
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.381 const harness = guard && !build ? harnessEnv(guard, vars) : {};
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.382 // Its HTTPS is re-signed as it passes the Worker, so it trusts that
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.383 // certificate.
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.384 const trust = brokering ? EGRESS_ENV : {};
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged385 const watch: Record<string, string> = this.env.ABUSE_WATCH === "off" ? { G1T_ABUSE: "off" } : {};
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.386 await this.start({ envVars: { ...vars, ...harness, ...trust, ...watch }, enableInternet: true });
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged387 // A backup has no guardrails, but still a time cap.
388 const cap = guard?.minutes ?? (run.kind === "backup" ? BACKUP_MINUTES : null);
389 if (cap) {
390 await this.ctx.storage.put("timeCap", cap);
391 await this.schedule(cap * 60 + ALARM_GRACE_SECONDS, "timeUp");
392 }
393 } catch (error) {
Merge remote-tracking branch 'origin/main' into workspace-chat394 console.error("sandbox not started", run.kind, describeError(error));
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.395 await revokeCredentials(this.env.IDENTITY, this.ctx.storage);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged396 if (tracked) await this.closeRun("failed", `The sandbox could not start: ${String(error)}`);
397 await this.settle(0);
398 throw error;
399 }
400 }
401
402 /** Settles what billing reserved for this sandbox at `micros`, once. */
403 private async settle(micros: number): Promise<void> {
404 const held = await this.ctx.storage.get<Held>("reservation");
405 if (!held) return;
406 await this.ctx.storage.delete("reservation");
407 await gateFor(this.env).settle(held.id, micros);
408 }
409
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.410 /** Settles the reservation at what the sandbox cost: its seconds at the price billing reserved at. */
411 private async settleStopped(started: number | undefined): Promise<void> {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged412 const held = await this.ctx.storage.get<Held>("reservation");
413 if (!held) return;
414 const seconds = started ? Math.max(1, Math.ceil((Date.now() - started) / 1000)) : 0;
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.415 await this.settle(actualMicros(seconds, held.microsPerSecond));
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged416 }
417
418 /**
419 * The sandbox stopped itself because it looked like it was mining
420 * (crates/runner abuse.rs), or exited saying so. Stops the run with
421 * `ABUSE_MESSAGE`, tells g1t's staff with `abuse.flagged`, and destroys
422 * the sandbox. Once.
423 */
424 async flagAbuse(verdict: unknown): Promise<void> {
425 if (await this.ctx.storage.get<boolean>("abuse")) return;
426 await this.ctx.storage.put("abuse", true);
427 const tracked = await this.ctx.storage.get<TrackedRun>("agentRun");
428 if (tracked) await reportRun(this.env.WORK, tracked, { halt: "abuse", error: ABUSE_MESSAGE });
429 const owner = await this.ctx.storage.get<{ workspace: string; repo: string | null; kind: string }>("owner");
430 console.log("abuse flagged", owner?.workspace, owner?.repo, owner?.kind, JSON.stringify(verdict));
431 if (this.env.EVENTS && owner) {
432 await eventsClient(this.env.EVENTS)
433 .publish([
434 {
435 type: "abuse.flagged",
436 source: "runner",
437 // Never on a repository's timeline or its webhooks.
438 repoId: null,
439 actor: null,
440 data: {
441 workspace: owner.workspace,
442 repo: owner.repo ?? null,
443 run: tracked?.runId ?? null,
444 kind: owner.kind,
445 sandbox: this.ctx.id.toString(),
446 metrics: verdict && typeof verdict === "object" ? (verdict as Record<string, unknown>) : null,
447 },
448 },
449 ])
450 .catch((error: unknown) => console.log("abuse.flagged not published", String(error)));
451 }
452 await this.destroy().catch((error: unknown) => console.log("sandbox not destroyed for abuse", String(error)));
453 }
454
455 /**
456 * Records the run, which the sandbox then reports its steps to. Never
457 * stops the sandbox from starting: without a record it just goes unseen.
458 */
459 private async openRun(track: Track, envVars: Record<string, string>, guard: RunGuard | null): Promise<TrackedRun | null> {
460 const opened = await agentsClient(this.env.WORK)
461 .openRun({
462 ...track,
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.463 model: null,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged464 sandbox: this.ctx.id.toString(),
465 budgetUsd: guard?.policy.budgetUsd ?? null,
466 timeCapMinutes: guard?.minutes ?? null,
467 })
468 .catch((error: unknown) => ({ ok: false as const, error: { message: String(error) } }));
469 if (!opened.ok) {
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.470 console.log("run not recorded", track.kind, opened.error.message);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged471 return null;
472 }
473 await this.ctx.storage.put("agentRun", opened.value);
474 return opened.value;
475 }
476
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.477 /**
478 * A workflow job sent a brokered secret's stand-in to a host it is not
479 * for: said once per secret and host, in the job's log.
480 */
481 async noteMisdirected(name: string, host: string): Promise<void> {
482 const run = await this.ctx.storage.get<Run>("run");
483 if (run?.kind !== "actions") return;
484 const noted = newlyMisdirected((await this.ctx.storage.get<string[]>("misdirected")) ?? [], name, host);
485 if (!noted) return;
486 await this.ctx.storage.put("misdirected", noted.seen);
487 try {
488 const response = await this.env.ACTIONS.fetch("https://actions/rpc/job_report", {
489 method: "POST",
490 headers: { "content-type": "application/json" },
491 body: JSON.stringify({ job: run.jobId, token: run.token, report: { kind: "note", text: noted.note } }),
492 });
493 await answered("job_report", response);
494 } catch (error) {
495 console.log("misdirected secret not noted", name, String(error));
496 }
497 }
498
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged499 /** The run's time cap has passed: stop it, as stopped for time. */
500 async timeUp(): Promise<void> {
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.501 // It already stopped (nothing to stop), or the run in it now is a later
502 // attempt whose cap has not passed: this alarm is an earlier attempt's,
503 // left by a version of g1t that did not clear it, and the run now has
504 // its own.
505 const started = await this.ctx.storage.get<number>("started");
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged506 const minutes = (await this.ctx.storage.get<number>("timeCap")) ?? 0;
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.507 if (!capReached(started, minutes, Date.now())) {
508 if (started) console.log("an earlier attempt's time cap passed; the run now keeps going", this.ctx.id.toString());
509 return;
510 }
511 const tracked = await this.ctx.storage.get<TrackedRun>("agentRun");
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged512 // A workflow job or a build has no run to halt: it fails saying why.
513 await this.ctx.storage.put("stopReason", timeCapMessage(minutes));
514 if (tracked) await reportRun(this.env.WORK, tracked, { halt: "time", error: timeCapMessage(minutes) });
515 if (await this.ctx.storage.get<boolean>("remote")) {
516 await cancelTask(this.env.ACTIONS, this.ctx.id.toString(), timeCapMessage(minutes));
517 await this.remoteEnded(1, null);
518 return;
519 }
520 await this.destroy().catch((error: unknown) => console.log("sandbox not destroyed at its time cap", String(error)));
521 }
522
523 /**
524 * Stops this sandbox's work: its container, or the task a self-hosted
525 * runner holds, which it hears about on its next poll.
526 */
527 async halt(reason: string | null): Promise<void> {
528 if (await this.ctx.storage.get<boolean>("remote")) {
529 await cancelTask(this.env.ACTIONS, this.ctx.id.toString(), reason);
530 await this.remoteEnded(1, reason);
531 return;
532 }
Merge remote-tracking branch 'origin/main' into workspace-chat533 // A container already gone has nothing to stop; one that will not stop
534 // is logged, and its time cap still ends it.
535 await this.destroy().catch((error: unknown) => console.error("sandbox not destroyed", reason, describeError(error)));
536 }
537
538 /**
539 * The library's `sleepAfter` has passed. The runner never fetches its
540 * container, so to the library every sandbox looks idle: a run inside its
541 * time cap keeps going, and the cap's own alarm (`timeUp`) ends it. Only
542 * a sandbox with no cap is stopped for inactivity.
543 */
544 override async onActivityExpired(): Promise<void> {
545 const started = await this.ctx.storage.get<number>("started");
546 const cap = await this.ctx.storage.get<number>("timeCap");
547 if (withinTimeCap(started, cap, Date.now(), ALARM_GRACE_SECONDS)) return;
548 await super.onActivityExpired();
549 }
550
551 /**
552 * A container that crashed or could not be reached, as the library tells
553 * it. Logged at error level with the sandbox, never thrown: the library
554 * ignores what this throws, and the stop that follows is handled by
555 * `onStop` or by `run`, which says why the work did not start.
556 */
557 override onError(error: unknown): void {
558 console.error("sandbox container error", this.ctx.id.toString(), describeError(error));
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged559 }
560
561 /**
Merge remote-tracking branch 'origin/main' into workspace-chat562 * The library's alarm: scheduled callbacks, the container's keep-alive,
563 * and `onStop` once it has stopped. A failure is logged with the retry it
564 * was, then thrown so Cloudflare tries the alarm again.
565 */
566 override async alarm(alarmProps?: AlarmInvocationInfo): Promise<void> {
567 try {
568 await super.alarm(alarmProps);
569 } catch (error) {
570 console.error("sandbox alarm failed", this.ctx.id.toString(), `retry ${alarmProps?.retryCount ?? 0}`, describeError(error));
571 throw error;
572 }
573 }
574
575 /**
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged576 * A self-hosted runner's task ended (the actions service says so, or g1t
577 * stopped it): everything a container's stop does, once.
578 */
579 async remoteEnded(exitCode: number, reason: string | null): Promise<void> {
580 if (!(await this.ctx.storage.get<boolean>("remote"))) return;
581 await this.ctx.storage.delete("remote");
582 await this.ctx.storage.put("selfHostedEnded", true);
583 if (exitCode !== 0 && reason && !(await this.ctx.storage.get<string>("stopReason"))) {
584 await this.ctx.storage.put("stopReason", reason);
585 }
586 await this.onStop({ exitCode, reason: "exit" } as StopParams);
587 await this.ctx.storage.delete("selfHostedEnded");
588 }
589
590 /**
591 * Ends the run's record, once. Returns the status it ended with:
592 * `stopped` when a person stopped it first.
593 */
594 private async closeRun(outcome: "succeeded" | "failed", error?: string): Promise<string | null> {
595 const tracked = await this.ctx.storage.get<TrackedRun>("agentRun");
596 if (!tracked) return null;
597 await this.ctx.storage.delete("agentRun");
598 const closed = await agentsClient(this.env.WORK)
599 .closeRun(tracked.runId, tracked.token, outcome, error)
600 .catch(() => null);
601 return closed?.ok ? closed.value : null;
602 }
603
604 /** Reports how long the sandbox ran, once, whatever it exited with. */
605 private async meterStop(): Promise<void> {
606 const metered = await this.ctx.storage.get<Meter & { started: number }>("meter");
607 if (!metered) return;
608 await this.ctx.storage.delete("meter");
609 const seconds = Math.max(1, Math.ceil((Date.now() - metered.started) / 1000));
610 const run = await this.ctx.storage.get<Run>("run");
611 // On the workspace's own runner: its minutes, at $0.
612 const selfHosted = (await this.ctx.storage.get<boolean>("selfHostedEnded")) ?? false;
613 const recorded = await billingClient(this.env.BILLING)
614 .recordSandbox({
615 workspace: metered.workspace,
616 seconds,
617 description: selfHosted ? `${metered.description} on a self-hosted runner` : metered.description,
618 repo: metered.repo,
619 reference: `sandbox/${this.ctx.id.toString()}/${metered.started}`,
620 // Whether g1t's open-source pool may pay for it.
621 kind: run ? computeKindOf(run.kind) : null,
622 selfHosted,
623 instance: metered.instance ?? null,
624 })
625 .catch((error: unknown) => ({ ok: false as const, error: { message: String(error) } }));
626 if (!recorded.ok) console.log("sandbox time not recorded", metered.workspace, seconds, recorded.error.message);
627 }
628
629 override async onStop({ exitCode, reason }: StopParams): Promise<void> {
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.630 await revokeCredentials(this.env.IDENTITY, this.ctx.storage);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged631 const started = await this.ctx.storage.get<number>("started");
632 await this.meterStop();
633 // It stopped itself for mining, and could not say so before it went.
634 if (exitCode === ABUSE_EXIT_CODE && !(await this.ctx.storage.get<boolean>("abuse"))) {
635 await this.flagAbuse(null);
636 }
637 const flagged = (await this.ctx.storage.get<boolean>("abuse")) ?? false;
638 // Why it stopped, when g1t stopped it: said in place of a plain failure.
639 const why = flagged ? ABUSE_MESSAGE : ((await this.ctx.storage.get<string>("stopReason")) ?? null);
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.640 // Stopped by g1t, it failed whatever exit code the library passed on.
641 const short = stoppedShort(exitCode, why);
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.642 await this.closeRun(
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.643 short ? "failed" : "succeeded",
644 short ? (why ?? `The sandbox exited with ${exitCode}.`) : undefined,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged645 );
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.646 await this.settleStopped(started);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged647 await this.ctx.storage.delete("started");
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.648 if (!short) return;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged649 const run = await this.ctx.storage.get<Run>("run");
650 console.log("sandbox stopped", run?.kind, "exit", exitCode, reason);
651 if (!run) return;
Merge remote-tracking branch 'origin/main' into workspace-chat652 // Never thrown: this runs in the sandbox's alarm, which a throw would
653 // fail, retry and count as an error, running all of the above again.
654 // What could not be told is logged, and the sweep catches it up.
655 await tellStopped(run.kind, () => this.reportStopped(run, why, exitCode));
656 }
657
658 /**
659 * Tells whoever is waiting on the sandbox's work that it stopped without
660 * finishing it. Each is refused harmlessly when the sandbox reported its
661 * end before it stopped. Throws when the service could not be reached.
662 */
663 private async reportStopped(run: Run, why: string | null, exitCode: number): Promise<void> {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged664 if (run.kind === "actions") {
665 // Refused harmlessly if the job reported its end before it stopped.
Merge remote-tracking branch 'origin/main' into workspace-chat666 const response = await this.env.ACTIONS.fetch("https://actions/rpc/job_report", {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged667 method: "POST",
668 headers: { "content-type": "application/json" },
669 body: JSON.stringify({
670 job: run.jobId,
671 token: run.token,
672 report: { kind: "done", conclusion: "failure", reason: why ?? "The runner stopped before the job finished." },
673 }),
674 });
Merge remote-tracking branch 'origin/main' into workspace-chat675 await answered("job_report", response);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged676 return;
677 }
678 // Nothing was pushed, so no pull request opens; why is in its log.
679 if (run.kind === "bump") return;
680 if (run.kind === "backup") {
681 // Refused harmlessly if the sandbox reported before it stopped; the
682 // job is otherwise tried again later tonight.
Merge remote-tracking branch 'origin/main' into workspace-chat683 await reposClient(this.env.REPOS).failBackup(run.jobId, run.token, why ?? `The sandbox exited with ${exitCode}.`);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged684 return;
685 }
686 if (run.kind === "deploy") {
687 // Refused harmlessly if the build reported its end before it stopped.
Merge remote-tracking branch 'origin/main' into workspace-chat688 const response = await this.env.DEPLOYMENTS.fetch(`https://deployments/jobs/${run.deployId}/fail`, {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged689 method: "POST",
690 headers: { "content-type": "application/json" },
691 body: JSON.stringify({ token: run.token, message: why ?? "The build stopped before it finished." }),
692 });
Merge remote-tracking branch 'origin/main' into workspace-chat693 await answered("deploy fail", response);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged694 return;
695 }
696 const work = workClient(this.env.WORK);
697 if (run.kind === "checks") {
698 // Refused harmlessly if the run did report before it stopped.
699 await work.reportChecks(run.runId, run.token, {
700 error: why ?? "The sandbox stopped before the checks finished.",
701 });
702 return;
703 }
704 if (run.kind === "queue") {
705 // Refused harmlessly if the state was reported before it stopped.
706 await work.failQueue(run.entryId, run.token, why ?? "The sandbox stopped before the state was checked.");
707 return;
708 }
709 if (run.kind === "mergecheck") {
710 // Refused harmlessly if the probe reported before it stopped.
711 await work.failMergecheck(run.pullId, run.token, why ?? "The sandbox stopped before the merge check finished.");
712 return;
713 }
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.714 // A sandbox of a kind g1t no longer starts, which ran from before: it
715 // has nobody left to tell.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged716 }
717}
718
719/**
720 * What the compute gate decided for one start: go, with what billing
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.721 * reserved and the plan's caps; or not, refused with what to tell people.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged722 */
723type Granted = {
724 ok: true;
725 held: Held | null;
726 limits: PlanLimits;
727 /** The labels of the self-hosted runners it goes to; null for a sandbox. */
728 route: string[] | null;
729};
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.730type Admitted = Granted | { ok: false; code: string; message: string };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged731
732/**
733 * The guardrails' default time cap of each kind of run, for estimating what
734 * it may cost before it starts; the sandbox applies the project's own.
735 */
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.736const DEFAULT_MINUTES: Record<"queue" | "mergecheck", number> = {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged737 queue: 45,
738 mergecheck: 10,
739};
740
741/** A plan's caps on one run, for the sandbox to apply under its guardrails'. */
742function limitsOf(ent: ComputeEntitlements | null): PlanLimits {
743 return {
744 minutes: ent && ent.maxRunMinutes > 0 ? ent.maxRunMinutes : null,
745 budgetUsd: ent && ent.runCapMicros > 0 ? ent.runCapMicros / 1_000_000 : null,
746 };
747}
748
749/** The shorter of a kind's time cap and the plan's, for an estimate. */
750function estimateMinutes(minutes: number, ent: ComputeEntitlements | null): number {
751 return ent && ent.maxRunMinutes > 0 ? Math.min(minutes, ent.maxRunMinutes) : minutes;
752}
753
754/** A start the gate did not let through, as a result for whoever asked. */
755function notAdmitted(admitted: Exclude<Admitted, Granted>): Result<never> {
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.756 return fail("payment_required", admitted.message);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged757}
758
759/**
760 * Larger sandboxes for workflow jobs that ask for one in `runs-on`: the
761 * same image and behaviour on a larger Containers instance type, each a
762 * class of its own (wrangler.jsonc). Outbound handlers are registered by
763 * class, so each registers its own.
764 */
765export class Sandbox2Core extends AttemptSandbox {
766 static {
767 Sandbox2Core.outboundHandlers = { egress, abuse };
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.768 Sandbox2Core.outboundByHost = { [ABUSE_HOST]: abuse };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged769 }
770}
771export class Sandbox4Core extends AttemptSandbox {
772 static {
773 Sandbox4Core.outboundHandlers = { egress, abuse };
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.774 Sandbox4Core.outboundByHost = { [ABUSE_HOST]: abuse };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged775 }
776}
777
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.778/**
779 * What the actions service sends to start a job (`StartJobArgs`). Callers
780 * from before may also send `workflow`, `environment` and `trusted`, for
781 * workflow-only domains, which are gone: they are ignored.
782 */
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged783type ActionsJobArgs = {
784 job: string;
785 token: string;
786 repo: RepoPath;
787 timeoutMinutes: number;
788 /** The machine its `runs-on` asked for, by label; absent, the standard one. */
789 instance?: string | null;
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.790 /** Whether it may be given brokered secrets, which its requests need filled in. */
791 brokered?: boolean;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged792};
793
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.794export default class RunnerService extends WorkerEntrypoint<RunnerEnv> {
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged795 /**
796 * The JSON protocol the Rust services speak: `POST /rpc/<method>` with the
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.797 * arguments as the body. Only bound services can.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged798 */
799 async fetch(request: Request): Promise<Response> {
800 const { pathname } = new URL(request.url);
801 if (request.method === "POST" && pathname === "/rpc/start_actions_job") {
802 return Response.json(await this.startActionsJob((await request.json()) as ActionsJobArgs));
803 }
804 if (request.method === "POST" && pathname === "/rpc/stop_actions_job") {
805 const args = (await request.json()) as { job: string };
806 // Whichever machine it asked for: the job's object in every namespace.
807 await Promise.all(
808 Object.keys(SANDBOX_BINDINGS).map((className) => {
809 const namespace = sandboxNamespace(this.env, className) as unknown as DurableObjectNamespace<AttemptSandbox>;
810 return namespace
811 .get(namespace.idFromName(`actions:${args.job}`))
812 .destroy()
813 .catch(() => undefined);
814 }),
815 );
816 return Response.json(ok(true));
817 }
818 // A self-hosted runner's task ended: the sandbox that handed it over
819 // does what it does when a container stops.
820 if (request.method === "POST" && pathname === "/rpc/task_ended") {
821 const args = (await request.json()) as { sandbox: string; exitCode: number; reason?: string | null };
822 const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromString(args.sandbox));
823 await sandbox.remoteEnded(args.exitCode, args.reason ?? null);
824 return Response.json(ok(true));
825 }
Merge branch 'main' into rm-agents826 // A self-hosted job's request to a brokered secret's host, which the
827 // API passes on as it came (broker-relay.ts).
828 if (request.method === "POST" && pathname === "/rpc/broker") {
829 try {
830 return await relay(request, relayDeps(this.env.ACTIONS, (upstream) => forward(upstream)));
831 } catch (error) {
832 console.log("broker relay failed", String(error));
833 return Response.json({ error: { code: "unavailable", message: "g1t could not relay the request." } }, { status: 503 });
834 }
835 }
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged836 if (request.method === "POST" && pathname === "/rpc/bump") {
837 return Response.json(await this.startBump(await request.json()));
838 }
839 if (request.method === "POST" && pathname === "/rpc/start_deploy") {
840 return Response.json(await this.startDeploy((await request.json()) as DeployJob));
841 }
842 return new Response("Not found\n", { status: 404 });
843 }
844
845 // ---- The compute gate (@g1t/contracts compute.ts) -------------------------
846
847 /** Whether `repo` is public: what g1t's open-source pool can pay for. */
848 private async isPublic(repo: RepoPath): Promise<boolean> {
849 const found = await reposClient(this.env.REPOS)
850 .get(repo, null)
851 .catch(() => null);
852 return Boolean(found?.ok && !found.value.isPrivate);
853 }
854
855 /**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.856 * Whether a sandbox (the merge queue, a
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged857 * merge check, a workflow job) may start in `repo`, with what it may cost
858 * for `minutes` reserved. Public repositories' checks, workflows and
859 * queue can be paid by the open-source pool. Never throws.
860 */
861 private async admitSandbox(
862 kind: ComputeKind,
863 repo: RepoPath,
864 minutes: number,
865 instance: InstanceType = STANDARD_INSTANCE,
866 { selfHosted = true }: { selfHosted?: boolean } = {},
867 ): Promise<Admitted> {
868 const workspace = repo.namespace.toLowerCase();
869 const compute = gateFor(this.env);
870 const ent = await compute.entitlements(workspace);
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.871 if (ent?.paused) return { ok: false, code: "paused", message: refusalMessage("paused", workspace, kind, ent.paused) };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged872 // Checks and the merge queue go to the workspace's own runners when it
873 // says so, and cost nothing there. Workflow jobs choose with `runs-on`.
874 const route = selfHosted && (kind === "check" || kind === "queue") ? await selfHostedRoute(this.env.ACTIONS, repo) : null;
875 if (route) return { ok: true, held: null, limits: limitsOf(ent), route };
876 const [standardMicros, isPublic] = await Promise.all([compute.microsPerSecond(), this.isPublic(repo)]);
Merge branch 'worktree-agent-a56293a6f836cd343'877 // Reserved for its time limit at the larger of what it may cost (the
878 // sandbox price, scaled to its machine) and its minutes at the
879 // machine's rate, which is what it is charged.
880 const limit = estimateMinutes(minutes, ent);
881 const microsPerSecond = standardMicros * priceScale(instance);
882 const estimateMicros = Math.max(
883 sandboxEstimateMicros(limit, microsPerSecond),
884 runnerMinutes(limit * 60) * instance.microsPerMinute,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged885 );
Merge branch 'worktree-agent-a56293a6f836cd343'886 const admission = await compute.admit({ workspace, repo, public: isPublic, kind, estimateMicros }, ent);
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.887 if (!admission.ok) return { ok: false, code: admission.code, message: admission.message };
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged888 return {
889 ok: true,
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.890 held: admission.reservation ? { id: admission.reservation.id, workspace, microsPerSecond } : null,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged891 limits: limitsOf(ent),
892 route: null,
893 };
894 }
895
896 /** Gives back what was reserved for a start that never reached its sandbox. */
897 private async release(held: Held | null): Promise<void> {
898 if (held) await gateFor(this.env).settle(held.id, 0);
899 }
900
901 /**
902 * Runs `start`, giving back what was reserved if it fails. A sandbox that
903 * could not start has given it back already; settling twice at nothing
904 * is harmless.
905 */
906 private async holding<T>(granted: Granted, start: () => Promise<T>): Promise<T> {
907 try {
908 return await start();
909 } catch (error) {
910 await this.release(granted.held);
911 throw error;
912 }
913 }
914
915 /**
916 * Starts one job of a GitHub Actions workflow in a sandbox of its own.
917 * The sandbox fetches the job, its contexts and its secrets with the
918 * job's token, and reports back to the actions service through the API.
919 * Jobs run on g1t's machines, so only for workspaces that may use them.
920 */
921 private async startActionsJob(args: ActionsJobArgs): Promise<Result<true>> {
922 // The machine its `runs-on` asked for; the standard one otherwise.
923 const instance = instanceNamed(args.instance);
924 // Workflow jobs run on g1t's machines: only as the workspace's plan
925 // allows, or on a public repository, from the open-source pool.
926 const admitted = await this.admitSandbox("workflow", args.repo, args.timeoutMinutes, instance);
927 if (!admitted.ok) return fail("payment_required", `Not started: ${admitted.message}`);
928 const namespace = this.jobNamespace(instance);
929 if (!namespace) {
930 await this.release(admitted.held);
931 return fail("invalid", `Not started: ${instance.label} machines are not available here.`);
932 }
933 const sandbox = namespace.get(namespace.idFromName(`actions:${args.job}`));
934 const on = instance === STANDARD_INSTANCE ? "" : ` on ${instance.label}`;
935 try {
936 await sandbox.run({
937 kind: "actions",
938 jobId: args.job,
939 token: args.token,
940 reservation: admitted.held,
941 limits: admitted.limits,
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.942 // The job's own time limit, and whether its requests pass the
943 // Worker for its brokered secrets.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged944 build: {
945 kind: "actions",
946 minutes: Math.max(1, args.timeoutMinutes),
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.947 brokered: args.brokered === true,
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged948 },
949 meter: {
950 ...meter(args.repo, `A workflow job in ${args.repo.namespace}/${args.repo.name}${on}`),
951 instance: instance === STANDARD_INSTANCE ? null : instance.label,
952 },
953 envVars: {
954 MODE: "actions",
955 G1T_API: "https://api.g1t.sh",
956 ACTIONS_JOB: args.job,
957 ACTIONS_TOKEN: args.token,
Merge branch 'main' into actions-toolkit-oidc-artifacts958 // Docker of the job's own, inside its sandbox (crates/runner docker/).
959 G1T_DOCKER: dockerFor(this.env.DOCKER),
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged960 },
961 });
962 } catch (error) {
963 // A sandbox that could not start, or stopped at once: the job fails
964 // with why, rather than waiting to be noticed.
965 return {
966 ok: false,
967 error: { code: "conflict", message: `The runner could not start the job: ${String(error).replace(/^Error: /, "")}` },
968 };
969 }
970 // `true`, not null: an outcome needs a value.
971 return ok(true);
972 }
973
Merge branch 'worktree-agent-a56293a6f836cd343'974 /** The sandboxes of a label: the class the catalogue names for it, by its binding. */
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged975 private jobNamespace(instance: InstanceType): DurableObjectNamespace<AttemptSandbox> | null {
Merge branch 'worktree-agent-a56293a6f836cd343'976 return jobNamespaceOf(this.env, instance) as DurableObjectNamespace<AttemptSandbox> | null;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged977 }
978
979 /**
980 * Builds one commit in a sandbox of its own and deploys it to g1t.page.
981 * Asked by the deployments service, which has already checked that the
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.982 * workspace pays for Deployments; that plan is what
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged983 * lets a build use g1t's machines.
984 */
985 private async startDeploy(job: DeployJob): Promise<Result<true>> {
986 // To read the commit, which may be private, as whoever pushed it.
987 const token = await runCredential(this.env.IDENTITY, {
988 onBehalfOf: job.actor,
989 repo: job.source,
990 kind: "deploy",
991 use: "runner",
992 read: [job.source],
993 ttlSeconds: DEPLOY_TOKEN_TTL_SECONDS,
994 });
995 const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromName(`deploy:${job.deployId}`));
996 const workspace = (job.workspace ?? job.source.namespace).toLowerCase();
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.997 // The project the build is for, and who it is charged to.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged998 const project = job.repo ?? job.source;
999 try {
1000 await sandbox.run({
1001 kind: "deploy",
1002 deployId: job.deployId,
1003 token: job.token,
1004 reservation: job.reservation
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1005 ? { id: job.reservation, workspace, microsPerSecond: job.microsPerSecond ?? 0 }
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1006 : null,
1007 limits: { minutes: job.maxRunMinutes ?? null },
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.1008 // For as long as its read token lasts.
1009 build: { kind: "deploy", minutes: DEPLOY_TOKEN_TTL_SECONDS / 60 },
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1010 owner: { workspace, repo: `${project.namespace}/${project.name}` },
1011 envVars: {
1012 MODE: "deploy",
1013 G1T_API: "https://api.g1t.sh",
1014 DEPLOY_ID: job.deployId,
1015 DEPLOY_TOKEN: job.token,
1016 G1T_USER: job.actor.username,
1017 G1T_TOKEN: token,
1018 GIT_REMOTE: `https://g1t.sh/${job.source.namespace}/${job.source.name}.git`,
1019 GIT_COMMIT: job.commit,
1020 ROOT_DIR: job.rootDir ?? "",
1021 BUILD_COMMAND: job.buildCommand ?? "",
1022 OUTPUT_DIR: job.outputDir ?? "",
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.1023 BUILD_AS: job.buildAs ?? "auto",
1024 DOCKERFILE: job.dockerfile ?? "",
1025 // A container build's own Docker Engine (crates/runner docker/),
1026 // started only if the project is built as one.
1027 G1T_DOCKER: dockerFor(this.env.DOCKER),
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1028 BUILD_ENV: JSON.stringify(job.buildEnv ?? {}),
1029 BUILD_SECRETS: JSON.stringify(job.buildSecrets ?? {}),
1030 },
1031 });
1032 } catch (error) {
1033 return {
1034 ok: false,
1035 error: { code: "conflict", message: `The runner could not start the build: ${String(error).replace(/^Error: /, "")}` },
1036 };
1037 }
1038 return ok(true);
1039 }
1040
1041 /**
1042 * Makes a security update in a sandbox of its own (crates/runner
1043 * bump.rs): raises one package to a fixed version in the lockfiles
1044 * named, commits that as g1t and pushes it to its `g1t/security/…`
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1045 * branch. A version update (`kind: "version"`) raises one or more
1046 * packages the same way, to the branch its dependency update file names,
1047 * which is never the default one. Asked by the security service, which
1048 * opens the pull request when it hears the push; nothing here opens one.
1049 * Admitted, reserved and metered like checks, always in g1t's sandbox (a
1050 * self-hosted runner may not know the mode), under the project's network
1051 * list plus the package registries. Returns whether the sandbox started.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1052 */
1053 private async startBump(input: unknown): Promise<Result<boolean>> {
1054 const problem = bumpProblem(input, UPDATE_BRANCH_PREFIX);
1055 if (problem) return fail("invalid", problem);
1056 const args = input as BumpArgs;
1057 const repo = args.repo;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1058 const what = args.kind === "version" ? "version update" : "security update";
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1059 const actor = systemActor(repo.namespace);
1060 const closed = await this.closedRepo(actor, repo);
1061 if (closed) return closed;
1062 const admitted = await this.admitSandbox("check", repo, BUMP_MINUTES, STANDARD_INSTANCE, { selfHosted: false });
1063 if (!admitted.ok) return notAdmitted(admitted);
1064 try {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1065 const defaultBranch = await this.defaultBranch(repo, actor);
1066 // From its `target-branch`, which its pull request merges into, or
1067 // the default branch.
1068 const base = args.base ?? defaultBranch;
1069 // A version update names its own branch, which is never the one it
1070 // starts from, nor the default one.
1071 if (args.kind === "version" && (args.branch === defaultBranch || args.branch === base)) {
1072 await this.release(admitted.held);
1073 return fail("invalid", `A version update cannot push to ${args.branch}, the branch it starts from.`);
1074 }
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1075 // As g1t, for the workspace: reads the repository and pushes this
1076 // branch only, with no API operations.
1077 const token = await runCredential(this.env.IDENTITY, {
1078 onBehalfOf: actor,
1079 repo,
1080 kind: "bump",
1081 use: "runner",
1082 read: [repo],
1083 push: [{ repo, branch: args.branch }],
1084 ttlSeconds: BUMP_TOKEN_TTL_SECONDS,
1085 });
1086 const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromName(bumpSandboxName(args)));
1087 await sandbox.run({
1088 kind: "bump",
1089 repo,
1090 branch: args.branch,
1091 reservation: admitted.held,
1092 limits: admitted.limits,
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.1093 build: { kind: "bump", minutes: BUMP_MINUTES },
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1094 meter: meter(repo, `${args.kind === "version" ? "Version" : "Security"} update in ${repo.namespace}/${repo.name}`),
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1095 envVars: bumpEnv(args, base, token),
1096 });
1097 } catch (error) {
1098 await this.release(admitted.held);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1099 return fail("conflict", `The runner could not start the ${what}: ${String(error).replace(/^Error: /, "")}`);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1100 }
1101 return ok(true);
1102 }
1103
1104 /**
1105 * Events from the bus. Each one that could change what a pull request
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1106 * needs next moves it along: the work service decides whether it can
1107 * merge by the repository's settings, and merges it if so. Merge checks
1108 * and the merge queue's states start here too.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1109 */
1110 async queue(batch: MessageBatch<G1tEvent>): Promise<void> {
1111 for (const message of batch.messages) {
1112 const event = message.body;
1113 switch (event.type) {
1114 case "pull.opened":
1115 case "pull.ready":
1116 case "pull.updated":
1117 case "checks.completed":
1118 case "review.completed":
1119 case "pull.mergeability":
1120 await this.advance(event.data.pullId);
1121 break;
1122 // Its head or its target moved and both changed the same files:
1123 // find out whether it still merges cleanly.
1124 case "pull.mergecheck":
1125 await this.startMergecheck(event.data.pullId);
1126 break;
1127 // Something joined, left or landed: test the next batch if none is.
1128 case "queue.changed":
1129 await this.buildQueue(event.data.repoId);
1130 break;
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1131 // A person approved or asked for changes, which may let it merge.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1132 case "comment.created":
1133 if (event.data.pullId && event.data.verdict) await this.advance(event.data.pullId);
1134 break;
1135 // The branch the others would land on has moved.
1136 case "pull.merged":
1137 await this.advanceAll(event.data.repoId);
1138 break;
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1139 // Read-only or gone: the runs there stop.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1140 case "repo.archived":
1141 case "repo.deleted":
1142 await this.stopRunsIn(event.data.repoId);
1143 break;
1144 }
1145 message.ack();
1146 }
1147 }
1148
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1149 /** A sweep, for steps whose trigger was missed or whose sandbox died, and the nightly backups. */
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1150 async scheduled(): Promise<void> {
1151 await this.advanceAll();
1152 await this.startBackups().catch((error: unknown) => console.log("backups not started", String(error)));
1153 }
1154
1155 /**
1156 * Starts a few of the nightly backups the repos service queued, each in
1157 * a sandbox of its own that holds only its job's token: the sandbox asks
1158 * for a read-only git credential itself, when it is ready to clone. No
1159 * plan is asked and nothing is metered: backups are g1t's own work.
1160 */
1161 private async startBackups(): Promise<void> {
1162 const pace = backupPace(this.env.BACKUPS_PER_SWEEP, this.env.BACKUPS_RUNNING);
1163 if (pace.perSweep === 0) return;
1164 const repos = reposClient(this.env.REPOS);
1165 for (const claim of await repos.claimBackups(pace.perSweep, pace.running)) {
1166 try {
1167 const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromName(backupSandboxName(claim)));
1168 await sandbox.run({
1169 kind: "backup",
1170 jobId: claim.jobId,
1171 token: claim.token,
1172 // For `abuse.flagged`: whose repository it was.
1173 owner: { workspace: claim.path.namespace, repo: `${claim.path.namespace}/${claim.path.name}` },
1174 envVars: backupEnv(claim, "https://api.g1t.sh"),
1175 });
1176 } catch (error) {
1177 await repos.failBackup(claim.jobId, claim.token, `The sandbox could not start: ${String(error)}`).catch(() => null);
1178 }
1179 }
1180 }
1181
1182 private async advanceAll(repoId?: string): Promise<void> {
1183 const pulls = await workClient(this.env.WORK).managedPulls(repoId);
1184 for (const pullId of pulls) await this.advance(pullId);
1185 }
1186
1187 /**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1188 * Merges a pull request by the repository's settings when it is ready:
1189 * the work service decides and merges, and claims the step, so calling
1190 * this twice merges nothing twice.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1191 */
1192 private async advance(pullId: string): Promise<void> {
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1193 const next = await workClient(this.env.WORK).advance(pullId);
1194 // g1t no longer reviews, revises or catches up pull requests itself.
1195 if (next.action !== "none") console.log("lifecycle step not taken", pullId, next.action);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1196 }
1197
1198 /**
1199 * Starts the next batch of a repository's merge queue, if it has one
1200 * ready: a sandbox per entry, all at once, each building the default
1201 * branch with that entry and everything ahead of it.
1202 */
1203 private async buildQueue(repoId: string): Promise<void> {
1204 const work = workClient(this.env.WORK);
1205 const jobs = await work.queueBuild(repoId);
1206 // Merge queue sandboxes, like any other, only as the workspace's plan
1207 // allows: refused states fail at once, saying why. A state whose
1208 // sandbox could not start fails at once too, rather than holding the
1209 // queue until it times out.
1210 await Promise.all(
1211 jobs.map(async (job) => {
1212 const admitted = await this.admitSandbox("queue", job.repo, DEFAULT_MINUTES.queue);
1213 if (!admitted.ok) {
1214 await work.failQueue(job.entryId, job.token, `Not started: ${admitted.message}`);
1215 return;
1216 }
1217 await this.holding(admitted, () => this.startQueueRun(job, admitted)).catch((error: unknown) =>
1218 work.failQueue(job.entryId, job.token, `Its sandbox could not start: ${String(error)}`),
1219 );
1220 }),
1221 );
1222 }
1223
1224 private async startQueueRun(job: QueueJob, granted: Granted): Promise<void> {
1225 // To read the changes and push the tested state, as a member.
1226 // Reads each queued change; pushes only the queue's own branch.
1227 const token = await runCredential(this.env.IDENTITY, {
1228 onBehalfOf: job.actor,
1229 repo: job.repo,
1230 kind: "queue",
1231 use: "runner",
1232 number: job.stack.at(-1)?.number ?? null,
1233 read: job.stack.map((item) => item.source),
1234 push: [{ repo: job.repo, branch: job.branch }],
1235 ttlSeconds: CHECKS_TOKEN_TTL_SECONDS,
1236 });
1237 const remote = (path: RepoPath) => `https://g1t.sh/${path.namespace}/${path.name}.git`;
1238 const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromName(`queue-${job.entryId}-${job.baseCommit}`));
1239 await sandbox.run({
1240 kind: "queue",
1241 entryId: job.entryId,
1242 token: job.token,
1243 reservation: granted.held,
1244 limits: granted.limits,
1245 selfHosted: granted.route,
1246 track: {
1247 actor: job.actor,
1248 repo: job.repo,
1249 kind: "queue",
1250 number: job.stack.at(-1)?.number ?? null,
1251 title: `Merge queue: ${job.stack.map((item) => `#${item.number}`).join(" + ")}`,
1252 },
1253 meter: meter(job.repo, `Merge queue on ${job.repo.namespace}/${job.repo.name}`),
1254 envVars: {
1255 MODE: "queue",
1256 G1T_API: "https://api.g1t.sh",
1257 QUEUE_ENTRY: job.entryId,
1258 QUEUE_TOKEN: job.token,
1259 G1T_USER: job.actor.username,
1260 G1T_TOKEN: token,
1261 BASE_REMOTE: remote(job.repo),
1262 BASE_COMMIT: job.baseCommit,
1263 QUEUE_BRANCH: job.branch,
1264 STACK: JSON.stringify(
1265 job.stack.map((item) => ({
1266 number: item.number,
1267 title: item.title,
1268 remote: remote(item.source),
1269 branch: item.branch,
1270 commit: item.commit,
1271 })),
1272 ),
1273 CHECKS: JSON.stringify(job.checks),
1274 CONTRACT_CHECKS: JSON.stringify(job.contractChecks),
1275 },
1276 });
1277 }
1278
1279 /**
1280 * Merges a pull request's head into its target in a sandbox of its own,
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1281 * pushing nothing, to find the files that conflict.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1282 * The work service decides when one is needed and how many may run.
1283 */
1284 private async startMergecheck(pullId: string): Promise<void> {
1285 const work = workClient(this.env.WORK);
1286 const started = await work.startMergecheck(pullId);
1287 if (!started.ok) return;
1288 const job = started.value;
1289 let granted: Granted | null = null;
1290 try {
1291 // Like any sandbox, only as the workspace's plan allows.
1292 const admitted = await this.admitSandbox("check", job.repo, DEFAULT_MINUTES.mergecheck);
1293 if (!admitted.ok) throw new Error(`Not started: ${admitted.message}`);
1294 granted = admitted;
1295 // To read the change, which may be private, as whoever opened it.
1296 const token = await runCredential(this.env.IDENTITY, {
1297 onBehalfOf: job.author,
1298 repo: job.repo,
1299 kind: "mergecheck",
1300 use: "runner",
1301 number: job.number,
1302 read: [job.repo, job.source],
1303 ttlSeconds: MERGECHECK_TOKEN_TTL_SECONDS,
1304 });
1305 const remote = (path: RepoPath) => `https://g1t.sh/${path.namespace}/${path.name}.git`;
1306 // One sandbox per pair of commits: asking twice starts nothing twice.
1307 const sandbox = this.env.SANDBOX.get(
1308 this.env.SANDBOX.idFromName(`mergecheck-${job.pullId}-${job.head}-${job.base}`),
1309 );
1310 await sandbox.run({
1311 kind: "mergecheck",
1312 pullId: job.pullId,
1313 token: job.token,
1314 reservation: granted.held,
1315 limits: granted.limits,
1316 selfHosted: granted.route,
1317 meter: meter(job.repo, `Merge check of ${job.repo.namespace}/${job.repo.name}#${job.number}`),
1318 envVars: {
1319 MODE: "mergecheck",
1320 G1T_API: "https://api.g1t.sh",
1321 MERGECHECK_PULL: job.pullId,
1322 MERGECHECK_TOKEN: job.token,
1323 G1T_USER: job.author.username,
1324 G1T_TOKEN: token,
1325 BASE_REMOTE: remote(job.repo),
1326 BASE_COMMIT: job.base,
1327 HEAD_REMOTE: remote(job.source),
1328 HEAD_BRANCH: job.branch,
1329 HEAD_COMMIT: job.head,
1330 },
1331 });
1332 } catch (error) {
1333 if (granted) await this.release(granted.held);
1334 await work.failMergecheck(job.pullId, job.token, error instanceof Error ? error.message : String(error));
1335 }
1336 }
1337
1338 /**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1339 * A refusal if `repo` takes no work from anyone: it is archived, so
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1340 * read-only, or it was deleted (repos hides a deleted one, so it is not
1341 * found). Null when repos cannot answer now; the other checks still run.
1342 */
1343 private async closedRepo(actor: User, repo: RepoPath): Promise<Result<never> | null> {
1344 const repos = reposClient(this.env.REPOS);
1345 const found = await repos.get(repo, actor).catch(() => null);
1346 if (!found) return null;
1347 if (!found.ok) {
1348 return found.error.code === "not_found"
1349 ? fail("not_found", `There is no repository at ${repo.namespace}/${repo.name}, or it was deleted.`)
1350 : null;
1351 }
1352 const status = await repos.statusById(found.value.id).catch(() => null);
1353 if (status?.deleted) {
1354 return fail("not_found", `${found.value.namespace}/${found.value.name} was deleted. An owner can restore it from the workspace's settings.`);
1355 }
1356 if (status?.archived || found.value.archivedAt) {
1357 return fail(
1358 "forbidden",
1359 `${found.value.namespace}/${found.value.name} is archived, so it is read-only. An owner can unarchive it in its settings.`,
1360 );
1361 }
1362 return null;
1363 }
1364
1365 /**
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.1366 * Stops every run in a repository that was archived or deleted: the
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged1367 * work service marks them stopped when it hears of it, and lists them
1368 * here (`runs_in_repo`, by id, so a deleted repository's runs are found
1369 * too), and each sandbox is destroyed. Never throws.
1370 */
1371 private async stopRunsIn(repoId: string): Promise<void> {
1372 try {
1373 const response = await this.env.WORK.fetch("https://work/rpc/runs_in_repo", {
1374 method: "POST",
1375 headers: { "content-type": "application/json" },
1376 body: JSON.stringify({ repoId }),
1377 });
1378 if (!response.ok) return;
1379 const runs = (await response.json()) as { runId: string; sandbox: string | null }[];
1380 for (const run of runs) {
1381 if (!run.sandbox) continue;
1382 try {
1383 await this.env.SANDBOX.get(this.env.SANDBOX.idFromString(run.sandbox)).halt("The repository was archived or deleted.");
1384 } catch (error) {
1385 // Already gone, or never started.
1386 console.log("sandbox not destroyed", run.runId, String(error));
1387 }
1388 }
1389 } catch (error) {
1390 console.error("could not stop the runs in", repoId, error);
1391 }
1392 }
1393
1394 private async defaultBranch(repo: RepoPath, viewer: Viewer): Promise<string> {
1395 const found = await reposClient(this.env.REPOS).get(repo, viewer);
1396 return found.ok ? found.value.defaultBranch : "main";
1397 }
1398}

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