Skip to content

Commit

Merge branch 'worktree-agent-ac950b9c891220ad3'

syntaqxcommitted Parents49e1e102fa2c7fBrowse files
47 files+18−56731 files not counted0/47 viewed
+1−10
11011101 "comment_count": 0,
11021102 "assignees": [],
11031103 "blocked_by": [],
1104− "queued": false,
11051104 "agent": "claude-code",
11061105 "milestone": null
11071106 }
11441143 "comment_count": 0,
11451144 "assignees": [],
11461145 "blocked_by": [],
1147− "queued": false,
11481146 "agent": null,
11491147 "milestone": null
11501148 },
11801178 "comment_count": 0,
11811179 "assignees": [],
11821180 "blocked_by": [],
1183− "queued": false,
11841181 "agent": null,
11851182 "milestone": null
11861183 },
13391336 "syntaqx"
13401337 ],
13411338 "blocked_by": [9],
1342− "queued": false,
13431339 "agent": null,
13441340 "milestone": {
13451341 "number": 3,
13791375 "comment_count": 0,
13801376 "assignees": [],
13811377 "blocked_by": [],
1382− "queued": false,
13831378 "agent": null,
13841379 "milestone": null
13851380 }
14131408 "comment_count": 0,
14141409 "assignees": [],
14151410 "blocked_by": [],
1416− "queued": false,
14171411 "agent": null,
14181412 "milestone": null
14191413 }
17011695 "comment_count": 0,
17021696 "assignees": [],
17031697 "blocked_by": [],
1704− "queued": false,
17051698 "agent": "claude-code",
17061699 "milestone": null
17071700 },
28552848 "blocked_by": [
28562849 9
28572850 ],
2858− "queued": false,
28592851 "agent": null,
28602852 "milestone": {
28612853 "number": 3,
42594251 "fork_pull_requests": false,
42604252 "inherited": false
42614253 },
4262− "notes": "Agent work on your runners still uses g1t's model proxy, paid as before, unless the workspace has its own model provider."
4254+ "notes": "`agents_on_self_hosted` and `agent_labels` keep their names from before g1t ran agents of its own: they now send g1t's own work (checks, merge checks and the merge queue) to your runners with those labels."
42634255 },
42644256 "list_runner_groups": {
42654257 "params": {
93109302 "syntaqx"
93119303 ],
93129304 "blocked_by": [],
9313− "queued": false,
93149305 "agent": null,
93159306 "milestone": {
93169307 "number": 3,
+0−0

Binary or large file; its contents are not shown.

+0−4
122122 ossRepoMicros: null,
123123 trialMicros: null,
124124 maxConcurrentAgents: null,
125− runCapMicros: null,
126− issueCapMicros: null,
127125 auditRetentionDays: null,
128126 hold: null,
129127 },
138136 trialMicros: 2_500_000,
139137 // Retired with g1t's own agents: always cleared, whatever an old form sends.
140138 maxConcurrentAgents: null,
141− runCapMicros: null,
142− issueCapMicros: null,
143139 auditRetentionDays: 365,
144140 hold: "Card disputed; waiting on the bank",
145141 },
+0−2
228228 ossRepoMicros: oss.value,
229229 trialMicros: trial.value,
230230 maxConcurrentAgents: null,
231− runCapMicros: null,
232− issueCapMicros: null,
233231 auditRetentionDays,
234232 hold: hold || null,
235233 },
+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−}
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…