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
146 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails1/**
2 * The front door's rate limits (workers/app.ts), per request before it is
3 * answered. The bindings and their limits are in `RATE_LIMITS`
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.4 * (packages/contracts/src/rate-limits.ts); CONTRIBUTING.md, "Rate limits",
5 * says how they are kept.
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails6 *
7 * - Git over HTTPS: without credentials, by address; with them, by a hash
8 * of the credential, much higher. A clone is about three requests.
9 * - Pages: every request that reaches the Worker counts against a ceiling
10 * per address. Signed out, by address, with a tighter limit on what is
11 * costly to answer (archives, run pages, logs, search). Signed in, by a
12 * hash of the session cookie, higher: the session is not checked here,
13 * which would cost a call to identity, and the ceiling per address keeps
Merge main into Artifacts Phase 214 * made-up cookies from getting round the signed-out limit. With an
15 * access token (`Authorization: Bearer`, app/lib/website-token.ts), by a
16 * hash of the token, at the API's limit for a token; the address ceiling
17 * applies as for cookies.
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails18 *
19 * Static assets never reach the Worker (the assets binding answers them),
20 * and the few files it serves itself are left out here too. Every limit
21 * fails open.
22 */
23import {
24 type RateLimitBinding,
25 checkLimit,
26 clientAddress,
27 secretKey,
28 tooManyRequests,
29} from "@g1t/contracts/rate-limits";
30
31export type FrontDoorLimits = {
32 WEB_ANONYMOUS_LIMIT?: RateLimitBinding;
33 WEB_HEAVY_LIMIT?: RateLimitBinding;
34 WEB_SESSION_LIMIT?: RateLimitBinding;
35 WEB_ADDRESS_LIMIT?: RateLimitBinding;
Merge main into Artifacts Phase 236 WEB_TOKEN_LIMIT?: RateLimitBinding;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails37 GIT_ANONYMOUS_LIMIT?: RateLimitBinding;
38 GIT_SIGNED_LIMIT?: RateLimitBinding;
39};
40
41/** Files the Worker serves that are never limited: build output, fonts, and top-level files such as robots.txt. */
42const UNLIMITED = /^\/(?:assets\/|fonts\/|favicon|[^/]+\.(?:ico|png|svg|txt|xml|webmanifest)$)/;
43
A running job's log is live, as on GitHub. A step's output reaches its run's page within about two seconds: the runner sends what is printed every second, and the page asks every second while a step runs, every three while none does; an answer that fails (a 429, a 5xx, the network) is asked again after a wait that doubles to half a minute, or as long as Retry-After says, where before one such answer stopped the log for good and the step said "No output yet." until the page was reloaded, and a signed-out viewer's run page, refreshing beside its log, ran into the 30-a-minute limit within a minute; the live log now counts as a page, and only log downloads as costly. A hidden tab stops asking and catches up when shown, and the page says when it is reconnecting. The running step opens by itself, its timer counts, its log follows the newest line while its end is on screen and offers Jump to latest once the viewer scrolls up, a step that has printed nothing for a while says "Running · no output for 2m 10s", and the job's header names the step it is on. Colour that tools print (bold, dim, italic, underline, the 16 colours as theme tokens for light and dark, 256 colours and 24-bit colour, text and background) is drawn as printed, other escape codes are dropped and links keep their text, and search reads the plain text and keeps the colours around what it marks. Every line is stamped with the time it was printed, as GitHub's raw logs are, so log.txt and the run's zip have a time at the start of each line, and a clock button beside Search logs shows the times, remembered by the browser. Set up job now gives the runner's version and name, the operating system, the runner image, the machine's architecture, CPUs and memory, the token's permissions under G1T_TOKEN Permissions, the secret source and the actions it downloads; each run step opens with GitHub's folded Run group, the script in cyan, the shell's whole path and the step's env, each uses step with its with inputs (an action's defaults among them) and env; cache and artifact transfers say how far they have got every five seconds; and a last Complete job step sets the job's outputs and stops what the steps left running, found by RUNNER_TRACKING_ID as GitHub's runner finds it. Two batches of a job's log written at once can no longer reach for the same seq and lose one. The Actions guide and the rate limits page say so.44/**
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r945 * What is costly to answer for a signed-out visitor, or must not be flooded:
46 * a repository's archives, a run's page, log downloads and artifacts,
47 * search, and the form that reports a vulnerability to a repository's
48 * maintainers privately, which takes no account. A running job's live log
49 * (`actions/jobs/:job/log`, a small read the page makes about once a
50 * second) counts as a page: with the run's page refreshing beside it, it
51 * would use up this limit in half a minute.
A running job's log is live, as on GitHub. A step's output reaches its run's page within about two seconds: the runner sends what is printed every second, and the page asks every second while a step runs, every three while none does; an answer that fails (a 429, a 5xx, the network) is asked again after a wait that doubles to half a minute, or as long as Retry-After says, where before one such answer stopped the log for good and the step said "No output yet." until the page was reloaded, and a signed-out viewer's run page, refreshing beside its log, ran into the 30-a-minute limit within a minute; the live log now counts as a page, and only log downloads as costly. A hidden tab stops asking and catches up when shown, and the page says when it is reconnecting. The running step opens by itself, its timer counts, its log follows the newest line while its end is on screen and offers Jump to latest once the viewer scrolls up, a step that has printed nothing for a while says "Running · no output for 2m 10s", and the job's header names the step it is on. Colour that tools print (bold, dim, italic, underline, the 16 colours as theme tokens for light and dark, 256 colours and 24-bit colour, text and background) is drawn as printed, other escape codes are dropped and links keep their text, and search reads the plain text and keeps the colours around what it marks. Every line is stamped with the time it was printed, as GitHub's raw logs are, so log.txt and the run's zip have a time at the start of each line, and a clock button beside Search logs shows the times, remembered by the browser. Set up job now gives the runner's version and name, the operating system, the runner image, the machine's architecture, CPUs and memory, the token's permissions under G1T_TOKEN Permissions, the secret source and the actions it downloads; each run step opens with GitHub's folded Run group, the script in cyan, the shell's whole path and the step's env, each uses step with its with inputs (an action's defaults among them) and env; cache and artifact transfers say how far they have got every five seconds; and a last Complete job step sets the job's outputs and stops what the steps left running, found by RUNNER_TRACKING_ID as GitHub's runner finds it. Two batches of a job's log written at once can no longer reach for the same seq and lose one. The Actions guide and the rate limits page say so.52 */
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails53const HEAVY =
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r954 /^\/(?:search(?:\.data)?$|[^/]+\/[^/]+\/(?:archive\/|actions\/runs\/[^/]+(?:\.data|\/logs\.zip|\/artifacts\/[^/]+)?$|actions\/jobs\/[^/]+\/log\.txt$|security\/report(?:\.data)?$))/;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails55
56export function unlimited(pathname: string): boolean {
57 return UNLIMITED.test(pathname);
58}
59
60export function heavy(pathname: string): boolean {
61 return HEAVY.test(pathname);
62}
63
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.64/** The signed token of an archive download link (`archive/<ref>.zip|.tar.gz?token=`), or null. */
65export function archiveLink(pathname: string, url: string): string | null {
66 if (!/^\/[^/]+\/[^/]+\/archive\/.+\.(?:zip|tar\.gz)$/.test(pathname)) return null;
67 try {
68 return new URL(url).searchParams.get("token") || null;
69 } catch {
70 return null;
71 }
72}
73
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails74/** The session cookie's value, or null when signed out. */
75export function sessionCookie(cookie: string | null): string | null {
76 const match = /(?:^|;\s*)g1t_session=([^;]+)/.exec(cookie ?? "");
77 return match?.[1] ?? null;
78}
79
80const GIT_MESSAGE_ANONYMOUS =
81 "Too many git requests from your network. Wait a minute and try again, or use credentials for a higher limit: https://docs.g1t.sh/reference/rate-limits/\n";
82const GIT_MESSAGE_SIGNED = "Too many git requests with these credentials. Wait a minute and try again: https://docs.g1t.sh/reference/rate-limits/\n";
83const PAGE_MESSAGE = "Too many requests from your network. Wait a minute and try again.\n";
Merge main into Artifacts Phase 284const TOKEN_PAGE_MESSAGE = "Too many requests with this access token. Wait a minute and try again: https://docs.g1t.sh/reference/rate-limits/\n";
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails85
86/**
87 * The 429 for a git request past its limit, or null to go on. Git shows a
88 * plain-text answer's body to the person running it.
89 */
90export async function gitLimited(env: FrontDoorLimits, request: Request): Promise<Response | null> {
91 const credentials = request.headers.get("authorization");
92 if (credentials) {
93 const verdict = await checkLimit(env.GIT_SIGNED_LIMIT, await secretKey("git", credentials));
94 return verdict === "limited" ? tooManyRequests(GIT_MESSAGE_SIGNED) : null;
95 }
96 const verdict = await checkLimit(env.GIT_ANONYMOUS_LIMIT, `ip:${clientAddress(request)}`);
97 return verdict === "limited" ? tooManyRequests(GIT_MESSAGE_ANONYMOUS) : null;
98}
99
Merge main into Artifacts Phase 2100/**
101 * The token in a page request's `Authorization: Bearer` header, or null
102 * (app/lib/website-token.ts). Not checked here either.
103 */
104export function websiteToken(authorization: string | null): string | null {
105 return /^\s*bearer\s+(\S+)\s*$/i.exec(authorization ?? "")?.[1] ?? null;
106}
107
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails108/** The 429 for a page or data request past its limit, or null to go on. */
109export async function pageLimited(env: FrontDoorLimits, request: Request, pathname: string): Promise<Response | null> {
110 if (unlimited(pathname)) return null;
111 const address = `ip:${clientAddress(request)}`;
Merge main into Artifacts Phase 2112 const token = websiteToken(request.headers.get("authorization"));
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails113 const session = sessionCookie(request.headers.get("cookie"));
114 const checks: Promise<string>[] = [checkLimit(env.WEB_ADDRESS_LIMIT, address)];
Merge main into Artifacts Phase 2115 if (token) {
116 // A token on the website: per token, as the API counts it.
117 checks.push(secretKey("token", token).then((key) => checkLimit(env.WEB_TOKEN_LIMIT, key)));
118 } else if (session) {
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails119 checks.push(secretKey("session", session).then((key) => checkLimit(env.WEB_SESSION_LIMIT, key)));
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.120 } else if (archiveLink(pathname, request.url)) {
121 // A signed archive link (`GET /repos/{owner}/{repo}/tarball/{ref}` made
122 // it for one commit, for 10 minutes) counts per link, as a token does:
123 // many jobs behind one address fall back to tarballs at once.
124 checks.push(secretKey("archive", archiveLink(pathname, request.url)!).then((key) => checkLimit(env.WEB_TOKEN_LIMIT, key)));
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails125 } else {
126 checks.push(checkLimit(env.WEB_ANONYMOUS_LIMIT, address));
127 if (heavy(pathname)) checks.push(checkLimit(env.WEB_HEAVY_LIMIT, address));
128 }
129 const verdicts = await Promise.all(checks);
130 if (!verdicts.includes("limited")) return null;
Merge main into Artifacts Phase 2131 if (token && verdicts[1] === "limited") return tooManyRequests(TOKEN_PAGE_MESSAGE);
132 return tooManyRequests(token || session ? PAGE_MESSAGE : `${PAGE_MESSAGE.trimEnd()} Signed-in accounts have a higher limit.\n`);
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails133}
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address134
135/**
136 * The 429 for a repository file on the usercontent origin past its limit,
137 * or null to go on. Nothing there is signed in (it never sees the session
Merge branch 'worktree-agent-a5b0d9e2733e9e382'138 * cookie), so a file counts as a signed-out page from its address. Avatars,
139 * custom emoji and production images are answered from cache and are not
140 * limited.
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address141 */
142export async function usercontentLimited(env: FrontDoorLimits, request: Request, path: string): Promise<Response | null> {
Merge branch 'worktree-agent-a5b0d9e2733e9e382'143 if (path.startsWith("/avatars/") || path.startsWith("/emoji/") || path.startsWith("/production-images/")) return null;
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address144 const verdict = await checkLimit(env.WEB_ANONYMOUS_LIMIT, `ip:${clientAddress(request)}`);
145 return verdict === "limited" ? tooManyRequests(PAGE_MESSAGE) : null;
146}

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