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