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
169 linesCodeBlameRaw
1/**
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";
29import { type BrokeredParams, brokered } from "./broker-egress.ts";
30
31export {
32 ABUSE_EXIT_CODE,
33 ABUSE_HOST,
34 ABUSE_MESSAGE,
35 SANDBOX_BINDINGS,
36 dockerFor,
37 harnessEnv,
38 jobHosts,
39 newlyBlocked,
40 sandboxNamespace,
41 timeCapMessage,
42 withPlanLimits,
43} from "./egress";
44export type { PlanLimits, RunGuard, WorkflowJob } from "./egress";
45
46/**
47 * What the outbound handler is given: the hosts this sandbox may reach,
48 * and what brokered secrets need (broker-egress.ts).
49 */
50export type EgressParams = { hosts: string[] } & BrokeredParams;
51
52
53/** The stop by the sandbox's alarm comes this long after the harness's own. */
54export const ALARM_GRACE_SECONDS = 3 * 60;
55
56/**
57 * The guardrails of a run in `repo`. Throws when they cannot be read: a
58 * sandbox is not started without them.
59 */
60export async function guardFor(work: ServiceBinding, repo: RepoPath, kind: RunKind): Promise<RunGuard> {
61 const found = await guardrailsClient(work).runGuardrails(repo);
62 if (!found.ok) throw new Error(`g1t could not read this project's guardrails: ${found.error.message}`);
63 const policy = found.value;
64 return { policy, minutes: policy.minutes[kind] ?? 60 };
65}
66
67/**
68 * The guardrails of a workflow job, deploy build or security update in
69 * `repo`: its project's network list, plus what builds need (`buildHosts`), and the
70 * time cap it was given. `repo` is the project, not a pull request's
71 * working copy; `repoId`, when known, finds it however it has moved.
72 * A workflow job of a trusted run also gets the workflow-only domains
73 * that name its workflow and environment (`jobHosts`); nothing else
74 * ever does. Throws when they cannot be read: no build starts without them.
75 */
76export async function buildGuardFor(
77 work: ServiceBinding,
78 repo: RepoPath,
79 kind: "actions" | "deploy" | "bump",
80 minutes: number,
81 repoId?: string | null,
82 job?: WorkflowJob | null,
83 extra: readonly string[] = [],
84): Promise<RunGuard> {
85 const found = await guardrailsClient(work).runGuardrails(repo, repoId);
86 if (!found.ok) throw new Error(`g1t could not read this project's guardrails: ${found.error.message}`);
87 const policy = found.value;
88 const hosts = [...policy.hosts, ...buildHosts(kind), ...jobHosts(policy, kind, job), ...extra];
89 return { policy: { ...policy, hosts: [...new Set(hosts)] }, minutes };
90}
91
92
93/** Every host the sandbox may reach, for the outbound handler. */
94export function egressHosts(guard: RunGuard, env: ModelHosts, sandboxEnv: Record<string, string>): string[] {
95 return sandboxHosts(guard.policy.hosts, env, sandboxEnv);
96}
97
98/**
99 * The outbound handler of a guarded sandbox: every HTTP and HTTPS request
100 * it makes. An allowed host is fetched as asked; any other is refused, and
101 * the sandbox told so it can say so on the run.
102 */
103export async function egress(
104 request: Request,
105 env: { SANDBOX: DurableObjectNamespace; ACTIONS?: ServiceBinding },
106 ctx: OutboundHandlerContext<EgressParams>,
107): Promise<Response> {
108 const host = new URL(request.url).host;
109 // A sandbox reporting that it stopped itself for mining.
110 if (host === ABUSE_HOST) return abuse(request, env, ctx);
111 if (ctx.params?.open || allows(ctx.params?.hosts ?? [], host)) {
112 // A workflow job's brokered secrets are filled in first, if it has any.
113 return (await brokered(request, env, ctx)) ?? fetch(request);
114 }
115 try {
116 const namespace = sandboxNamespace(env, ctx.className);
117 const sandbox = namespace.get(namespace.idFromString(ctx.containerId)) as unknown as {
118 noteBlocked(host: string): Promise<void>;
119 };
120 await sandbox.noteBlocked(host);
121 } catch (error) {
122 console.log("blocked host not reported", host, String(error));
123 }
124 return refusal(host);
125}
126
127/**
128 * A sandbox's report that it stopped itself for mining (crates/runner
129 * abuse.rs), handed to its Durable Object. Reached through `egress` for a
130 * guarded sandbox and as the handler for `ABUSE_HOST` for any other.
131 */
132export async function abuse(
133 request: Request,
134 env: { SANDBOX: DurableObjectNamespace },
135 ctx: OutboundHandlerContext<unknown>,
136): Promise<Response> {
137 let verdict: unknown = null;
138 try {
139 verdict = ((await request.json()) as { verdict?: unknown }).verdict ?? null;
140 } catch {
141 // A report without its metrics still stops the run.
142 }
143 try {
144 const namespace = sandboxNamespace(env, ctx.className);
145 const sandbox = namespace.get(namespace.idFromString(ctx.containerId)) as unknown as {
146 flagAbuse(verdict: unknown): Promise<void>;
147 };
148 await sandbox.flagAbuse(verdict);
149 } catch (error) {
150 console.log("abuse report not handled", String(error));
151 }
152 return new Response("noted\n");
153}
154
155/** Adds a step to a run, with its token. Never fails the caller. */
156export async function reportRun(
157 work: ServiceBinding,
158 tracked: { runId: string; token: string },
159 report: { steps?: string[]; halt?: "budget" | "time" | "abuse"; error?: string },
160): Promise<void> {
161 await work
162 .fetch("https://service/rpc/report_run", {
163 method: "POST",
164 headers: { "content-type": "application/json" },
165 body: JSON.stringify({ runId: tracked.runId, token: tracked.token, ...report }),
166 })
167 .catch((error: unknown) => console.log("run report failed", tracked.runId, String(error)));
168}
169