Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
170 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

g1t push: images with layers of any size, uploaded in chunks under the edge's limit1/**
2 * Guardrails, as the runner applies them to a sandbox: what it may reach,
3 * what its harness refuses, and how long and how much a run may take.
4 *
5 * - The network list is enforced here, outside the sandbox: a guarded
6 * sandbox starts with no internet, and every HTTP(S) request it makes
7 * comes to `egress` below, which forwards it or refuses it.
8 * - Command rules and the cost cap are handed to the harness inside the
9 * sandbox as `GUARDRAILS`, which applies them to the agent.
10 * - The time cap is enforced twice: by the harness, and by the sandbox's
11 * own alarm here, which stops the whole sandbox a little after.
12 */
13import type { OutboundHandlerContext } from "@cloudflare/containers";
14
15import { type RepoPath, type RunKind, type ServiceBinding, guardrailsClient } from "@g1t/contracts";
16
17import {
18 ABUSE_HOST,
19 type ModelHosts,
20 type RunGuard,
21 type WorkflowJob,
22 allows,
23 buildHosts,
24 jobHosts,
25 refusal,
26 sandboxHosts,
27 sandboxNamespace,
28} from "./egress";
A secret a job uses only to call an API can be brokered, so its value never enters the job's sandbox. Under Settings, Secrets and variables, a secret's new Brokered choice (Only for these hosts) takes the hosts it is for, written as allowed domains are (registry.npmjs.org, *.example.com); the list shows a Brokered badge whose hint names them. A workflow job then gets a stand-in in its place, g1tsec_ and 48 random hex characters, made for that job and that secret, wherever the value would have gone: as the secret, under any key a called workflow passes it as, and inside a value built from it. As the job's requests pass the runner Worker, a stand-in on a request to one of the secret's hosts becomes the real value in any header (inside Authorization: Basic it is decoded, filled in and encoded again) and in the query string, never the path or the body; a filled-in request is not followed through redirects outside the sandbox, and an answer that echoes the value has it swapped back for the stand-in in its headers, and in its body when that is small, uncompressed text. A stand-in sent to any other host goes as it is and the job's log says once that NPM_TOKEN was sent to example.com, which it is not for, so g1t did not fill it in.29import { type BrokeredParams, brokered } from "./broker-egress.ts";
A sandbox's request to an allowed host that fails upstream no longer resets the sandbox's connection. The runner Worker sent each one on with a bare fetch, so a reset or a failure upstream (as under npm ci's many tarball fetches) was thrown out of the outbound handler, and npm saw ECONNRESET and gave up. Forwarding is now one function (forward.ts) that egress() and the brokered-secret helper both use: a GET or HEAD with no body is tried twice more, after 250 ms and then a second, and what still fails comes back as a 502 whose plain-text body names the host and the error, with x-g1t-egress: upstream-failed, which npm and most tools retry. A request with a body is never sent twice. Each failure is one line of the Worker's log, with the host, the method and the error.30import { forward } from "./forward.ts";
g1t push: images with layers of any size, uploaded in chunks under the edge's limit31
32export {
33 ABUSE_EXIT_CODE,
34 ABUSE_HOST,
35 ABUSE_MESSAGE,
36 SANDBOX_BINDINGS,
Merge branch 'main' into actions-toolkit-oidc-artifacts37 dockerFor,
g1t push: images with layers of any size, uploaded in chunks under the edge's limit38 harnessEnv,
39 jobHosts,
40 newlyBlocked,
41 sandboxNamespace,
42 timeCapMessage,
43 withPlanLimits,
44} from "./egress";
45export type { PlanLimits, RunGuard, WorkflowJob } from "./egress";
46
A secret a job uses only to call an API can be brokered, so its value never enters the job's sandbox. Under Settings, Secrets and variables, a secret's new Brokered choice (Only for these hosts) takes the hosts it is for, written as allowed domains are (registry.npmjs.org, *.example.com); the list shows a Brokered badge whose hint names them. A workflow job then gets a stand-in in its place, g1tsec_ and 48 random hex characters, made for that job and that secret, wherever the value would have gone: as the secret, under any key a called workflow passes it as, and inside a value built from it. As the job's requests pass the runner Worker, a stand-in on a request to one of the secret's hosts becomes the real value in any header (inside Authorization: Basic it is decoded, filled in and encoded again) and in the query string, never the path or the body; a filled-in request is not followed through redirects outside the sandbox, and an answer that echoes the value has it swapped back for the stand-in in its headers, and in its body when that is small, uncompressed text. A stand-in sent to any other host goes as it is and the job's log says once that NPM_TOKEN was sent to example.com, which it is not for, so g1t did not fill it in.47/**
48 * What the outbound handler is given: the hosts this sandbox may reach,
49 * and what brokered secrets need (broker-egress.ts).
50 */
51export type EgressParams = { hosts: string[] } & BrokeredParams;
g1t push: images with layers of any size, uploaded in chunks under the edge's limit52
53
54/** The stop by the sandbox's alarm comes this long after the harness's own. */
55export const ALARM_GRACE_SECONDS = 3 * 60;
56
57/**
58 * The guardrails of a run in `repo`. Throws when they cannot be read: a
59 * sandbox is not started without them.
60 */
61export async function guardFor(work: ServiceBinding, repo: RepoPath, kind: RunKind): Promise<RunGuard> {
62 const found = await guardrailsClient(work).runGuardrails(repo);
63 if (!found.ok) throw new Error(`g1t could not read this project's guardrails: ${found.error.message}`);
64 const policy = found.value;
65 return { policy, minutes: policy.minutes[kind] ?? 60 };
66}
67
68/**
69 * The guardrails of a workflow job, deploy build or security update in
70 * `repo`: its project's network list, plus what builds need (`buildHosts`), and the
71 * time cap it was given. `repo` is the project, not a pull request's
72 * working copy; `repoId`, when known, finds it however it has moved.
73 * A workflow job of a trusted run also gets the workflow-only domains
74 * that name its workflow and environment (`jobHosts`); nothing else
75 * ever does. Throws when they cannot be read: no build starts without them.
76 */
77export async function buildGuardFor(
78 work: ServiceBinding,
79 repo: RepoPath,
80 kind: "actions" | "deploy" | "bump",
81 minutes: number,
82 repoId?: string | null,
83 job?: WorkflowJob | null,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar84 extra: readonly string[] = [],
g1t push: images with layers of any size, uploaded in chunks under the edge's limit85): Promise<RunGuard> {
86 const found = await guardrailsClient(work).runGuardrails(repo, repoId);
87 if (!found.ok) throw new Error(`g1t could not read this project's guardrails: ${found.error.message}`);
88 const policy = found.value;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar89 const hosts = [...policy.hosts, ...buildHosts(kind), ...jobHosts(policy, kind, job), ...extra];
g1t push: images with layers of any size, uploaded in chunks under the edge's limit90 return { policy: { ...policy, hosts: [...new Set(hosts)] }, minutes };
91}
92
93
94/** Every host the sandbox may reach, for the outbound handler. */
95export function egressHosts(guard: RunGuard, env: ModelHosts, sandboxEnv: Record<string, string>): string[] {
96 return sandboxHosts(guard.policy.hosts, env, sandboxEnv);
97}
98
99/**
100 * The outbound handler of a guarded sandbox: every HTTP and HTTPS request
101 * it makes. An allowed host is fetched as asked; any other is refused, and
102 * the sandbox told so it can say so on the run.
103 */
104export async function egress(
105 request: Request,
A secret a job uses only to call an API can be brokered, so its value never enters the job's sandbox. Under Settings, Secrets and variables, a secret's new Brokered choice (Only for these hosts) takes the hosts it is for, written as allowed domains are (registry.npmjs.org, *.example.com); the list shows a Brokered badge whose hint names them. A workflow job then gets a stand-in in its place, g1tsec_ and 48 random hex characters, made for that job and that secret, wherever the value would have gone: as the secret, under any key a called workflow passes it as, and inside a value built from it. As the job's requests pass the runner Worker, a stand-in on a request to one of the secret's hosts becomes the real value in any header (inside Authorization: Basic it is decoded, filled in and encoded again) and in the query string, never the path or the body; a filled-in request is not followed through redirects outside the sandbox, and an answer that echoes the value has it swapped back for the stand-in in its headers, and in its body when that is small, uncompressed text. A stand-in sent to any other host goes as it is and the job's log says once that NPM_TOKEN was sent to example.com, which it is not for, so g1t did not fill it in.106 env: { SANDBOX: DurableObjectNamespace; ACTIONS?: ServiceBinding },
g1t push: images with layers of any size, uploaded in chunks under the edge's limit107 ctx: OutboundHandlerContext<EgressParams>,
108): Promise<Response> {
109 const host = new URL(request.url).host;
110 // A sandbox reporting that it stopped itself for mining.
111 if (host === ABUSE_HOST) return abuse(request, env, ctx);
A secret a job uses only to call an API can be brokered, so its value never enters the job's sandbox. Under Settings, Secrets and variables, a secret's new Brokered choice (Only for these hosts) takes the hosts it is for, written as allowed domains are (registry.npmjs.org, *.example.com); the list shows a Brokered badge whose hint names them. A workflow job then gets a stand-in in its place, g1tsec_ and 48 random hex characters, made for that job and that secret, wherever the value would have gone: as the secret, under any key a called workflow passes it as, and inside a value built from it. As the job's requests pass the runner Worker, a stand-in on a request to one of the secret's hosts becomes the real value in any header (inside Authorization: Basic it is decoded, filled in and encoded again) and in the query string, never the path or the body; a filled-in request is not followed through redirects outside the sandbox, and an answer that echoes the value has it swapped back for the stand-in in its headers, and in its body when that is small, uncompressed text. A stand-in sent to any other host goes as it is and the job's log says once that NPM_TOKEN was sent to example.com, which it is not for, so g1t did not fill it in.112 if (ctx.params?.open || allows(ctx.params?.hosts ?? [], host)) {
113 // A workflow job's brokered secrets are filled in first, if it has any.
A sandbox's request to an allowed host that fails upstream no longer resets the sandbox's connection. The runner Worker sent each one on with a bare fetch, so a reset or a failure upstream (as under npm ci's many tarball fetches) was thrown out of the outbound handler, and npm saw ECONNRESET and gave up. Forwarding is now one function (forward.ts) that egress() and the brokered-secret helper both use: a GET or HEAD with no body is tried twice more, after 250 ms and then a second, and what still fails comes back as a 502 whose plain-text body names the host and the error, with x-g1t-egress: upstream-failed, which npm and most tools retry. A request with a body is never sent twice. Each failure is one line of the Worker's log, with the host, the method and the error.114 return (await brokered(request, env, ctx)) ?? forward(request);
A secret a job uses only to call an API can be brokered, so its value never enters the job's sandbox. Under Settings, Secrets and variables, a secret's new Brokered choice (Only for these hosts) takes the hosts it is for, written as allowed domains are (registry.npmjs.org, *.example.com); the list shows a Brokered badge whose hint names them. A workflow job then gets a stand-in in its place, g1tsec_ and 48 random hex characters, made for that job and that secret, wherever the value would have gone: as the secret, under any key a called workflow passes it as, and inside a value built from it. As the job's requests pass the runner Worker, a stand-in on a request to one of the secret's hosts becomes the real value in any header (inside Authorization: Basic it is decoded, filled in and encoded again) and in the query string, never the path or the body; a filled-in request is not followed through redirects outside the sandbox, and an answer that echoes the value has it swapped back for the stand-in in its headers, and in its body when that is small, uncompressed text. A stand-in sent to any other host goes as it is and the job's log says once that NPM_TOKEN was sent to example.com, which it is not for, so g1t did not fill it in.115 }
g1t push: images with layers of any size, uploaded in chunks under the edge's limit116 try {
117 const namespace = sandboxNamespace(env, ctx.className);
118 const sandbox = namespace.get(namespace.idFromString(ctx.containerId)) as unknown as {
119 noteBlocked(host: string): Promise<void>;
120 };
121 await sandbox.noteBlocked(host);
122 } catch (error) {
123 console.log("blocked host not reported", host, String(error));
124 }
125 return refusal(host);
126}
127
128/**
129 * A sandbox's report that it stopped itself for mining (crates/runner
130 * abuse.rs), handed to its Durable Object. Reached through `egress` for a
131 * guarded sandbox and as the handler for `ABUSE_HOST` for any other.
132 */
133export async function abuse(
134 request: Request,
135 env: { SANDBOX: DurableObjectNamespace },
136 ctx: OutboundHandlerContext<unknown>,
137): Promise<Response> {
138 let verdict: unknown = null;
139 try {
140 verdict = ((await request.json()) as { verdict?: unknown }).verdict ?? null;
141 } catch {
142 // A report without its metrics still stops the run.
143 }
144 try {
145 const namespace = sandboxNamespace(env, ctx.className);
146 const sandbox = namespace.get(namespace.idFromString(ctx.containerId)) as unknown as {
147 flagAbuse(verdict: unknown): Promise<void>;
148 };
149 await sandbox.flagAbuse(verdict);
150 } catch (error) {
151 console.log("abuse report not handled", String(error));
152 }
153 return new Response("noted\n");
154}
155
156/** Adds a step to a run, with its token. Never fails the caller. */
157export async function reportRun(
158 work: ServiceBinding,
159 tracked: { runId: string; token: string },
160 report: { steps?: string[]; halt?: "budget" | "time" | "abuse"; error?: string },
161): Promise<void> {
162 await work
163 .fetch("https://service/rpc/report_run", {
164 method: "POST",
165 headers: { "content-type": "application/json" },
166 body: JSON.stringify({ runId: tracked.runId, token: tracked.token, ...report }),
167 })
168 .catch((error: unknown) => console.log("run report failed", tracked.runId, String(error)));
169}
170

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