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-guardrails | 1 | /** |
| 2 | * Rate limits on g1t's public surfaces: Workers Rate Limiting bindings | |
| 3 | * (`ratelimits` in each wrangler.jsonc), asked once per request with a key: | |
| 4 | * the client's address, or a hash of its session or token, never the | |
| 5 | * secret itself. | |
| 6 | * | |
| 7 | * Every binding is listed in `RATE_LIMITS` with its namespace id and limit, | |
| 8 | * and apps/web's tests check each wrangler.jsonc against it, so this table | |
| 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. | 9 | * is the one place to read or change them. CONTRIBUTING.md, "Rate |
| 10 | * limits", says how; the docs' rate limits page | |
| 11 | * (docs.g1t.sh/reference/rate-limits/) shows the public ones. | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 12 | * |
| 13 | * A limit fails open: no binding (self-hosted, `wrangler dev` without one) | |
| 14 | * or a binding that throws lets the request through. A limit guards | |
| 15 | * against floods; it is never a reason for g1t to stop answering. | |
| 16 | */ | |
| 17 | ||
| 18 | /** A Workers Rate Limiting binding, as the runtime hands it over. */ | |
| 19 | export type RateLimitBinding = { limit(options: { key: string }): Promise<{ success: boolean }> }; | |
| 20 | ||
| 21 | /** What asking a limit said. `unavailable` lets the request through. */ | |
| 22 | export type LimitVerdict = "allowed" | "limited" | "unavailable"; | |
| 23 | ||
| 24 | /** | |
| 25 | * Every limit's window, in seconds. Workers Rate Limiting counts over 10 | |
| 26 | * or 60 seconds; every g1t limit uses 60, and a client past one is told to | |
| 27 | * wait that long. | |
| 28 | */ | |
| 29 | export const LIMIT_PERIOD_SECONDS = 60; | |
| 30 | ||
| 31 | export type RateLimitSpec = { | |
| 32 | /** The Worker whose wrangler.jsonc declares it. */ | |
| 33 | worker: string; | |
| 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. | 34 | /** Unique per Cloudflare account; allocated in blocks per Worker (CONTRIBUTING.md, "Rate limits"). */ |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 35 | namespaceId: number; |
| 36 | /** Requests per `LIMIT_PERIOD_SECONDS` per key. */ | |
| 37 | limit: number; | |
| 38 | /** What a key is. */ | |
| 39 | per: string; | |
| 40 | }; | |
| 41 | ||
| 42 | /** | |
| 43 | * Every rate limit binding, by name. Generous for people, tight for floods: | |
| 44 | * a person browsing makes a few requests a second at most, a clone is three | |
| 45 | * git requests, and nothing a person does needs dozens of archives a minute. | |
| 46 | * Namespace ids go in blocks of a hundred per Worker: 41xx packages, | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 47 | * 42xx web, 43xx repos, 44xx api, 45xx og, 46xx status, 47xx telemetry. |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 48 | */ |
| 49 | export const RATE_LIMITS = { | |
| 50 | // services/packages (src/limits.rs): registry pulls and token requests. | |
| 51 | ANONYMOUS_LIMIT: { worker: "services/packages", namespaceId: 4101, limit: 300, per: "address" }, | |
| 52 | SIGNED_LIMIT: { worker: "services/packages", namespaceId: 4102, limit: 5000, per: "person, workspace or agent" }, | |
| 53 | // apps/web (workers/app.ts): the front door. | |
| 54 | WEB_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4201, limit: 600, per: "address, signed-out pages" }, | |
| 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. | 55 | WEB_HEAVY_LIMIT: { worker: "apps/web", namespaceId: 4202, limit: 30, per: "address, signed-out archives, run pages, log downloads and search" }, |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 56 | WEB_SESSION_LIMIT: { worker: "apps/web", namespaceId: 4203, limit: 1200, per: "session, signed-in requests" }, |
| 57 | WEB_ADDRESS_LIMIT: { worker: "apps/web", namespaceId: 4204, limit: 3000, per: "address, every request that reaches the Worker" }, | |
| 58 | GIT_ANONYMOUS_LIMIT: { worker: "apps/web", namespaceId: 4205, limit: 120, per: "address, git requests without credentials" }, | |
| 59 | GIT_SIGNED_LIMIT: { worker: "apps/web", namespaceId: 4206, limit: 1200, per: "credential, git requests with credentials" }, | |
| Merge main into Artifacts Phase 2 | 60 | // The same as API_TOKEN_LIMIT: a token counts alike on the website and the API. |
| 61 | WEB_TOKEN_LIMIT: { worker: "apps/web", namespaceId: 4207, limit: 1000, per: "token, pages and data requests with an access token" }, | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 62 | // services/repos (src/lib.rs): what an anonymous clone can cost a repository's owner. |
| 63 | PACK_FILL_LIMIT: { worker: "services/repos", namespaceId: 4301, limit: 30, per: "repository, packs written to the pack cache" }, | |
| 64 | ANONYMOUS_FETCH_LIMIT: { worker: "services/repos", namespaceId: 4302, limit: 120, per: "repository, anonymous fetches the store answers" }, | |
| 65 | // apps/api (src/limits.rs): REST and MCP, which count apart. | |
| 66 | API_ANONYMOUS_LIMIT: { worker: "apps/api", namespaceId: 4401, limit: 60, per: "address, requests without a token" }, | |
| 67 | API_TOKEN_LIMIT: { worker: "apps/api", namespaceId: 4402, limit: 1000, per: "token" }, | |
| 68 | // services/og: drawing a card that is not in the cache. | |
| 69 | OG_RENDER_LIMIT: { worker: "services/og", namespaceId: 4501, limit: 60, per: "address, cards drawn" }, | |
| 70 | // apps/status: asking for a subscription, which sends an email. | |
| 71 | STATUS_SUBSCRIBE_LIMIT: { worker: "apps/status", namespaceId: 4601, limit: 3, per: "address" }, | |
| 72 | STATUS_EMAIL_LIMIT: { worker: "apps/status", namespaceId: 4602, limit: 2, per: "email address" }, | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 73 | // services/telemetry (src/index.ts): OTLP requests to otel.g1t.sh, counted |
| 74 | // before the body is read or a project's store is woken. | |
| 75 | TELEMETRY_INGEST_LIMIT: { worker: "services/telemetry", namespaceId: 4701, limit: 600, per: "address, OTLP requests" }, | |
| Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails | 76 | } as const satisfies Record<string, RateLimitSpec>; |
| 77 | ||
| 78 | export type RateLimitName = keyof typeof RATE_LIMITS; | |
| 79 | ||
| 80 | /** | |
| 81 | * Counts one request against `binding` under `key`. Never throws: a missing | |
| 82 | * binding or one that fails is `unavailable`, which callers let through. | |
| 83 | */ | |
| 84 | export async function checkLimit(binding: RateLimitBinding | undefined, key: string): Promise<LimitVerdict> { | |
| 85 | if (!binding) return "unavailable"; | |
| 86 | try { | |
| 87 | const { success } = await binding.limit({ key }); | |
| 88 | return success ? "allowed" : "limited"; | |
| 89 | } catch (error) { | |
| 90 | console.error(JSON.stringify({ event: "rate_limit.unavailable", error: String(error) })); | |
| 91 | return "unavailable"; | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | /** Whether the request is past its limit. Fails open, like `checkLimit`. */ | |
| 96 | export async function isLimited(binding: RateLimitBinding | undefined, key: string): Promise<boolean> { | |
| 97 | return (await checkLimit(binding, key)) === "limited"; | |
| 98 | } | |
| 99 | ||
| 100 | /** The client's address as Cloudflare saw it, for keys; `unknown` when there is none (local dev). */ | |
| 101 | export function clientAddress(request: Request): string { | |
| 102 | return request.headers.get("cf-connecting-ip")?.trim() || "unknown"; | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * A key for a secret (a session cookie, a token, a credential): `prefix:` | |
| 107 | * and the first 16 hex digits of its SHA-256, so the secret itself never | |
| 108 | * reaches the rate limiter. | |
| 109 | */ | |
| 110 | export async function secretKey(prefix: string, secret: string): Promise<string> { | |
| 111 | const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret))); | |
| 112 | let hex = ""; | |
| 113 | for (const byte of digest.subarray(0, 8)) hex += byte.toString(16).padStart(2, "0"); | |
| 114 | return `${prefix}:${hex}`; | |
| 115 | } | |
| 116 | ||
| 117 | /** A 429 with `Retry-After`, as plain text unless `headers` says otherwise. */ | |
| 118 | export function tooManyRequests(body: string, headers: Record<string, string> = {}): Response { | |
| 119 | return new Response(body, { | |
| 120 | status: 429, | |
| 121 | headers: { | |
| 122 | "content-type": "text/plain; charset=utf-8", | |
| 123 | "retry-after": String(LIMIT_PERIOD_SECONDS), | |
| 124 | "cache-control": "no-store", | |
| 125 | ...headers, | |
| 126 | }, | |
| 127 | }); | |
| 128 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.