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
159 linesCodeBlameRaw
1/**
2 * What a sandbox's guardrails come to inside it, and the host names its
3 * outbound requests are compared by. Pure, so it can be tested on its
4 * own; guard.ts applies it.
5 *
6 * Every sandbox reaches the internet, as a CI machine does. Its requests
7 * go straight out, except where the Worker has to see them: a workflow
8 * job that may be given brokered secrets sends its HTTP and HTTPS through
9 * the Worker (Cloudflare Containers' outbound interception), which fills
10 * the secrets in (broker-egress.ts), and every sandbox's report that it
11 * stopped itself for mining (`ABUSE_HOST`) is answered by its own Durable
12 * Object.
13 */
14
15import type { Guardrails } from "@g1t/contracts";
16
17/** A host name as it is compared: lower case, no port, no trailing dot. */
18export function normalizeHost(host: string): string {
19 let name = host.trim().toLowerCase();
20 // An IPv6 literal keeps its colons.
21 if (name.startsWith("[")) return name.slice(0, name.indexOf("]") + 1);
22 const colon = name.indexOf(":");
23 if (colon >= 0) name = name.slice(0, colon);
24 return name.replace(/\.+$/, "");
25}
26
27/**
28 * Whether `host` is on the list. `example.com` matches exactly that host;
29 * `*.example.com` matches its subdomains, at any depth, but not
30 * `example.com` itself. Used for the hosts a brokered secret is for.
31 */
32export function allows(patterns: readonly string[], host: string): boolean {
33 const name = normalizeHost(host);
34 if (!name) return false;
35 return patterns.some((raw) => {
36 const pattern = raw.trim().toLowerCase();
37 if (pattern.startsWith("*.")) {
38 const suffix = pattern.slice(1);
39 return name.length > suffix.length && name.endsWith(suffix);
40 }
41 return pattern === name;
42 });
43}
44
45/**
46 * The variables a sandbox whose HTTPS passes the Worker needs, so that its
47 * programs trust the certificate HTTPS is re-signed with there. The runner
48 * adds that certificate to the system's store when it starts; these point
49 * the tools that keep their own list at it.
50 */
51export const EGRESS_CA = "/etc/cloudflare/certs/cloudflare-containers-ca.crt";
52const SYSTEM_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
53export const EGRESS_ENV: Record<string, string> = {
54 G1T_EGRESS_CA: EGRESS_CA,
55 NODE_EXTRA_CA_CERTS: EGRESS_CA,
56 SSL_CERT_FILE: SYSTEM_BUNDLE,
57 REQUESTS_CA_BUNDLE: SYSTEM_BUNDLE,
58 PIP_CERT: SYSTEM_BUNDLE,
59 CURL_CA_BUNDLE: SYSTEM_BUNDLE,
60 CARGO_HTTP_CAINFO: SYSTEM_BUNDLE,
61 GIT_SSL_CAINFO: SYSTEM_BUNDLE,
62};
63
64/** What a sandbox's guardrails come to for one run. */
65export type RunGuard = {
66 policy: Guardrails;
67 /** The time cap of this kind of run, in minutes. */
68 minutes: number;
69};
70
71/** What the harness is told about the run's guardrails. */
72export function harnessEnv(guard: RunGuard, sandboxEnv: Record<string, string>): Record<string, string> {
73 return {
74 GUARDRAILS: JSON.stringify({
75 minutes: guard.minutes,
76 defaultBranch: sandboxEnv.UPSTREAM_BRANCH ?? null,
77 }),
78 };
79}
80
81/** What a run stopped by its time cap is told. */
82export function timeCapMessage(minutes: number): string {
83 return `Stopped: it reached its time cap of ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`;
84}
85
86/**
87 * Where a sandbox tells the runner it stopped itself for mining
88 * (crates/runner abuse.rs). The runner's Durable Object answers it; it
89 * never leaves the machine.
90 */
91export const ABUSE_HOST = "sandbox.g1t.internal";
92/** What a sandbox that stopped itself for mining exits with. */
93export const ABUSE_EXIT_CODE = 86;
94/** What such a run, check, job or build says. */
95export const ABUSE_MESSAGE = "Stopped: unusual CPU use; contact support if this was a real job.";
96
97/**
98 * Whether a workflow job gets a Docker Engine of its own, from the
99 * Worker's `DOCKER` setting: on unless it says `off`. Its containers share
100 * the job's network.
101 */
102export function dockerFor(setting: string | undefined): "on" | "off" {
103 return setting?.trim().toLowerCase() === "off" ? "off" : "on";
104}
105
106/**
107 * Whether a sandbox sends its HTTP and HTTPS through the Worker: only a
108 * workflow job that may be given brokered secrets, so they can be filled
109 * in, and not when the operator turned that off (`EGRESS` is `off`).
110 * Every other sandbox's requests go straight out.
111 */
112export function throughWorker(job: { brokered?: boolean } | null | undefined, setting: string | undefined): boolean {
113 return job?.brokered === true && setting?.trim().toLowerCase() !== "off";
114}
115
116/**
117 * The Durable Object namespace behind each sandbox class, by its binding:
118 * a sandbox's outbound handlers are told its class, and report back to its
119 * own object. Each label in the machine catalogue (`INSTANCE_TYPES`) has a
120 * class of its own (wrangler.jsonc), named by its `sandboxClass`.
121 */
122export const SANDBOX_BINDINGS: Record<string, string> = {
123 AttemptSandbox: "SANDBOX",
124 Sandbox2Core: "SANDBOX_2CORE",
125 Sandbox4Core: "SANDBOX_4CORE",
126};
127
128/** The namespace of a sandbox of `className`; the standard one when unknown. */
129export function sandboxNamespace(env: object, className: string | undefined): DurableObjectNamespace {
130 const bindings = env as Record<string, DurableObjectNamespace | undefined>;
131 const binding = (className && SANDBOX_BINDINGS[className]) || "SANDBOX";
132 return (bindings[binding] ?? bindings.SANDBOX) as DurableObjectNamespace;
133}
134
135/**
136 * The namespace a workflow job on `instance` starts in: its catalogue class's
137 * binding. Null when the class or its binding is missing here (a self-hosted
138 * g1t that did not deploy that class), so the job says so rather than
139 * running on another machine.
140 */
141export function jobNamespaceOf(env: object, instance: { sandboxClass: string }): DurableObjectNamespace | null {
142 const binding = SANDBOX_BINDINGS[instance.sandboxClass];
143 return (binding && (env as Record<string, DurableObjectNamespace | undefined>)[binding]) || null;
144}
145
146/** The lower of two caps, where null, zero or less means no cap. */
147function lower(a: number | null | undefined, b: number | null | undefined): number | null {
148 const caps = [a, b].filter((cap): cap is number => typeof cap === "number" && Number.isFinite(cap) && cap > 0);
149 return caps.length ? Math.min(...caps) : null;
150}
151
152/** A plan's cap on one run, from its entitlements; null where it sets none. */
153export type PlanLimits = { minutes?: number | null };
154
155/** A run's guardrails under its workspace's plan: the time cap is the lower of the two. */
156export function withPlanLimits(guard: RunGuard, limits: PlanLimits | null | undefined): RunGuard {
157 if (!limits) return guard;
158 return { policy: guard.policy, minutes: lower(guard.minutes, limits.minutes) ?? guard.minutes };
159}