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.
13 files+18−5520/13 viewed
| 61 | 61 | ||
| 62 | 62 | --animate-fade-up: fade-up 0.6s ease-out both; | |
| 63 | 63 | --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; | |
| 66 | 64 | ||
| 67 | 65 | /* Menus, popovers and dialogs (components/ui): in quickly, out quicker. */ | |
| 68 | 66 | --animate-pop-in: pop-in 0.16s cubic-bezier(0.16, 1, 0.3, 1); | |
| ⋯ | |||
| 178 | 176 | from { | |
| 179 | 177 | opacity: 0.4; | |
| 180 | 178 | transform: translateY(8px); | |
| 181 | − | } | |
| 182 | − | } | |
| 183 | − | ||
| 184 | − | @keyframes wiggle { | |
| 185 | − | 25% { | |
| 186 | − | transform: rotate(-8deg); | |
| 187 | − | } | |
| 188 | − | 60% { | |
| 189 | − | transform: rotate(6deg); | |
| 190 | 179 | } | |
| 191 | 180 | } | |
| 192 | 181 | ||
| 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 | − | } |
| 561 | 561 | "A live preview for every pull request", | |
| 562 | 562 | "Production on every merge to main", | |
| 563 | 563 | "Custom domains, with certificates", | |
| 564 | − | "Projects that depend on each other", | |
| 565 | 564 | ]} | |
| 566 | 565 | more={["Deployments", `${DOCS}/guides/deployments/`]} | |
| 567 | 566 | > |
| 729 | 729 | onChange={(value) => update(index, { value })} | |
| 730 | 730 | /> | |
| 731 | 731 | ) : 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> | |
| 733 | 733 | ) : ( | |
| 734 | 734 | <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 })} /> | |
| 735 | 735 | )} | |
| ⋯ | |||
| 754 | 754 | </ul> | |
| 755 | 755 | {hasG1t && ( | |
| 756 | 756 | <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. | |
| 758 | 758 | </p> | |
| 759 | 759 | )} | |
| 760 | 760 | {!disabled && ( | |
| 79 | 79 | <p className="mt-1.5 text-sm text-muted"> | |
| 80 | 80 | Security and quality has no price of its own. With the plan{price ? ` (${price} for the workspace)` : ""}, custom patterns, | |
| 81 | 81 | 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 | |
| 84 | 83 | alerts and security updates stay free everywhere. | |
| 85 | 84 | </p> | |
| 86 | 85 | <div className="mt-3 flex flex-wrap items-center gap-3 text-sm"> |
| 6 | 6 | - **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. | |
| 7 | 7 | - **New accounts confirm their email address** before they can create repositories or push. | |
| 8 | 8 | ||
| 9 | − | ## Agents act with credentials of their own | |
| 9 | + | ## Jobs act with credentials of their own | |
| 10 | 10 | ||
| 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: | |
| 12 | 12 | ||
| 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. | |
| 16 | 16 | ||
| 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. | |
| 18 | 18 | ||
| 19 | 19 | ## Every action is recorded | |
| 20 | 20 | ||
| 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. | |
| 22 | 22 | ||
| 23 | − | ## Guardrails on what agents can do | |
| 23 | + | ## Guardrails on what runs | |
| 24 | 24 | ||
| 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. | |
| 32 | 26 | ||
| 33 | 27 | ## Isolated sandboxes | |
| 34 | 28 | ||
| 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. | |
| 36 | 30 | ||
| 37 | 31 | ## Secrets stay secret | |
| 38 | 32 | ||
| ⋯ | |||
| 44 | 38 | ||
| 45 | 39 | ## Built on Cloudflare | |
| 46 | 40 | ||
| 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. | |
| 48 | 42 | ||
| 49 | 43 | 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. | |
| 50 | 44 | ||
| ⋯ | |||
| 54 | 48 | ||
| 55 | 49 | Free compute attracts people who would mine cryptocurrency with it, and stolen cards. g1t guards against both without making honest use harder: | |
| 56 | 50 | ||
| 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. | |
| 58 | 52 | - **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. | |
| 59 | 53 | - **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. | |
| 60 | 54 | ||
| ⋯ | |||
| 74 | 68 | ||
| 75 | 69 | - g1t.sh, api.g1t.sh, mcp.g1t.sh, og.g1t.sh, docs.g1t.sh and status.g1t.sh; | |
| 76 | 70 | - 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; | |
| 78 | 72 | - authentication, authorization, the API, the MCP server and git over HTTPS; | |
| 79 | 73 | - the [open-source code](/flagon-io/g1t), as deployed. | |
| 80 | 74 | ||
| 13 | 13 | creditLine, | |
| 14 | 14 | needsAttention, | |
| 15 | 15 | usageGlance, | |
| 16 | − | parseCaps, | |
| 17 | 16 | parseLimitRequest, | |
| 18 | 17 | parsePrepay, | |
| 19 | 18 | parseSpendLimit, | |
| ⋯ | |||
| 67 | 66 | value: { amountCents: 2_000_000, method: "bank_transfer" }, | |
| 68 | 67 | }); | |
| 69 | 68 | 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); | |
| 81 | 69 | }); | |
| 82 | 70 | ||
| 83 | 71 | test("a limit request needs an amount, a reason and a monthly spend; an overage only a reason", () => { | |
| 74 | 74 | return { ok: true, value: { amountCents: Math.round(amount * 100), method } }; | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 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 | − | ||
| 102 | 77 | /** A request for a higher limit, or for help with usage past what was meant. */ | |
| 103 | 78 | export function parseLimitRequest(form: { | |
| 104 | 79 | kind?: FormDataEntryValue | null; |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | − | import { capShare, formatCap, settingsFromForm } from "./guardrails.ts"; | |
| 4 | + | import { settingsFromForm } from "./guardrails.ts"; | |
| 5 | 5 | ||
| 6 | 6 | const catalog = { kinds: ["implement", "review"] as const }; | |
| 7 | 7 | ||
| ⋯ | |||
| 32 | 32 | test("nonsense is passed on for the service to refuse", () => { | |
| 33 | 33 | const settings = settingsFromForm(form({ "minutes:implement": "soon" }), catalog); | |
| 34 | 34 | 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); | |
| 46 | 35 | }); | |
| 1 | 1 | import type { GuardrailSettings, RunKind } from "@g1t/contracts"; | |
| 2 | − | import { microsOf, money } from "./money.ts"; | |
| 3 | 2 | ||
| 4 | 3 | /** A number from a field; empty means inherit. Not a number is kept, to be refused. */ | |
| 5 | 4 | function amount(value: FormDataEntryValue | null): number | null { | |
| ⋯ | |||
| 23 | 22 | if (cap != null) minutes[kind] = Number.isNaN(cap) ? 0 : Math.trunc(cap); | |
| 24 | 23 | } | |
| 25 | 24 | 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)); | |
| 37 | 25 | } | |
| 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 | − | }); |
| 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 | − | } |
| 161 | 161 | models: { | |
| 162 | 162 | title: "Your AI providers", | |
| 163 | 163 | 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.", | |
| 165 | 165 | }, | |
| 166 | 166 | alerts: { | |
| 167 | 167 | title: "Alerts", |