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
889 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.

A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1//! The public API: REST at api.g1t.sh and the MCP server at mcp.g1t.sh.
2//!
3//! One Worker, two hostnames. Both are thin adapters over the same
4//! operations (see [`operations::Op`]), which call the services that own
5//! the data. This Worker holds none.
6
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb977mod about;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.8mod run_artifacts;
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.9mod archives;
Merge branch 'worktree-agent-aaf03bdceac799c89'10mod addresses;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait11mod alerts;
12mod audit;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13mod billing;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait14mod blobs;
Merge branch 'main' into rm-agents15mod broker;
Merge checks: statuses and check runs on every commit16mod checks;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9717mod deployments;
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.18mod telemetry;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca19mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails20mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)21mod logs;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait22mod mcp;
API: notifications over REST and MCP, with notifications scopes23mod notifications;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait24mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R225mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API26mod packages;
Merge main into Artifacts Phase 227mod folios;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar28mod people;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait29mod openapi;
API: pinned projects over REST and MCP30mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9731mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'32mod protection;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait33mod operations;
34mod renamed;
35#[cfg(test)]
36mod responses;
37mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge38mod rules;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait39mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar40mod security;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait41mod tools;
API and MCP for a workspace's personal access token rules, members' tokens and approvals42mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R243mod toolkit;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait44
45use g1t_contracts::identity::{
46 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
47};
48use g1t_contracts::work::{
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.49 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
50 ReportQueueArgs,
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait51};
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)52use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait53use g1t_kit::wire;
54use serde_json::{Value, json};
55use worker::{Context, Env, Method, Request, Response, Result, event};
56
57use operations::Services;
58
59fn method_name(method: Method) -> &'static str {
60 match method {
61 Method::Get => "GET",
62 Method::Post => "POST",
63 Method::Patch => "PATCH",
64 Method::Put => "PUT",
65 Method::Delete => "DELETE",
66 Method::Options => "OPTIONS",
67 Method::Head => "HEAD",
68 _ => "OTHER",
69 }
70}
71
72/// A JSON response. Every body the API sends has its keys in `snake_case`;
73/// the contracts it passes through are `camelCase`, so they are converted
74/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
75/// the MCP protocol's own envelope keep the spelling their standards use.
76pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
77 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
78}
79
80/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
81/// workflow file, GitHub's contexts and event, and where to check out, all
82/// passed through as they are.
83const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'84 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait85];
86
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R287/// Puts the toolkit's variables in a job's spec: its runtime token, where
88/// the toolkit's services are, and where to ask for an OIDC token when the
89/// job may have one and this installation issues them. The `runtime` the
90/// actions service sent goes no further.
91fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
92 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
93 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
94 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
95 let vars = toolkit::runtime_variables(api, token, id_token);
96 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
97 variables.extend(vars);
98 }
99}
100
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)101/// What a person whose account has not confirmed its email address may
102/// call: who they are, their addresses, and confirming one with the code
103/// from the email. Nothing over MCP.
104fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
105 !on_mcp
106 && matches!(
107 (method, path.trim_end_matches('/')),
108 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
109 )
110}
111
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait112/// An error in the shape every endpoint uses.
113fn failure(failure: &Failure) -> Result<Response> {
114 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
115}
116
117fn fail(code: FailureCode, message: &str) -> Result<Response> {
118 failure(&Failure {
119 code,
120 message: message.to_owned(),
121 })
122}
123
124/// A request body as JSON. An empty or malformed body is no input.
125async fn json_body(request: &mut Request) -> Value {
126 request.json().await.unwrap_or(Value::Null)
127}
128
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'129/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
130/// Octokit sends it) names. A missing token is an anonymous viewer; a
131/// wrong one is refused, so that a typo does not silently look signed out.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait132async fn authenticate(
133 request: &Request,
134 services: &Services,
135) -> Result<std::result::Result<Viewer, Response>> {
136 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'137 let Some(token) = limits::bearer(&header) else {
138 return Ok(Ok(None));
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait139 };
140 let viewer: Viewer = g1t_kit::call(
141 &services.identity,
142 "user_for_access_token",
143 &TokenArgs {
144 token: token.to_owned(),
145 },
146 )
147 .await?;
148 if viewer.is_some() {
149 return Ok(Ok(viewer));
150 }
151 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
152 // Tells an MCP client where to sign in again.
153 response.headers_mut().set(
154 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'155 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait156 )?;
157 Ok(Err(response))
158}
159
160/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'161fn index(addresses: &addresses::Addresses) -> Value {
162 let api = &addresses.api;
163 let repo = format!("{api}/repos/{{owner}}/{{name}}");
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait164 json!({
165 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'166 "openapi_url": format!("{api}/openapi.json"),
167 "mcp_url": addresses.mcp,
168 "current_user_url": format!("{api}/user"),
169 "workspaces_url": format!("{api}/workspaces"),
170 "repositories_url": format!("{api}/repos{{?q}}"),
171 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait172 "repository_url": repo,
173 "repository_events_url": format!("{repo}/events{{?before}}"),
174 "labels_url": format!("{repo}/labels"),
175 "issues_url": format!("{repo}/issues{{?state,label}}"),
176 "issue_url": format!("{repo}/issues/{{number}}"),
177 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
178 "pulls_url": format!("{repo}/pulls{{?state}}"),
179 "pull_url": format!("{repo}/pulls/{{number}}"),
180 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
181 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
182 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'183 "device_code_url": format!("{api}/device/code"),
184 "device_token_url": format!("{api}/device/token"),
185 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
186 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
187 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait188 "context_url": format!("{repo}/context{{?reference}}"),
189 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'190 "hooks_url": format!("{api}/hooks/{{integration}}"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait191 })
192}
193
194// Signing in from a tool. Accounts are created, and passwords typed, only
195// in a browser; a tool gets its token by having a person approve a code.
196
197/// Passes a request from an outside system to its connection, as it came:
198/// its signature covers the exact bytes of the body.
199async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
200 let headers: std::collections::HashMap<String, String> = request
201 .headers()
202 .entries()
203 .map(|(name, value)| (name.to_lowercase(), value))
204 .collect();
205 let body = request.text().await.unwrap_or_default();
206 // A push to GitHub with many commits makes a large payload.
207 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
208 if body.len() > limit {
209 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
210 }
211 // g1t's GitHub App has one webhook for every installation; it is
212 // checked against the app's own secret.
213 let (method, args) = if id == "github" {
214 ("github_receive", json!({ "headers": headers, "body": body }))
215 } else {
216 ("receive", json!({ "id": id, "headers": headers, "body": body }))
217 };
218 let received: g1t_contracts::integrations::Received =
219 g1t_kit::call(&services.integrations, method, &args).await?;
220 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
221}
222
223async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
224 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
225 let payload = request.text().await.unwrap_or_default();
226 if payload.len() > 1_000_000 || signature.is_empty() {
227 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
228 }
229 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
230 &env.service("BILLING")?,
231 "stripe_webhook",
232 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
233 )
234 .await?;
235 // A refusal is a 400, so Stripe shows it as failed; anything handled,
236 // or already handled, is a 200, so Stripe stops sending it.
237 Ok(match handled {
238 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
239 g1t_contracts::Outcome::Fail(failure) => {
240 reply(&json!({ "message": failure.message }))?.with_status(400)
241 }
242 })
243}
244
245async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
246 let body = json_body(request).await;
247 let started: DeviceStart = g1t_kit::call(
248 &services.identity,
249 "device_start",
250 &DeviceStartArgs {
251 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
252 },
253 )
254 .await?;
255 reply(&json!({
256 "device_code": started.device_code,
257 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'258 "verification_uri": format!("{}/device", services.addresses.site),
259 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait260 "expires_in": started.expires_in,
261 "interval": started.interval,
262 }))
263}
264
265async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
266 let body = json_body(request).await;
267 let claim: DeviceClaim = g1t_kit::call(
268 &services.identity,
269 "device_claim",
270 &DeviceClaimArgs {
271 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
272 },
273 )
274 .await?;
275 reply(&match claim {
276 DeviceClaim::Approved { token, user } => json!({
277 "status": "approved",
278 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar279 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait280 "username": user.username,
281 "verified": user.verified,
282 }),
283 DeviceClaim::Pending => json!({ "status": "pending" }),
284 DeviceClaim::Denied => json!({ "status": "denied" }),
285 DeviceClaim::Expired => json!({ "status": "expired" }),
286 })
287}
288
289/// A sandbox reporting on a run of an issue's commands, from before a pull
290/// request's checks were the workflows run on it.
291/// The run's own token, in the body, is the credential: it was given to
292/// that sandbox and to nothing else.
293async fn report_checks(
294 request: &mut Request,
295 services: &Services,
296 run_id: &str,
297) -> Result<Response> {
298 let body = json_body(request).await;
299 let reported: Outcome<CheckRun> = g1t_kit::call(
300 &services.work,
301 "report_checks",
302 &ReportChecksArgs {
303 run_id: run_id.to_owned(),
304 token: body["token"].as_str().unwrap_or_default().to_owned(),
305 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
306 error: body["error"].as_str().map(str::to_owned),
307 skip: false,
308 },
309 )
310 .await?;
311 match reported {
312 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
313 Outcome::Fail(refused) => failure(&refused),
314 }
315}
316
317/// A sandbox reporting one tested state of a merge queue. As with checks,
318/// the entry's own token is the credential.
319async fn report_queue(
320 request: &mut Request,
321 services: &Services,
322 entry_id: &str,
323) -> Result<Response> {
324 let body = json_body(request).await;
325 let reported: Outcome<QueueState> = g1t_kit::call(
326 &services.work,
327 "report_queue",
328 &ReportQueueArgs {
329 entry_id: entry_id.to_owned(),
330 token: body["token"].as_str().unwrap_or_default().to_owned(),
331 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
332 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
333 error: body["error"].as_str().map(str::to_owned),
334 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
335 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
336 },
337 )
338 .await?;
339 match reported {
340 Outcome::Ok(state) => reply(&json!({ "state": state })),
341 Outcome::Fail(refused) => failure(&refused),
342 }
343}
344
345/// A sandbox reporting whether a pull request merges cleanly. As with
346/// checks, the probe's own token is the credential.
347async fn report_mergecheck(
348 request: &mut Request,
349 services: &Services,
350 pull_id: &str,
351) -> Result<Response> {
352 let body = json_body(request).await;
353 let reported: Outcome<Mergeable> = g1t_kit::call(
354 &services.work,
355 "report_mergecheck",
356 &ReportMergecheckArgs {
357 pull_id: pull_id.to_owned(),
358 token: body["token"].as_str().unwrap_or_default().to_owned(),
359 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
360 error: body["error"].as_str().map(str::to_owned),
361 },
362 )
363 .await?;
364 match reported {
365 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
366 Outcome::Fail(refused) => failure(&refused),
367 }
368}
369
Merge branch 'worktree-agent-ac5b181a013e54348'370/// A backup's sandbox, passed on to the repos service, which holds the
371/// job (services/repos/src/backups.rs; the flow is in
372/// `g1t_contracts::backups`):
373///
374/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
375/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
376/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
377/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
378///
379/// Bodies are passed through as they are: snake_case already, and a
380/// bundle's refs are keyed by ref names, which must not be converted.
381async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
382 use g1t_contracts::backups::TOKEN_HEADER;
383 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
384 let rest = path.trim_start_matches("/backups/");
385 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
386 if job.is_empty() || token.is_empty() {
387 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
388 }
389 if method == "PUT" && action.starts_with("parts/") {
390 let bytes = request.bytes().await?;
391 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
392 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
393 }
394 let headers = worker::Headers::new();
395 headers.set(TOKEN_HEADER, &token)?;
396 let mut init = worker::RequestInit::new();
397 init.with_method(Method::Put)
398 .with_headers(headers)
399 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
400 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
401 let mut answered = services.repos.fetch_request(forwarded).await?;
402 return outcome_as_given(answered.json().await?);
403 }
404 let rpc = match (method, action) {
405 ("POST", "spec") => "backup_spec",
406 ("POST", "complete") => "backup_complete",
407 ("POST", "fail") => "backup_fail",
408 _ => return fail(FailureCode::NotFound, "No such endpoint."),
409 };
410 let mut body = json_body(request).await;
411 if !body.is_object() {
412 body = json!({});
413 }
414 body["job_id"] = json!(job);
415 body["token"] = json!(token);
416 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
417 outcome_as_given(answered)
418}
419
420/// An `Outcome` from a service whose keys are already the API's: the value,
421/// or the failure in the shape every endpoint uses.
422fn outcome_as_given(answered: Value) -> Result<Response> {
423 match serde_json::from_value::<Outcome<Value>>(answered)? {
424 Outcome::Ok(value) => Response::from_json(&value),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait425 Outcome::Fail(refused) => failure(&refused),
426 }
427}
428
429async fn respond(mut request: Request, env: &Env) -> Result<Response> {
430 let method = method_name(request.method());
431 if method == "OPTIONS" {
432 return Ok(Response::empty()?.with_status(204));
433 }
434 let url = request.url()?;
435 // Paths carry no version. An earlier form began with `/v1`, which is
436 // still accepted so that nothing already written against it breaks.
437 let path = match url.path().strip_prefix("/v1") {
438 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
439 _ => url.path().to_owned(),
440 };
441 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'442 // MCP is a host of its own hosted, and may be a path on this one
443 // self-hosted (addresses.rs).
444 let on_mcp = services.addresses.mcp_path(&url).is_some();
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait445
446 // Stripe reporting to billing. Signed with the secret of the endpoint
447 // billing registered; the body goes through exactly as received, since
448 // the signature covers its bytes.
449 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
450 return receive_stripe(&mut request, env).await;
451 }
452
453 // Outside systems reporting to a connection. They sign what they send
454 // with the connection's own secret, which is not a g1t token, so this
455 // comes before anything that would read one.
456 if method == "POST" && !on_mcp
457 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
458 return receive_hook(&mut request, &services, id).await;
459 }
460
461 // A sandbox building a deployment, reporting with its build's token,
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.462 // which is not a g1t token, or a container app's Worker reporting how
463 // long its container ran, with its own token. The body goes through as
464 // it is: it can carry a Worker's bundled code.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait465 if method == "POST" && !on_mcp
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.466 && let Some(rest) = path
467 .strip_prefix("/deployments/jobs/")
468 .map(|rest| format!("jobs/{rest}"))
469 .or_else(|| path.strip_prefix("/deployments/apps/").filter(|rest| rest.ends_with("/meter")).map(|rest| format!("apps/{rest}")))
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait470 {
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.471 let target = format!("https://deployments/{rest}");
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait472 let body = request.bytes().await?;
473 let headers = worker::Headers::new();
474 headers.set("content-type", "application/json")?;
475 let mut init = worker::RequestInit::new();
476 init.with_method(Method::Post)
477 .with_headers(headers)
478 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
479 let mut answer = env
480 .service("DEPLOYMENTS")?
481 .fetch_request(Request::new_with_init(&target, &init)?)
482 .await?;
483 // A fresh response: a fetched one's headers cannot be changed, and
484 // every response gets the API's own on the way out.
485 let status = answer.status_code();
486 let bytes = answer.bytes().await?;
487 let bytes = match serde_json::from_slice::<Value>(&bytes) {
488 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
489 Err(_) => bytes,
490 };
491 return Ok(Response::from_bytes(bytes)?
492 .with_status(status)
493 .with_headers({
494 let headers = worker::Headers::new();
495 headers.set("content-type", "application/json")?;
496 headers
497 }));
498 }
499
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2500 // The services GitHub's toolkit calls from inside a job, with its
501 // runtime token, and the links they hand out (toolkit.rs).
502 if !on_mcp && method == "POST"
503 && let Some(rest) = path.strip_prefix("/twirp/")
504 {
505 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
506 let (service, rpc) = (service.to_owned(), rpc.to_owned());
507 return toolkit::twirp(request, env, &services, &service, &rpc).await;
508 }
509 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
510 let rest = rest.to_owned();
511 return toolkit::cache_v1(request, env, &services, method, &rest).await;
512 }
513 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
514 let token = token.to_owned();
515 return toolkit::blob(request, env, &services, method, &token).await;
516 }
517 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
518 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
519 return oidc::handle(&request, env, &services, &path).await;
520 }
521
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait522 // A sandbox's artifacts and cache, with its job's token, which is not a
523 // g1t token either.
524 if !on_mcp
525 && let Some(rest) = path.strip_prefix("/actions/jobs/")
526 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
527 {
528 let rest = rest.to_owned();
529 return blobs::for_job(request, env, &services, method, &rest).await;
530 }
Merge branch 'main' into rm-agents531 // A self-hosted job's request to a brokered secret's host, passed to
532 // g1t to be filled in (broker.rs). The job's token is the credential.
533 if !on_mcp
534 && method == "POST"
535 && let Some(job) = broker::job_of(&path)
536 {
537 let job = job.to_owned();
538 return broker::relay(request, &services, &job).await;
539 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait540
541 // A self-hosted runner, with a registration token or its own
542 // credential, neither of which is a g1t access token.
543 if method == "POST"
544 && !on_mcp
545 && path.starts_with("/runners/")
546 && let Some(response) = runners::handle(&mut request, &services, &path).await?
547 {
548 return Ok(response);
549 }
550
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails551 // Per token, or per address without one (limits.rs).
552 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
553 return Ok(limited);
554 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait555 let viewer = match authenticate(&request, &services).await? {
556 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails557 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait558 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)559 // A person who has not confirmed their email address: who they are,
560 // their addresses, and confirming one, nothing else (REST or MCP).
561 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
562 return fail(
563 FailureCode::Forbidden,
564 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
565 );
566 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait567 services.audit = audit::AuditContext::of(&request, on_mcp);
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.568 // A run's token: what it may do comes with it, on the composite
569 // identity identity resolved it to. Any other token of g1t's own
570 // principal (the agent tokens g1t no longer issues) does nothing.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait571 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
572 services.scope = Some(acting.scope.clone());
573 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.574 return fail(FailureCode::Unauthenticated, "Invalid access token.");
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait575 }
576 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
577 return Ok(response);
578 }
579 if on_mcp {
580 return mcp::handle(request, &services, &viewer).await;
581 }
582
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)583 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
584 if method == "GET" {
585 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
586 if let Some(wanted) = logs::wanted(&path, text) {
587 return logs::download(&services, &viewer, wanted).await;
588 }
589 }
590
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait591 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'592 ("GET", "") => return reply(&index(&services.addresses)),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait593 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2594 // One of a run's artifacts downloaded by name (the run's artifacts
595 // are listed by the REST route in rest.rs).
596 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait597 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2598 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'599 // A workflow job's token reaches its own repository only.
600 if viewer
601 .as_ref()
602 .and_then(|user| user.token.as_deref())
603 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
604 {
605 return fail(FailureCode::NotFound, "No such run.");
606 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2607 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait608 }
609 }
610 ("POST", "/device/code") => return device_code(&mut request, &services).await,
611 ("POST", "/device/token") => return device_token(&mut request, &services).await,
612 // Where a pull request lives, for a tool that knows only its fork.
613 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
614 let located: Outcome<Value> = g1t_kit::call(
615 &services.work,
616 "locate_pull",
617 &json!({ "id": &path[7..], "viewer": viewer }),
618 )
619 .await?;
620 return match located {
621 Outcome::Ok(value) => reply(&value),
622 Outcome::Fail(refused) => failure(&refused),
623 };
624 }
625 ("POST", path) if path.starts_with("/mergechecks/") => {
626 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
627 return report_mergecheck(&mut request, &services, &pull_id).await;
628 }
Merge branch 'worktree-agent-ac5b181a013e54348'629 // A sandbox making a repository's nightly backup. The job's own
630 // token, in its header, is the credential.
631 (method, path) if path.starts_with("/backups/") => {
632 return backup_job(&mut request, &services, method, path).await;
633 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait634 ("POST", path) if path.starts_with("/queue/") => {
635 let entry_id = path.trim_start_matches("/queue/").to_owned();
636 return report_queue(&mut request, &services, &entry_id).await;
637 }
638 // A sandbox running a GitHub Actions job: fetching the job, and
639 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'640 // A sandbox downloading an action's archive that g1t keeps
641 // (actions' registry.rs). The job's own token is the credential.
642 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
643 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
644 let body = json_body(&mut request).await;
645 let kept: Outcome<Value> = g1t_kit::call(
646 &services.actions,
647 "job_action_archive",
648 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
649 )
650 .await?;
651 let kept = match kept {
652 Outcome::Ok(kept) => kept,
653 Outcome::Fail(refused) => return failure(&refused),
654 };
655 let bucket = env.bucket("ACTIONS_CACHE")?;
656 let object = kept["object"].as_str().unwrap_or_default();
657 let Some(stored) = bucket.get(object).execute().await? else {
658 return fail(FailureCode::NotFound, "The archive is gone.");
659 };
660 let Some(body) = stored.body() else {
661 return fail(FailureCode::NotFound, "The archive is gone.");
662 };
663 let mut response = Response::from_body(body.response_body()?)?;
664 response.headers_mut().set("content-type", "application/gzip")?;
665 return Ok(response);
666 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait667 ("POST", path) if path.starts_with("/actions/jobs/") => {
668 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts669 // `/action`: where to fetch another repository's action from (on
670 // g1t, with a read token for a private one, or GitHub).
671 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
672 (Some(job), _) => (job.to_owned(), "job_spec"),
673 (_, Some(job)) => (job.to_owned(), "job_action"),
674 _ => (rest.to_owned(), "job_report"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait675 };
676 let body = json_body(&mut request).await;
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.677 // `rotate`: a harness that takes a new token with the spec and
678 // lets go of the one it asked with (plan.rs `job_spec`).
679 let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
Merge branch 'main' into rm-agents680 // `broker`: a self-hosted harness that passes its job's
681 // requests to brokered hosts through g1t (broker.rs).
682 let broker = method == "job_spec" && body["broker"].as_bool() == Some(true);
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait683 let answered: Outcome<Value> = g1t_kit::call(
684 &services.actions,
685 method,
Merge branch 'main' into rm-agents686 &json!({ "job": job, "token": body["token"], "report": body["report"], "rotate": rotate, "broker": broker }),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait687 )
688 .await?;
689 return match answered {
690 // A job's spec is the workflow and its contexts as GitHub
691 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2692 Outcome::Ok(value) => {
693 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
694 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
695 Response::from_json(&spec)
696 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait697 Outcome::Fail(refused) => failure(&refused),
698 };
699 }
700 ("POST", path) if path.starts_with("/checks/") => {
701 let run_id = path.trim_start_matches("/checks/").to_owned();
702 return report_checks(&mut request, &services, &run_id).await;
703 }
704 _ => {}
705 }
706
707 let query: Vec<(String, String)> = url
708 .query_pairs()
709 .map(|(name, value)| (name.into_owned(), value.into_owned()))
710 .collect();
711 let body = if method == "GET" {
712 Value::Null
713 } else {
714 snake_case_keys(json_body(&mut request).await)
715 };
716 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
717 return fail(FailureCode::NotFound, "No such endpoint.");
718 };
719 match audit::run(route.op, &services, &viewer, &input).await? {
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.720 // A download is a redirect to its signed link, as GitHub's is: an
721 // artifact's, and a repository's tarball and zipball.
722 Outcome::Ok(value) if route.op.redirects() => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2723 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
724 Some(url) => Response::redirect_with_status(url, 302),
725 None => reply(&value),
726 }
727 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts728 // A deleted comment has nothing to say, as GitHub's says nothing.
729 Outcome::Ok(_) if route.no_content() => Ok(Response::empty()?.with_status(204)),
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.730 Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, route.op.as_given())),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait731 // A token without the scope a call needs is told which one.
732 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
733 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
734 "error": {
735 "code": refused.code,
736 "message": refused.message,
737 "needed_scope": scope.as_str(),
738 }
739 }))?
740 .with_status(403)),
741 _ => failure(&refused),
742 },
743 }
744}
745
746/// Request bodies take the same keys as the MCP tools, `snake_case`, as
747/// responses use; the `camelCase` spelling is accepted too.
748fn snake_case_keys(body: Value) -> Value {
749 let Value::Object(fields) = body else {
750 return body;
751 };
752 let mut out = serde_json::Map::new();
753 for (key, value) in fields {
754 let mut snake = String::with_capacity(key.len() + 4);
755 for c in key.chars() {
756 if c.is_ascii_uppercase() {
757 snake.push('_');
758 snake.push(c.to_ascii_lowercase());
759 } else {
760 snake.push(c);
761 }
762 }
763 // A key given in both spellings keeps the snake_case one.
764 if snake != key && out.contains_key(&snake) {
765 continue;
766 }
767 out.insert(snake, value);
768 }
769 Value::Object(out)
770}
771
772#[cfg(test)]
773mod tests {
774 use super::snake_case_keys;
775 use serde_json::json;
776
777 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)778 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
779 assert!(super::pending_may("GET", "/user", false));
780 assert!(super::pending_may("GET", "/user/emails/", false));
781 assert!(super::pending_may("POST", "/user/emails/confirm", false));
782 assert!(!super::pending_may("POST", "/user/emails", false));
783 assert!(!super::pending_may("POST", "/workspaces", false));
784 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
785 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
786 assert!(!super::pending_may("POST", "/", true));
787 }
788
789 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2790 fn a_job_spec_gets_the_toolkits_variables() {
791 // As the actions service sends it, converted as the API does.
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.792 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true }, "nextToken": "n" });
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2793 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
794 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
795 assert!(spec.get("runtime").is_none(), "the runner never sees it");
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.796 // The token that replaces the one the harness asked with.
797 assert_eq!(spec["next_token"], "n");
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2798 let vars = &spec["variables"];
799 assert_eq!(vars["GITHUB_SHA"], "abc");
800 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
801 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
802 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
803 // No OIDC key here: no OIDC variables, whatever the job may do.
804 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
805 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
806 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
807 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
808 }
809
810 #[test]
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait811 fn camel_case_keys_are_accepted() {
812 assert_eq!(
813 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
814 json!({ "count_agent_approvals": false, "title": "x" })
815 );
816 }
817
818 #[test]
819 fn snake_case_wins_when_both_are_given() {
820 assert_eq!(
821 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
822 json!({ "keep_issue_open": true })
823 );
824 }
825}
826
827// The API is called from browsers too: the reference's explorer, and apps
828// built on g1t. It carries no cookies, so any origin may call it.
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.829/// The signal an OTLP path sends (`/otlp/v1/traces`, `logs` or `metrics`),
830/// if it is one.
831fn otlp_signal(path: &str) -> Option<&'static str> {
832 let path = path.strip_prefix("/v1").filter(|rest| rest.starts_with('/')).unwrap_or(path);
833 match path.trim_end_matches('/') {
834 "/otlp/v1/traces" => Some("traces"),
835 "/otlp/v1/logs" => Some("logs"),
836 "/otlp/v1/metrics" => Some("metrics"),
837 _ => None,
838 }
839}
840
841/// OpenTelemetry sent to the API's own address: a self-hosted install's
842/// only door, and a second one hosted. It goes to the telemetry service as
843/// it came, which checks the project's ingest token itself (not a g1t
844/// token), and its answer comes back as it is, CORS headers included.
845async fn forward_otlp(mut request: Request, env: &Env, signal: &str) -> Result<Response> {
846 let method = request.method();
847 let headers = worker::Headers::new();
848 for name in ["authorization", "x-g1t-token", "content-type", "content-encoding"] {
849 if let Some(value) = request.headers().get(name)? {
850 headers.set(name, &value)?;
851 }
852 }
853 let mut init = worker::RequestInit::new();
854 init.with_method(method.clone()).with_headers(headers);
855 if method == Method::Post {
856 let body = request.bytes().await?;
857 init.with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
858 }
859 let target = format!("https://telemetry/v1/{signal}");
860 let mut answer = env.service("TELEMETRY")?.fetch_request(Request::new_with_init(&target, &init)?).await?;
861 // A fresh response, with the service's headers: a fetched one's cannot
862 // be changed, and nothing should be added to them.
863 let status = answer.status_code();
864 let headers = worker::Headers::new();
865 for (name, value) in answer.headers().entries() {
866 headers.set(&name, &value)?;
867 }
868 let bytes = answer.bytes().await?;
869 let response = if bytes.is_empty() { Response::empty()? } else { Response::from_bytes(bytes)? };
870 Ok(response.with_status(status).with_headers(headers))
871}
872
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait873#[event(fetch)]
874async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> {
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.875 // OTLP passes through untouched, before any g1t token is read.
876 if let Some(signal) = otlp_signal(&request.path()) {
877 return forward_otlp(request, &env, signal).await;
878 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait879 let mut response = respond(request, &env).await?;
880 let headers = response.headers_mut();
881 headers.set("access-control-allow-origin", "*")?;
882 headers.set(
883 "access-control-allow-headers",
884 "authorization, content-type",
885 )?;
886 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails887 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait888 Ok(response)
889}

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