Skip to content
346 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,
The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples.130 // Artifacts are shaped by the API itself, in `snake_case`.
131 Op::Folios(_) => return as_is,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily132 // Built by the API itself, in `snake_case`.
133 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
134 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
135 return through::<crate::alerts::SecurityAlert>(op, as_is);
136 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look137 _ => {}
138 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar139 let mut sent = as_services_send(example);
140 // A pull request's code owners are `snake_case` inside it.
141 if op == Op::GetPullRequest
142 && let Some(code_owners) = example.get("code_owners")
143 {
144 sent["codeOwners"] = code_owners.clone();
145 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API146 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'147 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 API148 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
Search across all of g1t, Explore, and a command palette149 Op::Search => through::<search::SearchResults>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look150 Op::GetRepo
151 | Op::CreateRepo
152 | Op::UpdateRepo
153 | Op::TransferRepo
154 | Op::RenameRepo
155 | Op::RenameBranch
156 | Op::ArchiveRepo
157 | Op::UnarchiveRepo
158 | Op::SetRepoVisibility
159 | Op::RestoreRepo => through::<repos::Repo>(op, sent),
160 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
161 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 API162 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents163 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 API164 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
165 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
166 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
167 through::<work::Issue>(op, sent)
168 }
169 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 step170 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 API171 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar172 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
173 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
174 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
175 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
176 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
177 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 API178 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar179 Op::MarkPullRequestReady
180 | Op::ClosePullRequest
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts181 | Op::ReopenPullRequest
182 | Op::ConvertPullRequestToDraft
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar183 | Op::MergePullRequest
184 | Op::AssignIssue
185 | Op::RequestReviewers
186 | Op::RemoveRequestedReviewers => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API187 through::<work::Pull>(op, sent)
188 }
189 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
190 Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
191 Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
192 Op::GetJobLogs => through::<actions::JobLog>(op, sent),
193 Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
194 through::<actions::WorkflowRun>(op, sent)
195 }
196 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents197 Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
198 Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
199 Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
200 Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
201 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 API202 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
203 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
204 Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
205 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 look206 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
207 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
208 }
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)209 Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look210 Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent),
211 Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => {
212 through::<g1t_contracts::identity::Invite>(op, sent)
213 }
214 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)215 Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent),
API: notifications over REST and MCP, with notifications scopes216 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
217 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
218 through::<g1t_contracts::inbox::InboxThread>(op, sent)
219 }
220 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
221 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
222 }
Merge checks: statuses and check runs on every commit223 Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent),
224 Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent),
225 Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent),
226 Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => {
227 through::<checks::CommitCheckRun>(op, sent)
228 }
229 Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent),
230 Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent),
231 Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent),
232 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 API233 _ => sent,
234 }
235}
236
237/// Every key of `example`, as paths, outside the maps passed through.
238fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
239 match value {
240 Value::Object(fields) => {
241 for (key, value) in fields {
242 let here = format!("{path}.{key}");
243 out.push(here.clone());
244 let user_keyed = value.is_object()
245 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
246 if !user_keyed {
247 paths(value, &here, out);
248 }
249 }
250 }
251 Value::Array(items) => {
252 for item in items {
253 paths(item, &format!("{path}[]"), out);
254 }
255 }
256 _ => {}
257 }
258}
259
260#[test]
261fn no_route_answers_with_camel_case() {
262 let document = document();
263 let (mut checked, mut converted) = (0, 0);
264 for (path, methods) in document["paths"].as_object().unwrap() {
265 for (method, operation) in methods.as_object().unwrap() {
266 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 step267 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 API268 let Some(op) = Op::by_name(tool) else {
269 // Device sign-in, which is written in `snake_case` by hand.
270 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
271 continue;
272 };
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar273 let mut sample = sample(op, example);
274 // Each person gets their chosen case on the way out (people.rs).
275 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 API276 converted += wire::camel_case_keys(&sample).len();
277 let sent = wire::snake_case(sample);
278 let leaked = wire::camel_case_keys(&sent);
279 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
280 // The reference shows what is sent: each of its names is one.
281 let (mut shown, mut real) = (Vec::new(), Vec::new());
282 paths(example, "", &mut shown);
283 paths(&sent, "", &mut real);
284 for name in shown {
285 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
286 }
287 checked += 1;
288 }
289 }
290 assert!(checked >= Op::ALL.len());
291 // The samples are in the services' spelling, so there was something to
292 // convert.
293 assert!(converted > 100, "{converted}");
294}
295
296#[test]
297fn every_route_has_a_sample() {
298 let document = document();
299 for route in crate::rest::ROUTES {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts300 if route.no_content() {
301 continue;
302 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API303 let path = route
304 .path
305 .split('/')
306 .map(|segment| match segment.strip_prefix(':') {
307 Some(name) => format!("{{{name}}}"),
308 None => segment.to_owned(),
309 })
310 .collect::<Vec<_>>()
311 .join("/");
312 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
313 ["content"]["application/json"]["example"];
314 assert!(!example.is_null(), "{} {path}", route.method);
315 }
316}
317
318#[test]
319fn errors_and_reports_are_snake_case() {
320 let failure = g1t_contracts::Failure {
321 code: g1t_contracts::FailureCode::NotFound,
322 message: "No such endpoint.".to_owned(),
323 };
324 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
325}
326
327#[test]
328fn a_job_spec_keeps_github_s_spelling() {
329 let spec = json!({
330 "job": "job_1",
331 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
332 "workflow": { "env": { "nodeEnv": "x" } },
333 "github": { "eventName": "push", "headRef": "" },
334 "event": { "pull_request": { "headSha": "x" } },
335 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
336 "checkout": { "ref": "main" },
337 "timeoutMinutes": 30,
338 "masks": [],
339 });
340 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
341 assert_eq!(sent["timeout_minutes"], 30);
342 assert!(sent.get("timeoutMinutes").is_none());
343 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
344 assert_eq!(sent[kept], spec[kept], "{kept}");
345 }
346}

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