Skip to content
348 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! Every response the REST routes give, run through the converter the API
2//! sends them with, checked for `camelCase` that would leak out.
3//!
4//! The samples are the reference's example responses, put back into the
5//! `camelCase` the services send (as serde's `rename_all` writes it) and,
6//! where an operation returns a contract type, decoded into that type and
7//! encoded again, so that every field the type has is sent, not only the
8//! ones an example shows.
9
Merge checks: statuses and check runs on every commit10use g1t_contracts::{access, actions, checks, codeowners, integrations, repos, rules, search, teams, webhooks, work};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API11use g1t_kit::wire::{self, USER_KEYED};
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14use serde_json::{Map, Value, json};
15
16use crate::openapi::document;
17use crate::operations::Op;
Merge checks: statuses and check runs on every commit18use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge19use crate::rules::RulesOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca20use crate::deploy_keys::DeployKeysOp;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API21
22/// A key as `#[serde(rename_all = "camelCase")]` writes it.
23fn camel_key(key: &str) -> String {
24 let mut out = String::with_capacity(key.len());
25 let mut upper = false;
26 for c in key.chars() {
27 if c == '_' {
28 upper = true;
29 } else if upper {
30 out.extend(c.to_uppercase());
31 upper = false;
32 } else {
33 out.push(c);
34 }
35 }
36 out
37}
38
39/// A response as the services send it: `camelCase`, but for the maps the
40/// converter passes through, which are data.
41fn as_services_send(value: &Value) -> Value {
42 match value {
43 Value::Object(fields) => {
44 let mut out = Map::new();
45 for (key, value) in fields {
46 let user_keyed = value.is_object()
47 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
48 let value = if user_keyed { value.clone() } else { as_services_send(value) };
49 // A `by_…` map keeps its name in the converter's spelling.
50 let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) };
51 out.insert(key, value);
52 }
53 Value::Object(out)
54 }
55 Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()),
56 other => other.clone(),
57 }
58}
59
60/// `value` decoded as `T` and encoded again, as the service would send it.
61fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value {
62 let decoded: T = serde_json::from_value(value)
63 .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>()));
64 serde_json::to_value(decoded).unwrap()
65}
66
67/// What the service behind an operation sends, from its example.
68fn sample(op: Op, example: &Value) -> Value {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 // Types serde already writes in `snake_case`: a person, and who has
70 // access. Sent as they are.
71 let as_is = example.clone();
72 match op {
73 Op::Whoami => return through::<g1t_contracts::User>(op, as_is),
74 Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is),
75 Op::AddCollaborator => return through::<access::Added>(op, as_is),
76 Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is),
77 Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is),
78 Op::ListRepoInvitations | Op::ListMyRepoInvitations => {
79 return through::<Vec<access::RepoInvitation>>(op, as_is);
80 }
81 Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => {
82 return through::<access::RepoInvitation>(op, as_is);
83 }
84 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
Merge main (membership, two-factor, GitHub repo roles) into tokens85 // Members, also `snake_case`.
86 Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is),
87 Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is),
88 Op::RemoveMember | Op::TransferOwnership | Op::LeaveWorkspace => return through::<bool>(op, as_is),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar89 // Teams and code owners, also `snake_case`.
90 Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is),
91 Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => {
92 return through::<teams::Team>(op, as_is);
93 }
94 Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is),
95 Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is),
96 Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is),
97 Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
98 Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
99 Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge100 // Rulesets travel in `snake_case` between services too.
101 Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => {
102 return through::<Vec<rules::Ruleset>>(op, as_is);
103 }
104 Op::Rules(
105 RulesOp::GetRepoRuleset
106 | RulesOp::CreateRepoRuleset
107 | RulesOp::UpdateRepoRuleset
108 | RulesOp::GetWorkspaceRuleset
109 | RulesOp::CreateWorkspaceRuleset
110 | RulesOp::UpdateWorkspaceRuleset,
111 ) => return through::<rules::Ruleset>(op, as_is),
112 Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is),
113 Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => {
114 return through::<rules::EvaluationPage>(op, as_is);
115 }
116 // Built by the API itself.
117 Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97118 // Deployments travel in `snake_case` between services too.
119 Op::Deployments(_) => return as_is,
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2120 // Artifacts are shaped by the API itself, in `snake_case`.
121 Op::Artifacts(_) => return as_is,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca122 // Deploy keys travel in `snake_case` from identity.
123 Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is),
124 Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => {
125 return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is);
126 }
127 Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is),
Merge packages: roles, Actions access, source label, soft delete, API128 // Packages are shaped by the API itself, in `snake_case`.
129 Op::Packages(_) => return as_is,
Merge main into Artifacts Phase 2130 // Artifacts are shaped by the API itself, in `snake_case`.
131 Op::Folios(_) => return as_is,
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.132 // Agents' computers travel in `snake_case` from the agents service.
133 Op::GetAgentComputer | Op::WakeAgentComputer | Op::SleepAgentComputer | Op::ResetAgentComputer | Op::ListAgentComputerCommands => return as_is,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily134 // Built by the API itself, in `snake_case`.
135 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
136 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
137 return through::<crate::alerts::SecurityAlert>(op, as_is);
138 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look139 _ => {}
140 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar141 let mut sent = as_services_send(example);
142 // A pull request's code owners are `snake_case` inside it.
143 if op == Op::GetPullRequest
144 && let Some(code_owners) = example.get("code_owners")
145 {
146 sent["codeOwners"] = code_owners.clone();
147 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API148 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'149 Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API150 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
Search across all of g1t, Explore, and a command palette151 Op::Search => through::<search::SearchResults>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look152 Op::GetRepo
153 | Op::CreateRepo
154 | Op::UpdateRepo
155 | Op::TransferRepo
156 | Op::RenameRepo
157 | Op::RenameBranch
158 | Op::ArchiveRepo
159 | Op::UnarchiveRepo
160 | Op::SetRepoVisibility
161 | Op::RestoreRepo => through::<repos::Repo>(op, sent),
162 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
163 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API164 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents165 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API166 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
167 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
168 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
169 through::<work::Issue>(op, sent)
170 }
171 Op::GetIssue => through::<work::IssueDetail>(op, sent),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step172 Op::Delegate => through::<work::Delegated>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API173 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar174 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
175 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
176 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
177 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
178 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
179 Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar181 Op::MarkPullRequestReady
182 | Op::ClosePullRequest
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts183 | Op::ReopenPullRequest
184 | Op::ConvertPullRequestToDraft
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar185 | Op::MergePullRequest
186 | Op::AssignIssue
187 | Op::RequestReviewers
188 | Op::RemoveRequestedReviewers => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API189 through::<work::Pull>(op, sent)
190 }
191 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
192 Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
193 Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
194 Op::GetJobLogs => through::<actions::JobLog>(op, sent),
195 Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
196 through::<actions::WorkflowRun>(op, sent)
197 }
198 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents199 Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
200 Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
201 Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
202 Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
203 Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API204 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
205 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
206 Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
207 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
209 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
210 }
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)211 Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look212 Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent),
213 Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => {
214 through::<g1t_contracts::identity::Invite>(op, sent)
215 }
216 Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)217 Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent),
API: notifications over REST and MCP, with notifications scopes218 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
219 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
220 through::<g1t_contracts::inbox::InboxThread>(op, sent)
221 }
222 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
223 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
224 }
Merge checks: statuses and check runs on every commit225 Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent),
226 Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent),
227 Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent),
228 Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => {
229 through::<checks::CommitCheckRun>(op, sent)
230 }
231 Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent),
232 Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent),
233 Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent),
234 Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API235 _ => sent,
236 }
237}
238
239/// Every key of `example`, as paths, outside the maps passed through.
240fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
241 match value {
242 Value::Object(fields) => {
243 for (key, value) in fields {
244 let here = format!("{path}.{key}");
245 out.push(here.clone());
246 let user_keyed = value.is_object()
247 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
248 if !user_keyed {
249 paths(value, &here, out);
250 }
251 }
252 }
253 Value::Array(items) => {
254 for item in items {
255 paths(item, &format!("{path}[]"), out);
256 }
257 }
258 _ => {}
259 }
260}
261
262#[test]
263fn no_route_answers_with_camel_case() {
264 let document = document();
265 let (mut checked, mut converted) = (0, 0);
266 for (path, methods) in document["paths"].as_object().unwrap() {
267 for (method, operation) in methods.as_object().unwrap() {
268 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step269 let tool = operation["x-operation"].as_str().unwrap_or_default();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API270 let Some(op) = Op::by_name(tool) else {
271 // Device sign-in, which is written in `snake_case` by hand.
272 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
273 continue;
274 };
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar275 let mut sample = sample(op, example);
276 // Each person gets their chosen case on the way out (people.rs).
277 crate::people::fill(&mut sample, &std::collections::HashMap::new());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API278 converted += wire::camel_case_keys(&sample).len();
279 let sent = wire::snake_case(sample);
280 let leaked = wire::camel_case_keys(&sent);
281 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
282 // The reference shows what is sent: each of its names is one.
283 let (mut shown, mut real) = (Vec::new(), Vec::new());
284 paths(example, "", &mut shown);
285 paths(&sent, "", &mut real);
286 for name in shown {
287 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
288 }
289 checked += 1;
290 }
291 }
292 assert!(checked >= Op::ALL.len());
293 // The samples are in the services' spelling, so there was something to
294 // convert.
295 assert!(converted > 100, "{converted}");
296}
297
298#[test]
299fn every_route_has_a_sample() {
300 let document = document();
301 for route in crate::rest::ROUTES {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts302 if route.no_content() {
303 continue;
304 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API305 let path = route
306 .path
307 .split('/')
308 .map(|segment| match segment.strip_prefix(':') {
309 Some(name) => format!("{{{name}}}"),
310 None => segment.to_owned(),
311 })
312 .collect::<Vec<_>>()
313 .join("/");
314 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
315 ["content"]["application/json"]["example"];
316 assert!(!example.is_null(), "{} {path}", route.method);
317 }
318}
319
320#[test]
321fn errors_and_reports_are_snake_case() {
322 let failure = g1t_contracts::Failure {
323 code: g1t_contracts::FailureCode::NotFound,
324 message: "No such endpoint.".to_owned(),
325 };
326 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
327}
328
329#[test]
330fn a_job_spec_keeps_github_s_spelling() {
331 let spec = json!({
332 "job": "job_1",
333 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
334 "workflow": { "env": { "nodeEnv": "x" } },
335 "github": { "eventName": "push", "headRef": "" },
336 "event": { "pull_request": { "headSha": "x" } },
337 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
338 "checkout": { "ref": "main" },
339 "timeoutMinutes": 30,
340 "masks": [],
341 });
342 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
343 assert_eq!(sent["timeout_minutes"], 30);
344 assert!(sent.get("timeoutMinutes").is_none());
345 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
346 assert_eq!(sent[kept], spec[kept], "{kept}");
347 }
348}

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