Skip to content
162 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.

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

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