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

Commit

The site stops promising what g1t no longer does. The security page says jobs, checks and builds act with a token made for that job alone (its repository only, its permissions only, revoked when it ends), where it described credentials for agent runs that review or push to a fork; coding agents you connect use your own tokens; guardrails are time caps on checks, the merge queue and merge checks, with branch protection as the hard boundary, not a command hook, No sudo and spend caps; and the model proxy is no longer among what is reachable from outside. The landing page drops projects that depend on each other, a ruleset's bypass list says g1t itself (the merge queue, security updates) where it said g1t's agents, the security plan card no longer charges fixes by g1t's agent as agent usage, and Your AI providers says Ask g1t answers with one and an Anthropic key starts sessions on issues. Dead code goes: the unused marketing illustrations, lib/trial.ts (the free allowance on g1t's models), the run and issue spend caps' form parser, the dollar-cap helpers of guardrails and the wiggle animation of agent templates, with the tests that only tested them. Checked: the site's typecheck, its 907 tests and a production build.

syntaqxcommitted Parentd6d966fBrowse files
13 files+18−5520/13 viewed
+0−11
6161
6262 --animate-fade-up: fade-up 0.6s ease-out both;
6363 --animate-dash: dash 1.2s linear infinite;
64− /* A template's creature, on hover: a small, quick wiggle (components/agent-avatar.tsx). */
65− --animate-wiggle: wiggle 0.45s ease-in-out;
6664
6765 /* Menus, popovers and dialogs (components/ui): in quickly, out quicker. */
6866 --animate-pop-in: pop-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
178176 from {
179177 opacity: 0.4;
180178 transform: translateY(8px);
181− }
182−}
183−
184−@keyframes wiggle {
185− 25% {
186− transform: rotate(-8deg);
187− }
188− 60% {
189− transform: rotate(6deg);
190179 }
191180 }
192181
+0−400
1−/**
2− * Line-art illustrations for the marketing pages. They are plain SVG with
3− * SMIL animation, so they render on the server and move without any script.
4− */
5−
6−const LINE = "var(--color-line-strong)";
7−const FAINT = "var(--color-faint)";
8−const MUTED = "var(--color-muted)";
9−const ACCENT = "var(--color-accent)";
10−const DANGER = "var(--color-danger)";
11−const SURFACE = "var(--color-surface)";
12−const RAISED = "var(--color-raised)";
13−
14−/** A dot that travels along `path` forever. */
15−function Pulse({
16− path,
17− dur,
18− begin = "0s",
19− color = ACCENT,
20− r = 3,
21−}: {
22− path: string;
23− dur: string;
24− begin?: string;
25− color?: string;
26− r?: number;
27−}) {
28− return (
29− <circle r={r} fill={color}>
30− <animateMotion path={path} dur={dur} begin={begin} repeatCount="indefinite" />
31− <animate
32− attributeName="opacity"
33− values="0;1;1;0"
34− keyTimes="0;0.1;0.85;1"
35− dur={dur}
36− begin={begin}
37− repeatCount="indefinite"
38− />
39− </circle>
40− );
41−}
42−
43−const LANES = [
44− { y: 60, state: "passed" },
45− { y: 135, state: "failed" },
46− { y: 210, state: "merged" },
47− { y: 285, state: "passed" },
48− { y: 360, state: "working" },
49−] as const;
50−
51−/**
52− * The hero: one issue fans out to five pull requests, each in its own
53− * lane; the one that is merged lands on main.
54− */
55−export function HeroIllustration() {
56− return (
57− <svg
58− viewBox="0 0 1000 420"
59− fill="none"
60− className="w-full"
61− role="img"
62− aria-label="An issue fans out to five pull requests being worked on in parallel; one is merged into main."
63− >
64− {/* Faint grid. */}
65− <g stroke={LINE} strokeWidth="1" opacity="0.35">
66− {[60, 135, 210, 285, 360].map((y) => (
67− <path key={y} d={`M0 ${y}H1000`} strokeDasharray="2 10" />
68− ))}
69− {[250, 400, 550, 700].map((x) => (
70− <path key={x} d={`M${x} 20V400`} strokeDasharray="2 10" />
71− ))}
72− </g>
73−
74− {/* The issue. */}
75− <g>
76− <rect x="24" y="150" width="170" height="120" rx="12" fill={SURFACE} stroke={LINE} />
77− <text x="42" y="178" fill={MUTED} fontSize="11" fontFamily="var(--font-mono)">
78− issue #12
79− </text>
80− <rect x="42" y="190" width="120" height="7" rx="3.5" fill={FAINT} />
81− <rect x="42" y="206" width="134" height="5" rx="2.5" fill={LINE} />
82− <rect x="42" y="218" width="104" height="5" rx="2.5" fill={LINE} />
83− <g fontSize="10" fontFamily="var(--font-mono)" fill={MUTED}>
84− <path d="M43 245l3 3 6-6" stroke={ACCENT} strokeWidth="1.5" strokeLinecap="round" />
85− <text x="58" y="250">
86− cargo test
87− </text>
88− </g>
89− <circle cx="194" cy="210" r="4" fill={SURFACE} stroke={MUTED} strokeWidth="1.5" />
90− </g>
91−
92− {LANES.map(({ y, state }, i) => {
93− const out = `M194 210C250 210 250 ${y} 310 ${y}`;
94− const lane = `M310 ${y}H690`;
95− const back = `M690 ${y}C750 ${y} 750 210 806 210`;
96− const merges = state === "merged";
97− const failed = state === "failed";
98− const working = state === "working";
99− const stroke = merges ? ACCENT : failed ? LINE : FAINT;
100− const end = working ? 520 : failed ? 480 : 690;
101− return (
102− <g key={y}>
103− <path d={out} stroke={stroke} strokeWidth="1.5" />
104− <path
105− d={`M310 ${y}H${end}`}
106− stroke={stroke}
107− strokeWidth="1.5"
108− strokeDasharray={failed ? "4 5" : undefined}
109− />
110− {working && (
111− <path
112− d={`M${end} ${y}H690`}
113− stroke={LINE}
114− strokeWidth="1.5"
115− strokeDasharray="4 4"
116− className="animate-dash"
117− />
118− )}
119− {!failed && !working && (
120− <path
121− d={back}
122− stroke={stroke}
123− strokeWidth="1.5"
124− strokeDasharray={merges ? undefined : "3 6"}
125− opacity={merges ? 1 : 0.5}
126− />
127− )}
128−
129− <text x="310" y={y - 14} fill={merges ? ACCENT : MUTED} fontSize="11" fontFamily="var(--font-mono)">
130− #{13 + i}
131− </text>
132−
133− {/* Commits along the lane. */}
134− {[350, 410, 470, 560, 630]
135− .filter((x) => x <= end)
136− .map((x) => (
137− <circle
138− key={x}
139− cx={x}
140− cy={y}
141− r="4"
142− fill={SURFACE}
143− stroke={merges ? ACCENT : MUTED}
144− strokeWidth="1.5"
145− />
146− ))}
147−
148− {failed && (
149− <path
150− d={`M${end + 6} ${y - 5}l10 10m0-10l-10 10`}
151− stroke={DANGER}
152− strokeWidth="1.5"
153− strokeLinecap="round"
154− />
155− )}
156− {working && (
157− <circle cx={end} cy={y} r="5" fill={ACCENT}>
158− <animate attributeName="r" values="4;7;4" dur="1.6s" repeatCount="indefinite" />
159− <animate attributeName="opacity" values="1;0.4;1" dur="1.6s" repeatCount="indefinite" />
160− </circle>
161− )}
162−
163− <Pulse
164− path={`${out}H${end}`}
165− dur={`${3.2 + i * 0.35}s`}
166− begin={`${i * 0.5}s`}
167− color={merges ? ACCENT : MUTED}
168− />
169− {merges && <Pulse path={`${lane}${back.replace("M690 " + y, "")}`} dur="2.4s" begin="1.2s" r={3.5} />}
170− </g>
171− );
172− })}
173−
174− {/* Main. */}
175− <g>
176− <circle cx="840" cy="210" r="34" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" />
177− <circle cx="840" cy="210" r="34" stroke={ACCENT} strokeWidth="1.5" opacity="0.5">
178− <animate attributeName="r" values="34;50" dur="2.4s" repeatCount="indefinite" />
179− <animate attributeName="opacity" values="0.5;0" dur="2.4s" repeatCount="indefinite" />
180− </circle>
181− <path
182− d="M826 210l10 10 18-20"
183− stroke={ACCENT}
184− strokeWidth="2.5"
185− strokeLinecap="round"
186− strokeLinejoin="round"
187− />
188− <path d="M874 210H976" stroke={ACCENT} strokeWidth="1.5" />
189− <text x="902" y="198" fill={ACCENT} fontSize="12" fontFamily="var(--font-mono)">
190− main
191− </text>
192− {[910, 945].map((x) => (
193− <circle key={x} cx={x} cy="210" r="4" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" />
194− ))}
195− </g>
196− </svg>
197− );
198−}
199−
200−/** An isometric tile, as a path centred on (cx, cy). */
201−function tile(cx: number, cy: number, w = 60, h = 30): string {
202− return `M${cx} ${cy - h}L${cx + w} ${cy}L${cx} ${cy + h}L${cx - w} ${cy}Z`;
203−}
204−
205−/** An issue with its checks ticking off. */
206−export function IssueIllustration() {
207− return (
208− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
209− <path d="M0 150H320" stroke={LINE} strokeDasharray="2 8" />
210− <rect x="70" y="22" width="180" height="136" rx="10" fill={RAISED} stroke={LINE} />
211− <rect x="90" y="42" width="96" height="8" rx="4" fill={MUTED} />
212− <rect x="90" y="62" width="140" height="5" rx="2.5" fill={LINE} />
213− <rect x="90" y="74" width="120" height="5" rx="2.5" fill={LINE} />
214− <rect x="90" y="86" width="132" height="5" rx="2.5" fill={LINE} />
215− {[110, 128, 146].map((y, i) => (
216− <g key={y}>
217− <rect x="90" y={y - 7} width="12" height="12" rx="3" stroke={FAINT} />
218− <path
219− d={`M93 ${y - 1}l2.5 2.5 4.5-5`}
220− stroke={ACCENT}
221− strokeWidth="1.5"
222− strokeLinecap="round"
223− opacity="0"
224− >
225− <animate
226− attributeName="opacity"
227− values="0;0;1;1;0"
228− keyTimes={`0;${0.15 + i * 0.2};${0.2 + i * 0.2};0.95;1`}
229− dur="5s"
230− repeatCount="indefinite"
231− />
232− </path>
233− <rect x="110" y={y - 3} width={90 - i * 14} height="5" rx="2.5" fill={FAINT} />
234− </g>
235− ))}
236− </svg>
237− );
238−}
239−
240−/** One repository lifting into a stack of separate copies. */
241−export function ForkIllustration() {
242− return (
243− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
244− <path d="M40 150L160 90L280 150" stroke={LINE} strokeDasharray="2 8" />
245− <path d={tile(160, 138)} fill={RAISED} stroke={MUTED} />
246− {[0, 1, 2].map((i) => (
247− <g key={i}>
248− <path
249− d={tile(160, 138)}
250− fill={SURFACE}
251− stroke={i === 1 ? ACCENT : FAINT}
252− opacity="0"
253− >
254− <animateTransform
255− attributeName="transform"
256− type="translate"
257− values={`0 0;${(i - 1) * 84} ${-58 - (i === 1 ? 22 : 0)};${(i - 1) * 84} ${-58 - (i === 1 ? 22 : 0)};0 0`}
258− keyTimes="0;0.3;0.85;1"
259− dur="5s"
260− begin={`${i * 0.15}s`}
261− repeatCount="indefinite"
262− />
263− <animate
264− attributeName="opacity"
265− values="0;1;1;0"
266− keyTimes="0;0.15;0.85;1"
267− dur="5s"
268− begin={`${i * 0.15}s`}
269− repeatCount="indefinite"
270− />
271− </path>
272− </g>
273− ))}
274− <path d="M160 138V168M100 138V160M220 138V160" stroke={LINE} />
275− </svg>
276− );
277−}
278−
279−/** A line of code linked to the reasoning that produced it. */
280−export function SessionIllustration() {
281− return (
282− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
283− <rect x="20" y="30" width="150" height="120" rx="10" fill={RAISED} stroke={LINE} />
284− {[52, 68, 84, 100, 116, 132].map((y, i) => (
285− <g key={y}>
286− <rect x="34" y={y - 3} width="8" height="5" rx="2" fill={LINE} />
287− <rect
288− x="50"
289− y={y - 3}
290− width={[80, 104, 64, 96, 72, 88][i]}
291− height="5"
292− rx="2.5"
293− fill={i === 3 ? ACCENT : FAINT}
294− opacity={i === 3 ? 0.9 : 0.6}
295− />
296− </g>
297− ))}
298− <rect x="28" y="92" width="134" height="16" rx="4" stroke={ACCENT} opacity="0.5" />
299− <path d="M162 100H196" stroke={ACCENT} strokeDasharray="3 4" className="animate-dash" />
300− <rect x="196" y="58" width="108" height="84" rx="10" fill={SURFACE} stroke={ACCENT} strokeOpacity="0.6" />
301− <circle cx="212" cy="76" r="5" fill={ACCENT} />
302− <rect x="224" y="73" width="52" height="5" rx="2.5" fill={MUTED} />
303− <rect x="208" y="92" width="84" height="5" rx="2.5" fill={FAINT} />
304− <rect x="208" y="104" width="70" height="5" rx="2.5" fill={FAINT} />
305− <rect x="208" y="116" width="78" height="5" rx="2.5" fill={FAINT} />
306− </svg>
307− );
308−}
309−
310−/** Any agent, anywhere, joining through one endpoint. */
311−export function AgentsIllustration() {
312− const spokes = [
313− { x: 50, y: 40 },
314− { x: 50, y: 140 },
315− { x: 270, y: 40 },
316− { x: 270, y: 140 },
317− ];
318− return (
319− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
320− {spokes.map(({ x, y }, i) => {
321− const path = `M${x} ${y}L160 90`;
322− return (
323− <g key={`${x}-${y}`}>
324− <path d={path} stroke={LINE} />
325− <rect x={x - 26} y={y - 16} width="52" height="32" rx="6" fill={RAISED} stroke={FAINT} />
326− <path d={`M${x - 16} ${y - 4}l5 4-5 4`} stroke={MUTED} strokeWidth="1.5" strokeLinecap="round" />
327− <rect x={x - 5} y={y + 2} width="14" height="3" rx="1.5" fill={FAINT} />
328− <Pulse path={path} dur="2.4s" begin={`${i * 0.6}s`} />
329− </g>
330− );
331− })}
332− <circle cx="160" cy="90" r="26" fill={SURFACE} stroke={ACCENT} />
333− <text x="160" y="94" textAnchor="middle" fill={ACCENT} fontSize="11" fontFamily="var(--font-mono)">
334− mcp
335− </text>
336− </svg>
337− );
338−}
339−
340−/** Every change published once and delivered to each subscriber. */
341−export function EventsIllustration() {
342− const sinks = [50, 90, 130];
343− return (
344− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
345− <path d="M20 90H150" stroke={FAINT} />
346− {[0, 1, 2].map((i) => (
347− <Pulse key={i} path="M20 90H150" dur="1.8s" begin={`${i * 0.6}s`} color={MUTED} />
348− ))}
349− <rect x="150" y="68" width="44" height="44" rx="10" fill={RAISED} stroke={ACCENT} />
350− <path d="M163 90h18m-6-6l6 6-6 6" stroke={ACCENT} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
351− {sinks.map((y, i) => {
352− const path = `M194 90C230 90 220 ${y} 256 ${y}`;
353− return (
354− <g key={y}>
355− <path d={path} stroke={LINE} />
356− <rect x="256" y={y - 12} width="44" height="24" rx="6" fill={SURFACE} stroke={FAINT} />
357− <rect x="266" y={y - 2.5} width="24" height="5" rx="2.5" fill={FAINT} />
358− <Pulse path={path} dur="1.8s" begin={`${0.9 + i * 0.3}s`} />
359− </g>
360− );
361− })}
362− </svg>
363− );
364−}
365−
366−/** Pull requests queued in order, landing on main one at a time. */
367−export function MergeIllustration() {
368− return (
369− <svg viewBox="0 0 320 180" fill="none" className="w-full" aria-hidden="true">
370− <path d="M20 120H300" stroke={ACCENT} strokeWidth="1.5" />
371− {[60, 110, 160].map((x) => (
372− <circle key={x} cx={x} cy="120" r="4" fill={SURFACE} stroke={ACCENT} strokeWidth="1.5" />
373− ))}
374− {[0, 1, 2].map((i) => {
375− const x = 200 + i * 34;
376− return (
377− <g key={i}>
378− <rect
379− x={x - 12}
380− y={50 - i * 4}
381− width="24"
382− height="24"
383− rx="6"
384− fill={RAISED}
385− stroke={i === 0 ? ACCENT : FAINT}
386− >
387− {i === 0 && (
388− <animate attributeName="y" values="50;108;108;50" keyTimes="0;0.4;0.9;1" dur="4s" repeatCount="indefinite" />
389− )}
390− </rect>
391− {i > 0 && <path d={`M${x} ${78 - i * 4}V120`} stroke={LINE} strokeDasharray="2 5" />}
392− </g>
393− );
394− })}
395− <text x="20" y="144" fill={MUTED} fontSize="11" fontFamily="var(--font-mono)">
396− main
397− </text>
398− </svg>
399− );
400−}
+0−1
561561 "A live preview for every pull request",
562562 "Production on every merge to main",
563563 "Custom domains, with certificates",
564− "Projects that depend on each other",
565564 ]}
566565 more={["Deployments", `${DOCS}/guides/deployments/`]}
567566 >
+2−2
729729 onChange={(value) => update(index, { value })}
730730 />
731731 ) : actor.kind === "g1t" ? (
732− <span className="grow text-sm text-muted">g1t's agents and g1t itself (the merge queue, security updates)</span>
732+ <span className="grow text-sm text-muted">g1t itself (the merge queue, security updates)</span>
733733 ) : (
734734 <Input aria-label="Who, by name" value={actor.value} disabled={disabled} placeholder={KINDS.find((kind) => kind.value === actor.kind)?.placeholder} className="w-48 grow" onChange={(event) => update(index, { value: event.target.value })} />
735735 )}
754754 </ul>
755755 {hasG1t && (
756756 <p className="flex items-start gap-2 text-xs text-warn">
757− <TriangleAlert size={13} className="mt-px shrink-0" /> g1t's agents bypass these rules. Agents obey rules exactly as people do unless they are listed here.
757+ <TriangleAlert size={13} className="mt-px shrink-0" /> g1t itself (the merge queue, security updates) bypasses these rules. Coding agents obey rules exactly as the people they act for do.
758758 </p>
759759 )}
760760 {!disabled && (
+1−2
7979 <p className="mt-1.5 text-sm text-muted">
8080 Security and quality has no price of its own. With the plan{price ? ` (${price} for the workspace)` : ""}, custom patterns,
8181 validity checks, delegated bypass, code scanning, dependency review and the security overview are on for every private
82− repository in {workspace}, and their scans are charged at cost plus 20%, like everything g1t runs. Fixes by g1t's agent
83− are charged as agent usage. Public repositories have all of it free, and secret scanning, push protection, vulnerability
82+ repository in {workspace}, and their scans are charged at cost plus 20%, like everything g1t runs. Public repositories have all of it free, and secret scanning, push protection, vulnerability
8483 alerts and security updates stay free everywhere.
8584 </p>
8685 <div className="mt-3 flex flex-wrap items-center gap-3 text-sm">
+13−19
66 - **Your tools never see your password.** The CLI, coding agents and MCP clients sign in through your browser, with device sign-in or OAuth with PKCE, and get a token you can revoke from your settings. Access tokens are stored only as hashes.
77 - **New accounts confirm their email address** before they can create repositories or push.
88
9−## Agents act with credentials of their own
9+## Jobs act with credentials of their own
1010
11−Every agent run gets **credentials made for that run alone**, created when it starts and revoked the moment it stops, and never valid for more than two hours. They're not your access tokens:
11+Every workflow job, check and build gets **a token made for that job alone**, revoked the moment it ends. It's not your access token:
1212
13−- They're bound to the run's repository and to what that kind of run needs. A review can't push; an implementation can push only to its own pull request's fork.
14−- They act **on behalf of the person who started the work**, and only while that person is still a member of the workspace. An owner's agent is only ever a member.
15−- They can never touch a workspace's settings, members, tokens, billing, integrations, webhooks, secrets, or other repositories, and they can't merge a pull request.
13+- It reaches **its own repository only**, and does only what the job's `permissions:` allow.
14+- What it changes is recorded in the audit log as that job's, and starts no workflows of its own.
15+- A pull request from a fork, or from someone without write access, runs without secrets and with a token that can only read.
1616
17−The runner that clones and pushes holds one credential; the agent's own tools hold another, which can't be used with git at all. Both are removed from everything recorded about the run.
17+Coding agents you connect use your own access tokens or an OAuth sign-in, limited to the scopes and repositories you chose, and you can revoke them from your settings at any time.
1818
1919 ## Every action is recorded
2020
21−The **audit log** records every action through the API, the MCP server and git, by people and by agents: who did it, on whose behalf, what, to what, and whether it was allowed or refused, with the rule that decided. It's kept for 90 days on every plan, free or paid, and can be exported as CSV or JSON. Security is never a paid extra on g1t.
21+The **audit log** records every action through the API, the MCP server and git, by people and by the agents and jobs acting for them: who did it, on whose behalf, what, to what, and whether it was allowed or refused, with the rule that decided. It's kept for 90 days on every plan, free or paid, and can be exported as CSV or JSON. Security is never a paid extra on g1t.
2222
23−## Guardrails on what agents can do
23+## Guardrails on what runs
2424
25−Each workspace decides what its agents may do, and each project can tighten it:
26−
27−- **A command hook.** The agent's harness checks every tool call before it runs, and refuses force-pushes, rewriting the default branch, reading files outside the project, printing the environment, `sudo`, and any rules you add. With **No sudo** on, the sandbox gives up root before the agent starts.
28−- **Caps** on what one run may spend and how long it may take, enforced by the harness and, for time, by stopping the sandbox itself.
29−- **Branch protection**, enforced by g1t when a push arrives, whatever happened in the sandbox.
30−
31−Command rules guard against an agent's mistakes; the run's credentials and branch protection are the hard boundaries. The [guardrails guide](https://docs.g1t.sh/guides/guardrails/) explains each one and how it's enforced.
25+Each workspace decides how long its checks, merge queue and merge checks may run in g1t's sandboxes, and each project can tighten it. A run that passes its time cap is stopped by stopping the sandbox itself, and **branch protection** is enforced by g1t when a push arrives, whatever happened in the sandbox. The [guardrails guide](https://docs.g1t.sh/guides/guardrails/) explains each one.
3226
3327 ## Isolated sandboxes
3428
35−Agents, checks, workflows and builds each run in **a container of their own** on Cloudflare Containers, with one checkout and its run's credentials, and nothing from any other run or workspace. Each is stopped when its work ends, and when its time cap passes, whatever is running in it. A pull request from a fork is checked out without loading that fork's agent settings, hooks or tools.
29+Checks, workflows and builds each run in **a container of their own** on Cloudflare Containers, with one checkout and its job's token, and nothing from any other run or workspace. Each is stopped when its work ends, and when its time cap passes, whatever is running in it.
3630
3731 ## Secrets stay secret
3832
4438
4539 ## Built on Cloudflare
4640
47−g1t runs on Cloudflare's network: Workers for the site, API and services, D1 for databases, Artifacts for git storage, Containers for sandboxes. Everything is served over HTTPS. The services behind the site talk to each other over Cloudflare service bindings, not the public internet, and only the site, the API, the MCP server, the model proxy, link previews (og.g1t.sh), the documentation, the status page and g1t.page are reachable from outside. Uploaded images are served with a policy that lets nothing in them run.
41+g1t runs on Cloudflare's network: Workers for the site, API and services, D1 for databases, Artifacts for git storage, Containers for sandboxes. Everything is served over HTTPS. The services behind the site talk to each other over Cloudflare service bindings, not the public internet, and only the site, the API, the MCP server, link previews (og.g1t.sh), the documentation, the status page and g1t.page are reachable from outside. Uploaded images are served with a policy that lets nothing in them run.
4842
4943 Staff access to accounts and billing goes through an internal tool behind Cloudflare Access. Every change made there is recorded with who made it and why.
5044
5448
5549 Free compute attracts people who would mine cryptocurrency with it, and stolen cards. g1t guards against both without making honest use harder:
5650
57−- **No card, no compute.** Agents and sandboxes need a card check or the plan. The check is never charged; each card gets one trial, wherever it's used; prepaid cards start no trial.
51+- **No card, no compute.** Sandboxes need a card check or the plan. The check is never charged; each card gets one trial, wherever it's used; prepaid cards start no trial.
5852 - **Mining detection.** Commands that name known miners, pools or their flags are refused, and a sandbox that stays at full CPU for ten minutes without the disk, network and process activity of real builds is stopped, and a person looks at what ran.
5953 - **Spend limits and spike pauses.** Every workspace has a spend limit and a ceiling on unpaid usage, and if spending suddenly jumps, new compute pauses until an owner says to keep going.
6054
7468
7569 - g1t.sh, api.g1t.sh, mcp.g1t.sh, og.g1t.sh, docs.g1t.sh and status.g1t.sh;
7670 - the g1t.page platform itself: routing, isolation between apps, custom domains;
77−- the sandboxes: escaping one, reaching another run or workspace, or getting around guardrails or run credentials;
71+- the sandboxes: escaping one, reaching another run or workspace, or getting around guardrails or a job's token;
7872 - authentication, authorization, the API, the MCP server and git over HTTPS;
7973 - the [open-source code](/flagon-io/g1t), as deployed.
8074
+0−12
1313 creditLine,
1414 needsAttention,
1515 usageGlance,
16− parseCaps,
1716 parseLimitRequest,
1817 parsePrepay,
1918 parseSpendLimit,
6766 value: { amountCents: 2_000_000, method: "bank_transfer" },
6867 });
6968 assert.equal(parsePrepay({}).ok, false);
70−});
71−
72−test("caps stay in range, and empty goes back to the default", () => {
73− assert.deepEqual(parseCaps({ run: "", issue: "" }), { ok: true, value: { runCapMicros: null, issueCapMicros: null } });
74− assert.deepEqual(parseCaps({ run: "0.10", issue: "1000" }), {
75− ok: true,
76− value: { runCapMicros: 100_000, issueCapMicros: 1_000_000_000 },
77− });
78− assert.equal(parseCaps({ run: "0.05" }).ok, false);
79− assert.equal(parseCaps({ issue: "2000" }).ok, false);
80− assert.equal(parseCaps({ run: "two" }).ok, false);
8169 });
8270
8371 test("a limit request needs an amount, a reason and a monthly spend; an overage only a reason", () => {
+0−25
7474 return { ok: true, value: { amountCents: Math.round(amount * 100), method } };
7575 }
7676
77−/** What owners may set each cap to, in micros, and what applies when they set none. */
78−export const CAPS = {
79− run: { min: 100_000, max: 100_000_000, default: 2_000_000 },
80− issue: { min: 1_000_000, max: 1_000_000_000, default: 10_000_000 },
81−} as const;
82−
83−/** The owners' caps: empty goes back to the default. */
84−export function parseCaps(form: {
85− run?: FormDataEntryValue | null;
86− issue?: FormDataEntryValue | null;
87−}): Parsed<{ runCapMicros: number | null; issueCapMicros: number | null }> {
88− const run = readDollars(form.run);
89− const issue = readDollars(form.issue);
90− if (String(form.run ?? "").trim() && run == null) return { ok: false, error: "A run's cap is a dollar amount." };
91− if (String(form.issue ?? "").trim() && issue == null) return { ok: false, error: "An issue's cap is a dollar amount." };
92− if (run != null && (run < CAPS.run.min || run > CAPS.run.max))
93− return { ok: false, error: `A run's cap is between ${wholeDollars(CAPS.run.min)} and ${wholeDollars(CAPS.run.max)}.` };
94− if (issue != null && (issue < CAPS.issue.min || issue > CAPS.issue.max))
95− return {
96− ok: false,
97− error: `An issue's cap is between ${wholeDollars(CAPS.issue.min)} and ${wholeDollars(CAPS.issue.max)}.`,
98− };
99− return { ok: true, value: { runCapMicros: run, issueCapMicros: issue } };
100−}
101−
10277 /** A request for a higher limit, or for help with usage past what was meant. */
10378 export function parseLimitRequest(form: {
10479 kind?: FormDataEntryValue | null;
+1−12
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { capShare, formatCap, settingsFromForm } from "./guardrails.ts";
4+import { settingsFromForm } from "./guardrails.ts";
55
66 const catalog = { kinds: ["implement", "review"] as const };
77
3232 test("nonsense is passed on for the service to refuse", () => {
3333 const settings = settingsFromForm(form({ "minutes:implement": "soon" }), catalog);
3434 assert.equal(settings.minutes?.implement, 0);
35−});
36−
37−test("a cap reads as dollars, or no cap", () => {
38− assert.equal(formatCap(null), "no cap");
39− assert.equal(formatCap(5), "$5.00");
40−});
41−
42−test("helpers", () => {
43− assert.equal(capShare(1, 4), 0.25);
44− assert.equal(capShare(9, 4), 1);
45− assert.equal(capShare(1, null), null);
4635 });
+0−12
11 import type { GuardrailSettings, RunKind } from "@g1t/contracts";
2−import { microsOf, money } from "./money.ts";
32
43 /** A number from a field; empty means inherit. Not a number is kept, to be refused. */
54 function amount(value: FormDataEntryValue | null): number | null {
2322 if (cap != null) minutes[kind] = Number.isNaN(cap) ? 0 : Math.trunc(cap);
2423 }
2524 return { minutes };
26−}
27−
28−/** A cost in dollars, or "no cap". */
29−export function formatCap(usd: number | null | undefined): string {
30− return usd == null ? "no cap" : money(microsOf(usd));
31−}
32−
33−/** How full a cap is, from 0 to 1, or null with no cap. */
34−export function capShare(used: number | null | undefined, cap: number | null | undefined): number | null {
35− if (cap == null || cap <= 0 || used == null) return null;
36− return Math.min(1, Math.max(0, used / cap));
3725 }
+0−26
1−import assert from "node:assert/strict";
2−import { test } from "node:test";
3−
4−import { trialClosed } from "./trial.ts";
5−
6−const base = { usedMicros: 0, limitMicros: 1_000_000, endsAt: null };
7−
8−test("an open trial says nothing", () => {
9− assert.equal(trialClosed({ ...base, open: true, reason: null }, "acme"), null);
10− assert.equal(trialClosed(null, "acme"), null);
11−});
12−
13−test("a spent trial and a given-out month say so", () => {
14− assert.equal(trialClosed({ ...base, open: false, reason: "used" }, "acme"), "acme has used its trial credit on g1t.");
15− assert.equal(
16− trialClosed({ ...base, open: false, reason: "pool", waitsUntil: "2026-11-01T00:00:00Z" }, "acme"),
17− "This month's free trials are all given out; new ones start on November 1.",
18− );
19−});
20−
21−test("a workspace without a card check is told it needs one", () => {
22− assert.equal(
23− trialClosed({ ...base, open: false, reason: "verify" as never }, "acme"),
24− "acme needs a card check before its trial starts. The card is never charged.",
25− );
26−});
+0−29
1−/**
2− * How the site speaks of a workspace's trial credit. Pure, so it can be
3− * tested.
4− */
5−
6−import type { Trial } from "@g1t/contracts";
7−
8−/** "November 1", in UTC, when new trials start again. */
9−export function resumesOn(at: string): string {
10− return new Date(at).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone: "UTC" });
11−}
12−
13−/**
14− * Why the trial cannot pay for anything now, in a sentence, or null when it
15− * can: no card check yet (`verify`), used, or this month's pool given out.
16− */
17−export function trialClosed(trial: Trial | null | undefined, workspace: string): string | null {
18− if (!trial || trial.open) return null;
19− // `verify` is newer than the contract's list of reasons: the trial waits for a card check.
20− if ((trial.reason as string | null) === "verify")
21− return `${workspace} needs a card check before its trial starts. The card is never charged.`;
22− if (trial.reason === "used") return `${workspace} has used its trial credit on g1t.`;
23− if (trial.reason === "pool")
24− return trial.waitsUntil
25− ? `This month's free trials are all given out; new ones start on ${resumesOn(trial.waitsUntil)}.`
26− : "This month's free trials are all given out; new ones start next month.";
27− if (trial.reason === "ended") return "The free allowance on g1t's models has ended.";
28− return null;
29−}
+1−1
161161 models: {
162162 title: "Your AI providers",
163163 icon: <Cpu size={16} />,
164− blurb: "Connect the AI providers your team uses, so g1t can hand work to them.",
164+ blurb: "Connect the AI providers your team uses: Ask g1t answers with one, and an Anthropic key starts sessions on issues.",
165165 },
166166 alerts: {
167167 title: "Alerts",