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
890 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;
The API and MCP server coordinate work: start_session, update_session (an empty body is a heartbeat), end_session, get_session, list_sessions and record_session_entries at /repos/{owner}/{name}/sessions, as the new session tool's start, update, end, get, list and record; claim_issue, release_claim, hand_off_issue and get_work at /repos/{owner}/{name}/issues/{number}/claim, /release, /handoff and /work, and list_claims at /repos/{owner}/{name}/claims, as the issue tool's claim, release, handoff, work and list_claims; and update_issue takes blocked_by. Sessions need pull_requests scopes and claims and get_work issues scopes, reads are open to anyone who can read the repository, the reference has an example of each, and the OpenAPI copy and the CLI's table of operations are regenerated.17mod coordination;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9718mod 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.19mod telemetry;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca20mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails21mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)22mod logs;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait23mod mcp;
API: notifications over REST and MCP, with notifications scopes24mod notifications;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait25mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R226mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API27mod packages;
Merge main into Artifacts Phase 228mod folios;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar29mod people;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait30mod openapi;
API: pinned projects over REST and MCP31mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9732mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'33mod protection;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait34mod operations;
35mod renamed;
36#[cfg(test)]
37mod responses;
38mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge39mod rules;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait40mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar41mod security;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait42mod tools;
API and MCP for a workspace's personal access token rules, members' tokens and approvals43mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R244mod toolkit;
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait45
46use g1t_contracts::identity::{
47 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
48};
49use 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.50 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
51 ReportQueueArgs,
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait52};
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)53use 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 wait54use g1t_kit::wire;
55use serde_json::{Value, json};
56use worker::{Context, Env, Method, Request, Response, Result, event};
57
58use operations::Services;
59
60fn method_name(method: Method) -> &'static str {
61 match method {
62 Method::Get => "GET",
63 Method::Post => "POST",
64 Method::Patch => "PATCH",
65 Method::Put => "PUT",
66 Method::Delete => "DELETE",
67 Method::Options => "OPTIONS",
68 Method::Head => "HEAD",
69 _ => "OTHER",
70 }
71}
72
73/// A JSON response. Every body the API sends has its keys in `snake_case`;
74/// the contracts it passes through are `camelCase`, so they are converted
75/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
76/// the MCP protocol's own envelope keep the spelling their standards use.
77pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
78 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
79}
80
81/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
82/// workflow file, GitHub's contexts and event, and where to check out, all
83/// passed through as they are.
84const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'85 "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 wait86];
87
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R288/// Puts the toolkit's variables in a job's spec: its runtime token, where
89/// the toolkit's services are, and where to ask for an OIDC token when the
90/// job may have one and this installation issues them. The `runtime` the
91/// actions service sent goes no further.
92fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
93 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
94 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
95 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
96 let vars = toolkit::runtime_variables(api, token, id_token);
97 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
98 variables.extend(vars);
99 }
100}
101
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)102/// What a person whose account has not confirmed its email address may
103/// call: who they are, their addresses, and confirming one with the code
104/// from the email. Nothing over MCP.
105fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
106 !on_mcp
107 && matches!(
108 (method, path.trim_end_matches('/')),
109 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
110 )
111}
112
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait113/// An error in the shape every endpoint uses.
114fn failure(failure: &Failure) -> Result<Response> {
115 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
116}
117
118fn fail(code: FailureCode, message: &str) -> Result<Response> {
119 failure(&Failure {
120 code,
121 message: message.to_owned(),
122 })
123}
124
125/// A request body as JSON. An empty or malformed body is no input.
126async fn json_body(request: &mut Request) -> Value {
127 request.json().await.unwrap_or(Value::Null)
128}
129
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'130/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
131/// Octokit sends it) names. A missing token is an anonymous viewer; a
132/// 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 wait133async fn authenticate(
134 request: &Request,
135 services: &Services,
136) -> Result<std::result::Result<Viewer, Response>> {
137 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'138 let Some(token) = limits::bearer(&header) else {
139 return Ok(Ok(None));
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait140 };
141 let viewer: Viewer = g1t_kit::call(
142 &services.identity,
143 "user_for_access_token",
144 &TokenArgs {
145 token: token.to_owned(),
146 },
147 )
148 .await?;
149 if viewer.is_some() {
150 return Ok(Ok(viewer));
151 }
152 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
153 // Tells an MCP client where to sign in again.
154 response.headers_mut().set(
155 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'156 &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 wait157 )?;
158 Ok(Err(response))
159}
160
161/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'162fn index(addresses: &addresses::Addresses) -> Value {
163 let api = &addresses.api;
164 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 wait165 json!({
166 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'167 "openapi_url": format!("{api}/openapi.json"),
168 "mcp_url": addresses.mcp,
169 "current_user_url": format!("{api}/user"),
170 "workspaces_url": format!("{api}/workspaces"),
171 "repositories_url": format!("{api}/repos{{?q}}"),
172 "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 wait173 "repository_url": repo,
174 "repository_events_url": format!("{repo}/events{{?before}}"),
175 "labels_url": format!("{repo}/labels"),
176 "issues_url": format!("{repo}/issues{{?state,label}}"),
177 "issue_url": format!("{repo}/issues/{{number}}"),
178 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
179 "pulls_url": format!("{repo}/pulls{{?state}}"),
180 "pull_url": format!("{repo}/pulls/{{number}}"),
181 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
182 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
183 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'184 "device_code_url": format!("{api}/device/code"),
185 "device_token_url": format!("{api}/device/token"),
186 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
187 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
188 "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 wait189 "context_url": format!("{repo}/context{{?reference}}"),
190 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'191 "hooks_url": format!("{api}/hooks/{{integration}}"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait192 })
193}
194
195// Signing in from a tool. Accounts are created, and passwords typed, only
196// in a browser; a tool gets its token by having a person approve a code.
197
198/// Passes a request from an outside system to its connection, as it came:
199/// its signature covers the exact bytes of the body.
200async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
201 let headers: std::collections::HashMap<String, String> = request
202 .headers()
203 .entries()
204 .map(|(name, value)| (name.to_lowercase(), value))
205 .collect();
206 let body = request.text().await.unwrap_or_default();
207 // A push to GitHub with many commits makes a large payload.
208 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
209 if body.len() > limit {
210 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
211 }
212 // g1t's GitHub App has one webhook for every installation; it is
213 // checked against the app's own secret.
214 let (method, args) = if id == "github" {
215 ("github_receive", json!({ "headers": headers, "body": body }))
216 } else {
217 ("receive", json!({ "id": id, "headers": headers, "body": body }))
218 };
219 let received: g1t_contracts::integrations::Received =
220 g1t_kit::call(&services.integrations, method, &args).await?;
221 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
222}
223
224async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
225 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
226 let payload = request.text().await.unwrap_or_default();
227 if payload.len() > 1_000_000 || signature.is_empty() {
228 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
229 }
230 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
231 &env.service("BILLING")?,
232 "stripe_webhook",
233 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
234 )
235 .await?;
236 // A refusal is a 400, so Stripe shows it as failed; anything handled,
237 // or already handled, is a 200, so Stripe stops sending it.
238 Ok(match handled {
239 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
240 g1t_contracts::Outcome::Fail(failure) => {
241 reply(&json!({ "message": failure.message }))?.with_status(400)
242 }
243 })
244}
245
246async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
247 let body = json_body(request).await;
248 let started: DeviceStart = g1t_kit::call(
249 &services.identity,
250 "device_start",
251 &DeviceStartArgs {
252 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
253 },
254 )
255 .await?;
256 reply(&json!({
257 "device_code": started.device_code,
258 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'259 "verification_uri": format!("{}/device", services.addresses.site),
260 "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 wait261 "expires_in": started.expires_in,
262 "interval": started.interval,
263 }))
264}
265
266async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
267 let body = json_body(request).await;
268 let claim: DeviceClaim = g1t_kit::call(
269 &services.identity,
270 "device_claim",
271 &DeviceClaimArgs {
272 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
273 },
274 )
275 .await?;
276 reply(&match claim {
277 DeviceClaim::Approved { token, user } => json!({
278 "status": "approved",
279 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar280 "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 wait281 "username": user.username,
282 "verified": user.verified,
283 }),
284 DeviceClaim::Pending => json!({ "status": "pending" }),
285 DeviceClaim::Denied => json!({ "status": "denied" }),
286 DeviceClaim::Expired => json!({ "status": "expired" }),
287 })
288}
289
290/// A sandbox reporting on a run of an issue's commands, from before a pull
291/// request's checks were the workflows run on it.
292/// The run's own token, in the body, is the credential: it was given to
293/// that sandbox and to nothing else.
294async fn report_checks(
295 request: &mut Request,
296 services: &Services,
297 run_id: &str,
298) -> Result<Response> {
299 let body = json_body(request).await;
300 let reported: Outcome<CheckRun> = g1t_kit::call(
301 &services.work,
302 "report_checks",
303 &ReportChecksArgs {
304 run_id: run_id.to_owned(),
305 token: body["token"].as_str().unwrap_or_default().to_owned(),
306 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
307 error: body["error"].as_str().map(str::to_owned),
308 skip: false,
309 },
310 )
311 .await?;
312 match reported {
313 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
314 Outcome::Fail(refused) => failure(&refused),
315 }
316}
317
318/// A sandbox reporting one tested state of a merge queue. As with checks,
319/// the entry's own token is the credential.
320async fn report_queue(
321 request: &mut Request,
322 services: &Services,
323 entry_id: &str,
324) -> Result<Response> {
325 let body = json_body(request).await;
326 let reported: Outcome<QueueState> = g1t_kit::call(
327 &services.work,
328 "report_queue",
329 &ReportQueueArgs {
330 entry_id: entry_id.to_owned(),
331 token: body["token"].as_str().unwrap_or_default().to_owned(),
332 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
333 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
334 error: body["error"].as_str().map(str::to_owned),
335 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
336 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
337 },
338 )
339 .await?;
340 match reported {
341 Outcome::Ok(state) => reply(&json!({ "state": state })),
342 Outcome::Fail(refused) => failure(&refused),
343 }
344}
345
346/// A sandbox reporting whether a pull request merges cleanly. As with
347/// checks, the probe's own token is the credential.
348async fn report_mergecheck(
349 request: &mut Request,
350 services: &Services,
351 pull_id: &str,
352) -> Result<Response> {
353 let body = json_body(request).await;
354 let reported: Outcome<Mergeable> = g1t_kit::call(
355 &services.work,
356 "report_mergecheck",
357 &ReportMergecheckArgs {
358 pull_id: pull_id.to_owned(),
359 token: body["token"].as_str().unwrap_or_default().to_owned(),
360 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
361 error: body["error"].as_str().map(str::to_owned),
362 },
363 )
364 .await?;
365 match reported {
366 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
367 Outcome::Fail(refused) => failure(&refused),
368 }
369}
370
Merge branch 'worktree-agent-ac5b181a013e54348'371/// A backup's sandbox, passed on to the repos service, which holds the
372/// job (services/repos/src/backups.rs; the flow is in
373/// `g1t_contracts::backups`):
374///
375/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
376/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
377/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
378/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
379///
380/// Bodies are passed through as they are: snake_case already, and a
381/// bundle's refs are keyed by ref names, which must not be converted.
382async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
383 use g1t_contracts::backups::TOKEN_HEADER;
384 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
385 let rest = path.trim_start_matches("/backups/");
386 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
387 if job.is_empty() || token.is_empty() {
388 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
389 }
390 if method == "PUT" && action.starts_with("parts/") {
391 let bytes = request.bytes().await?;
392 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
393 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
394 }
395 let headers = worker::Headers::new();
396 headers.set(TOKEN_HEADER, &token)?;
397 let mut init = worker::RequestInit::new();
398 init.with_method(Method::Put)
399 .with_headers(headers)
400 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
401 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
402 let mut answered = services.repos.fetch_request(forwarded).await?;
403 return outcome_as_given(answered.json().await?);
404 }
405 let rpc = match (method, action) {
406 ("POST", "spec") => "backup_spec",
407 ("POST", "complete") => "backup_complete",
408 ("POST", "fail") => "backup_fail",
409 _ => return fail(FailureCode::NotFound, "No such endpoint."),
410 };
411 let mut body = json_body(request).await;
412 if !body.is_object() {
413 body = json!({});
414 }
415 body["job_id"] = json!(job);
416 body["token"] = json!(token);
417 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
418 outcome_as_given(answered)
419}
420
421/// An `Outcome` from a service whose keys are already the API's: the value,
422/// or the failure in the shape every endpoint uses.
423fn outcome_as_given(answered: Value) -> Result<Response> {
424 match serde_json::from_value::<Outcome<Value>>(answered)? {
425 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 wait426 Outcome::Fail(refused) => failure(&refused),
427 }
428}
429
430async fn respond(mut request: Request, env: &Env) -> Result<Response> {
431 let method = method_name(request.method());
432 if method == "OPTIONS" {
433 return Ok(Response::empty()?.with_status(204));
434 }
435 let url = request.url()?;
436 // Paths carry no version. An earlier form began with `/v1`, which is
437 // still accepted so that nothing already written against it breaks.
438 let path = match url.path().strip_prefix("/v1") {
439 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
440 _ => url.path().to_owned(),
441 };
442 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'443 // MCP is a host of its own hosted, and may be a path on this one
444 // self-hosted (addresses.rs).
445 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 wait446
447 // Stripe reporting to billing. Signed with the secret of the endpoint
448 // billing registered; the body goes through exactly as received, since
449 // the signature covers its bytes.
450 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
451 return receive_stripe(&mut request, env).await;
452 }
453
454 // Outside systems reporting to a connection. They sign what they send
455 // with the connection's own secret, which is not a g1t token, so this
456 // comes before anything that would read one.
457 if method == "POST" && !on_mcp
458 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
459 return receive_hook(&mut request, &services, id).await;
460 }
461
462 // 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.463 // which is not a g1t token, or a container app's Worker reporting how
464 // long its container ran, with its own token. The body goes through as
465 // 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 wait466 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.467 && let Some(rest) = path
468 .strip_prefix("/deployments/jobs/")
469 .map(|rest| format!("jobs/{rest}"))
470 .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 wait471 {
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.472 let target = format!("https://deployments/{rest}");
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait473 let body = request.bytes().await?;
474 let headers = worker::Headers::new();
475 headers.set("content-type", "application/json")?;
476 let mut init = worker::RequestInit::new();
477 init.with_method(Method::Post)
478 .with_headers(headers)
479 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
480 let mut answer = env
481 .service("DEPLOYMENTS")?
482 .fetch_request(Request::new_with_init(&target, &init)?)
483 .await?;
484 // A fresh response: a fetched one's headers cannot be changed, and
485 // every response gets the API's own on the way out.
486 let status = answer.status_code();
487 let bytes = answer.bytes().await?;
488 let bytes = match serde_json::from_slice::<Value>(&bytes) {
489 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
490 Err(_) => bytes,
491 };
492 return Ok(Response::from_bytes(bytes)?
493 .with_status(status)
494 .with_headers({
495 let headers = worker::Headers::new();
496 headers.set("content-type", "application/json")?;
497 headers
498 }));
499 }
500
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2501 // The services GitHub's toolkit calls from inside a job, with its
502 // runtime token, and the links they hand out (toolkit.rs).
503 if !on_mcp && method == "POST"
504 && let Some(rest) = path.strip_prefix("/twirp/")
505 {
506 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
507 let (service, rpc) = (service.to_owned(), rpc.to_owned());
508 return toolkit::twirp(request, env, &services, &service, &rpc).await;
509 }
510 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
511 let rest = rest.to_owned();
512 return toolkit::cache_v1(request, env, &services, method, &rest).await;
513 }
514 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
515 let token = token.to_owned();
516 return toolkit::blob(request, env, &services, method, &token).await;
517 }
518 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
519 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
520 return oidc::handle(&request, env, &services, &path).await;
521 }
522
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait523 // A sandbox's artifacts and cache, with its job's token, which is not a
524 // g1t token either.
525 if !on_mcp
526 && let Some(rest) = path.strip_prefix("/actions/jobs/")
527 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
528 {
529 let rest = rest.to_owned();
530 return blobs::for_job(request, env, &services, method, &rest).await;
531 }
Merge branch 'main' into rm-agents532 // A self-hosted job's request to a brokered secret's host, passed to
533 // g1t to be filled in (broker.rs). The job's token is the credential.
534 if !on_mcp
535 && method == "POST"
536 && let Some(job) = broker::job_of(&path)
537 {
538 let job = job.to_owned();
539 return broker::relay(request, &services, &job).await;
540 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait541
542 // A self-hosted runner, with a registration token or its own
543 // credential, neither of which is a g1t access token.
544 if method == "POST"
545 && !on_mcp
546 && path.starts_with("/runners/")
547 && let Some(response) = runners::handle(&mut request, &services, &path).await?
548 {
549 return Ok(response);
550 }
551
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails552 // Per token, or per address without one (limits.rs).
553 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
554 return Ok(limited);
555 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait556 let viewer = match authenticate(&request, &services).await? {
557 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails558 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 wait559 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)560 // A person who has not confirmed their email address: who they are,
561 // their addresses, and confirming one, nothing else (REST or MCP).
562 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
563 return fail(
564 FailureCode::Forbidden,
565 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
566 );
567 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait568 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.569 // A run's token: what it may do comes with it, on the composite
570 // identity identity resolved it to. Any other token of g1t's own
571 // 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 wait572 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
573 services.scope = Some(acting.scope.clone());
574 } 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.575 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 wait576 }
577 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
578 return Ok(response);
579 }
580 if on_mcp {
581 return mcp::handle(request, &services, &viewer).await;
582 }
583
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)584 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
585 if method == "GET" {
586 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
587 if let Some(wanted) = logs::wanted(&path, text) {
588 return logs::download(&services, &viewer, wanted).await;
589 }
590 }
591
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait592 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'593 ("GET", "") => return reply(&index(&services.addresses)),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait594 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2595 // One of a run's artifacts downloaded by name (the run's artifacts
596 // are listed by the REST route in rest.rs).
597 ("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 wait598 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2599 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'600 // A workflow job's token reaches its own repository only.
601 if viewer
602 .as_ref()
603 .and_then(|user| user.token.as_deref())
604 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
605 {
606 return fail(FailureCode::NotFound, "No such run.");
607 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2608 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 wait609 }
610 }
611 ("POST", "/device/code") => return device_code(&mut request, &services).await,
612 ("POST", "/device/token") => return device_token(&mut request, &services).await,
613 // Where a pull request lives, for a tool that knows only its fork.
614 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
615 let located: Outcome<Value> = g1t_kit::call(
616 &services.work,
617 "locate_pull",
618 &json!({ "id": &path[7..], "viewer": viewer }),
619 )
620 .await?;
621 return match located {
622 Outcome::Ok(value) => reply(&value),
623 Outcome::Fail(refused) => failure(&refused),
624 };
625 }
626 ("POST", path) if path.starts_with("/mergechecks/") => {
627 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
628 return report_mergecheck(&mut request, &services, &pull_id).await;
629 }
Merge branch 'worktree-agent-ac5b181a013e54348'630 // A sandbox making a repository's nightly backup. The job's own
631 // token, in its header, is the credential.
632 (method, path) if path.starts_with("/backups/") => {
633 return backup_job(&mut request, &services, method, path).await;
634 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait635 ("POST", path) if path.starts_with("/queue/") => {
636 let entry_id = path.trim_start_matches("/queue/").to_owned();
637 return report_queue(&mut request, &services, &entry_id).await;
638 }
639 // A sandbox running a GitHub Actions job: fetching the job, and
640 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'641 // A sandbox downloading an action's archive that g1t keeps
642 // (actions' registry.rs). The job's own token is the credential.
643 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
644 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
645 let body = json_body(&mut request).await;
646 let kept: Outcome<Value> = g1t_kit::call(
647 &services.actions,
648 "job_action_archive",
649 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
650 )
651 .await?;
652 let kept = match kept {
653 Outcome::Ok(kept) => kept,
654 Outcome::Fail(refused) => return failure(&refused),
655 };
656 let bucket = env.bucket("ACTIONS_CACHE")?;
657 let object = kept["object"].as_str().unwrap_or_default();
658 let Some(stored) = bucket.get(object).execute().await? else {
659 return fail(FailureCode::NotFound, "The archive is gone.");
660 };
661 let Some(body) = stored.body() else {
662 return fail(FailureCode::NotFound, "The archive is gone.");
663 };
664 let mut response = Response::from_body(body.response_body()?)?;
665 response.headers_mut().set("content-type", "application/gzip")?;
666 return Ok(response);
667 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait668 ("POST", path) if path.starts_with("/actions/jobs/") => {
669 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts670 // `/action`: where to fetch another repository's action from (on
671 // g1t, with a read token for a private one, or GitHub).
672 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
673 (Some(job), _) => (job.to_owned(), "job_spec"),
674 (_, Some(job)) => (job.to_owned(), "job_action"),
675 _ => (rest.to_owned(), "job_report"),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait676 };
677 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.678 // `rotate`: a harness that takes a new token with the spec and
679 // lets go of the one it asked with (plan.rs `job_spec`).
680 let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
Merge branch 'main' into rm-agents681 // `broker`: a self-hosted harness that passes its job's
682 // requests to brokered hosts through g1t (broker.rs).
683 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 wait684 let answered: Outcome<Value> = g1t_kit::call(
685 &services.actions,
686 method,
Merge branch 'main' into rm-agents687 &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 wait688 )
689 .await?;
690 return match answered {
691 // A job's spec is the workflow and its contexts as GitHub
692 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2693 Outcome::Ok(value) => {
694 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
695 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
696 Response::from_json(&spec)
697 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait698 Outcome::Fail(refused) => failure(&refused),
699 };
700 }
701 ("POST", path) if path.starts_with("/checks/") => {
702 let run_id = path.trim_start_matches("/checks/").to_owned();
703 return report_checks(&mut request, &services, &run_id).await;
704 }
705 _ => {}
706 }
707
708 let query: Vec<(String, String)> = url
709 .query_pairs()
710 .map(|(name, value)| (name.into_owned(), value.into_owned()))
711 .collect();
712 let body = if method == "GET" {
713 Value::Null
714 } else {
715 snake_case_keys(json_body(&mut request).await)
716 };
717 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
718 return fail(FailureCode::NotFound, "No such endpoint.");
719 };
720 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.721 // A download is a redirect to its signed link, as GitHub's is: an
722 // artifact's, and a repository's tarball and zipball.
723 Outcome::Ok(value) if route.op.redirects() => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2724 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
725 Some(url) => Response::redirect_with_status(url, 302),
726 None => reply(&value),
727 }
728 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts729 // A deleted comment has nothing to say, as GitHub's says nothing.
730 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.731 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 wait732 // A token without the scope a call needs is told which one.
733 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
734 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
735 "error": {
736 "code": refused.code,
737 "message": refused.message,
738 "needed_scope": scope.as_str(),
739 }
740 }))?
741 .with_status(403)),
742 _ => failure(&refused),
743 },
744 }
745}
746
747/// Request bodies take the same keys as the MCP tools, `snake_case`, as
748/// responses use; the `camelCase` spelling is accepted too.
749fn snake_case_keys(body: Value) -> Value {
750 let Value::Object(fields) = body else {
751 return body;
752 };
753 let mut out = serde_json::Map::new();
754 for (key, value) in fields {
755 let mut snake = String::with_capacity(key.len() + 4);
756 for c in key.chars() {
757 if c.is_ascii_uppercase() {
758 snake.push('_');
759 snake.push(c.to_ascii_lowercase());
760 } else {
761 snake.push(c);
762 }
763 }
764 // A key given in both spellings keeps the snake_case one.
765 if snake != key && out.contains_key(&snake) {
766 continue;
767 }
768 out.insert(snake, value);
769 }
770 Value::Object(out)
771}
772
773#[cfg(test)]
774mod tests {
775 use super::snake_case_keys;
776 use serde_json::json;
777
778 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)779 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
780 assert!(super::pending_may("GET", "/user", false));
781 assert!(super::pending_may("GET", "/user/emails/", false));
782 assert!(super::pending_may("POST", "/user/emails/confirm", false));
783 assert!(!super::pending_may("POST", "/user/emails", false));
784 assert!(!super::pending_may("POST", "/workspaces", false));
785 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
786 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
787 assert!(!super::pending_may("POST", "/", true));
788 }
789
790 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2791 fn a_job_spec_gets_the_toolkits_variables() {
792 // 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.793 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 R2794 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
795 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
796 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.797 // The token that replaces the one the harness asked with.
798 assert_eq!(spec["next_token"], "n");
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2799 let vars = &spec["variables"];
800 assert_eq!(vars["GITHUB_SHA"], "abc");
801 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
802 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
803 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
804 // No OIDC key here: no OIDC variables, whatever the job may do.
805 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
806 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
807 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
808 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
809 }
810
811 #[test]
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait812 fn camel_case_keys_are_accepted() {
813 assert_eq!(
814 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
815 json!({ "count_agent_approvals": false, "title": "x" })
816 );
817 }
818
819 #[test]
820 fn snake_case_wins_when_both_are_given() {
821 assert_eq!(
822 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
823 json!({ "keep_issue_open": true })
824 );
825 }
826}
827
828// The API is called from browsers too: the reference's explorer, and apps
829// 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.830/// The signal an OTLP path sends (`/otlp/v1/traces`, `logs` or `metrics`),
831/// if it is one.
832fn otlp_signal(path: &str) -> Option<&'static str> {
833 let path = path.strip_prefix("/v1").filter(|rest| rest.starts_with('/')).unwrap_or(path);
834 match path.trim_end_matches('/') {
835 "/otlp/v1/traces" => Some("traces"),
836 "/otlp/v1/logs" => Some("logs"),
837 "/otlp/v1/metrics" => Some("metrics"),
838 _ => None,
839 }
840}
841
842/// OpenTelemetry sent to the API's own address: a self-hosted install's
843/// only door, and a second one hosted. It goes to the telemetry service as
844/// it came, which checks the project's ingest token itself (not a g1t
845/// token), and its answer comes back as it is, CORS headers included.
846async fn forward_otlp(mut request: Request, env: &Env, signal: &str) -> Result<Response> {
847 let method = request.method();
848 let headers = worker::Headers::new();
849 for name in ["authorization", "x-g1t-token", "content-type", "content-encoding"] {
850 if let Some(value) = request.headers().get(name)? {
851 headers.set(name, &value)?;
852 }
853 }
854 let mut init = worker::RequestInit::new();
855 init.with_method(method.clone()).with_headers(headers);
856 if method == Method::Post {
857 let body = request.bytes().await?;
858 init.with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
859 }
860 let target = format!("https://telemetry/v1/{signal}");
861 let mut answer = env.service("TELEMETRY")?.fetch_request(Request::new_with_init(&target, &init)?).await?;
862 // A fresh response, with the service's headers: a fetched one's cannot
863 // be changed, and nothing should be added to them.
864 let status = answer.status_code();
865 let headers = worker::Headers::new();
866 for (name, value) in answer.headers().entries() {
867 headers.set(&name, &value)?;
868 }
869 let bytes = answer.bytes().await?;
870 let response = if bytes.is_empty() { Response::empty()? } else { Response::from_bytes(bytes)? };
871 Ok(response.with_status(status).with_headers(headers))
872}
873
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait874#[event(fetch)]
875async 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.876 // OTLP passes through untouched, before any g1t token is read.
877 if let Some(signal) = otlp_signal(&request.path()) {
878 return forward_otlp(request, &env, signal).await;
879 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait880 let mut response = respond(request, &env).await?;
881 let headers = response.headers_mut();
882 headers.set("access-control-allow-origin", "*")?;
883 headers.set(
884 "access-control-allow-headers",
885 "authorization, content-type",
886 )?;
887 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails888 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 wait889 Ok(response)
890}

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