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

Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1//! The MCP server's tools: a few resource tools, each with an `action`.
2//!
3//! Every operation is one action of one tool. A call is dispatched to the
4//! operation it names, so permissions, the audit log, billing and outcomes
5//! are exactly those of the REST API. A token sees only the actions its
6//! scopes allow, and a tool none of whose actions it may use is not listed.
7//!
8//! The listed input schema is one flat object: `action`, then every field
9//! any of its actions takes. Which fields each action needs is in the
10//! `action` field's description and checked on every call. Claude's API,
11//! and so most MCP clients, refuse a tool whose input schema has `oneOf`
12//! at its top level, so the schema keyed by action, with each action's
13//! required fields, is [`discriminated`], published on the server's card
14//! and in the docs.
15
16use g1t_contracts::credentials::NEVER;
17use g1t_contracts::identity::AgentScope;
18use g1t_contracts::scopes::{Level, NO_SCOPE, TokenAccess, scope_for};
19use serde_json::{Map, Value, json};
20
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9721use crate::about::AboutOp;
22use crate::deployments::DeploymentsOp;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step23use crate::operations::Op;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge24use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar25use crate::security::SecurityOp;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step26
27pub struct Action {
28 pub name: &'static str,
29 pub op: Op,
30 /// One line, for the `action` field's description.
31 pub summary: &'static str,
32}
33
34pub struct Tool {
35 pub name: &'static str,
36 pub title: &'static str,
37 /// What it is for, in a sentence or two.
38 pub description: &'static str,
39 pub actions: &'static [Action],
40 /// The action a call without one runs.
41 pub default_action: Option<&'static str>,
42}
43
44const fn a(name: &'static str, op: Op, summary: &'static str) -> Action {
45 Action { name, op, summary }
46}
47
48pub const TOOLS: &[Tool] = &[
49 Tool {
50 name: "search",
51 title: "Search",
52 description: "Find things. `code` (the default) searches all of g1t you can see: repositories, code, issues, pull requests and people, with qualifiers like repo:owner/name, language:rust, is:issue. `context` searches one workspace's catalog, docs, issues and memory by meaning.",
53 default_action: Some("code"),
54 actions: &[
55 a("code", Op::Search, "Search all of g1t: repositories, code, issues, pull requests, people"),
56 a("context", Op::SearchContext, "Search a workspace's context hub by meaning"),
57 a("entity", Op::GetEntity, "One catalog entry and its relations"),
58 a("ticket", Op::GetContext, "A Jira, Linear or Sentry item the work refers to, as it is now"),
59 ],
60 },
61 Tool {
62 name: "repository",
63 title: "Repositories",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9764 description: "Repositories: find, read and create them, change their settings and rulesets (what may happen to branches and tags, and what a pull request needs to merge), check their CODEOWNERS file, manage their labels and milestones, see and dismiss their security alerts (secrets and vulnerable dependencies), read what their default branch says (languages, contributors, license), star them, and publish releases. Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step65 default_action: None,
66 actions: &[
67 a("list", Op::ListRepos, "Repositories you can see"),
68 a("get", Op::GetRepo, "One repository"),
69 a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"),
70 a("update", Op::UpdateRepo, "Change description, website, topics, default branch, protection"),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge71 a("get_settings", Op::GetRepoSettings, "How pull requests merge, and the default branch's protection as its rules stack"),
72 a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge and the default branch protection ruleset"),
73 a("check_names", Op::ListCheckNames, "Check names reported lately, to require in a ruleset"),
74 a("list_rulesets", Op::Rules(RulesOp::ListRepoRulesets), "Its rulesets, and its workspace's that hold in it"),
75 a("get_ruleset", Op::Rules(RulesOp::GetRepoRuleset), "One ruleset"),
76 a("create_ruleset", Op::Rules(RulesOp::CreateRepoRuleset), "Create a ruleset for its branches or tags"),
77 a("update_ruleset", Op::Rules(RulesOp::UpdateRepoRuleset), "Change a ruleset"),
78 a("delete_ruleset", Op::Rules(RulesOp::DeleteRepoRuleset), "Delete a ruleset"),
79 a("branch_rules", Op::Rules(RulesOp::GetBranchRules), "Every rule that holds for a branch or tag, and where it comes from"),
80 a("rule_evaluations", Op::Rules(RulesOp::ListRuleEvaluations), "How its rules judged pushes and merges, with insights"),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar81 a("codeowners", Op::GetCodeownersErrors, "Problems in its CODEOWNERS file, by line"),
82 a("list_labels", Op::ListLabels, "Labels, with colors and how many issues and pull requests carry each"),
83 a("create_label", Op::CreateLabel, "Create a label"),
84 a("update_label", Op::UpdateLabel, "Rename a label or change its color or description"),
85 a("delete_label", Op::DeleteLabel, "Delete a label, from everything that carries it"),
86 a("add_default_labels", Op::AddDefaultLabels, "Add the default labels it is missing"),
87 a("list_milestones", Op::ListMilestones, "Milestones, with progress and due dates"),
88 a("get_milestone", Op::GetMilestone, "One milestone with its issues and pull requests"),
89 a("create_milestone", Op::CreateMilestone, "Create a milestone"),
90 a("update_milestone", Op::UpdateMilestone, "Change a milestone's title, description, due date or state"),
91 a("delete_milestone", Op::DeleteMilestone, "Delete a milestone"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step92 a("list_events", Op::ListEvents, "Timeline: pushes, issues, pull requests, comments"),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9793 a("languages", Op::About(AboutOp::GetLanguages), "Its languages by bytes, with colors and percentages"),
94 a("contributors", Op::About(AboutOp::ListContributors), "Who made it: commits per person, agent and author, by week"),
95 a("license", Op::About(AboutOp::GetLicense), "The license its LICENSE file holds"),
96 a("stargazers", Op::About(AboutOp::ListStargazers), "Who starred it"),
97 a("starred", Op::About(AboutOp::CheckStarred), "Whether you starred it, and how many have"),
98 a("star", Op::About(AboutOp::Star), "Star it"),
99 a("unstar", Op::About(AboutOp::Unstar), "Take your star back"),
100 a("list_starred", Op::About(AboutOp::ListStarred), "Repositories you starred"),
101 a("list_releases", Op::About(AboutOp::ListReleases), "Releases, newest first"),
102 a("latest_release", Op::About(AboutOp::GetLatestRelease), "The latest release"),
103 a("get_release", Op::About(AboutOp::GetRelease), "One release by id"),
104 a("get_release_by_tag", Op::About(AboutOp::GetReleaseByTag), "The release of a tag"),
105 a("create_release", Op::About(AboutOp::CreateRelease), "Publish a release of a tag, making the tag if needed"),
106 a("update_release", Op::About(AboutOp::UpdateRelease), "Change a release's title, notes, draft or prerelease"),
107 a("delete_release", Op::About(AboutOp::DeleteRelease), "Delete a release; its tag stays"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step108 a("rename_branch", Op::RenameBranch, "Rename a branch"),
109 a("rename", Op::RenameRepo, "Rename it; old addresses redirect"),
110 a("transfer", Op::TransferRepo, "Move it to another workspace you own"),
111 a("archive", Op::ArchiveRepo, "Make it read-only"),
112 a("unarchive", Op::UnarchiveRepo, "Make it writable again"),
113 a("set_visibility", Op::SetRepoVisibility, "Make it public or private"),
114 a("delete", Op::DeleteRepo, "Delete it; restorable for 30 days"),
115 a("list_deleted", Op::ListDeletedRepos, "A workspace's deleted repositories"),
116 a("restore", Op::RestoreRepo, "Restore a deleted one"),
117 a("purge", Op::PurgeRepo, "Remove a deleted one for good"),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily118 a("security_alerts", Op::ListSecurityAlerts, "Secret and dependency alerts, filtered by state"),
119 a("dismiss_alert", Op::DismissSecurityAlert, "Dismiss an alert with a reason"),
120 a("reopen_alert", Op::ReopenSecurityAlert, "Reopen a dismissed alert"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step121 ],
122 },
123 Tool {
124 name: "issue",
125 title: "Issues",
126 description: "Issues: what should change. Read one before working on it to see the pull requests already made for it. Issues and pull requests share numbers; `comment` works on either.",
127 default_action: None,
128 actions: &[
129 a("list", Op::ListIssues, "Issues on a repository, newest first"),
Fast pages, required checks on the branch, self-hosted runners, honest incidents130 a("get", Op::GetIssue, "One issue with comments and its pull requests"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step131 a("create", Op::CreateIssue, "Open an issue"),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar132 a("update", Op::UpdateIssue, "Change title, body, labels, milestone or assignees"),
133 a("labels", Op::ListIssueLabels, "The labels an issue or pull request carries"),
134 a("add_labels", Op::AddIssueLabels, "Add labels to an issue or pull request"),
135 a("set_labels", Op::SetIssueLabels, "Replace the labels of an issue or pull request"),
136 a("remove_labels", Op::RemoveIssueLabels, "Take labels off an issue or pull request"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step137 a("close", Op::CloseIssue, "Close it without a pull request"),
138 a("reopen", Op::ReopenIssue, "Reopen it"),
139 a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"),
140 a("import", Op::ImportIssue, "Open an issue from a Jira, Linear or Sentry item"),
141 ],
142 },
143 Tool {
144 name: "pull_request",
145 title: "Pull requests",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar146 description: "Pull requests: start a change for an issue, record your session, mark it ready, ask people and teams to review, review and merge. Read `overlaps` and `behind` on `get` before going far, and `code_owners` for whose approval it needs.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step147 default_action: None,
148 actions: &[
149 a("list", Op::ListPullRequests, "Pull requests on a repository, newest first"),
Fast pages, required checks on the branch, self-hosted runners, honest incidents150 a("get", Op::GetPullRequest, "Status, checks and required checks, reviews, overlaps, whether it is behind"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step151 a("changes", Op::GetPullRequestChanges, "Files and line-by-line diff"),
152 a("create", Op::CreatePullRequest, "Start a draft with its own fork to push to, or open one from a pushed branch"),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar153 a("update", Op::UpdatePullRequest, "Change its base branch, labels, milestone, assignees or reviewers"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step154 a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"),
155 a("read_session", Op::ReadSession, "Its recorded session"),
156 a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar157 a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"),
158 a("remove_requested_reviewers", Op::RemoveRequestedReviewers, "Stop asking people or teams to review it"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step159 a("review", Op::ReviewPullRequest, "Approve or request changes"),
160 a("close", Op::ClosePullRequest, "Close without merging"),
161 a("merge", Op::MergePullRequest, "Land it, or join the merge queue"),
162 a("merge_queue", Op::GetMergeQueue, "The repository's merge queue"),
163 ],
164 },
165 Tool {
166 name: "agent",
167 title: "g1t agents",
168 description: "Put g1t's agent to work and talk to it. One agent per issue; to do more at once, use more issues. Starting an agent uses the workspace's money.",
169 default_action: None,
170 actions: &[
171 a("delegate", Op::Delegate, "Open an issue and put an agent on it in one step"),
172 a("assign", Op::AssignIssue, "Put an agent on an existing issue"),
173 a("message", Op::MessageAgent, "Tell the agent on a pull request something, or ask another agent"),
174 a("answer", Op::AnswerMessage, "Answer a question or handoff sent to you"),
175 a("take_messages", Op::TakeMessages, "For a g1t agent: messages not seen yet"),
176 ],
177 },
178 Tool {
179 name: "plan",
180 title: "Plans",
Fast pages, required checks on the branch, self-hosted runners, honest incidents181 description: "Turn an outcome into issues: an agent proposes them with what done means and their dependencies; nothing opens until you apply the plan.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step182 default_action: None,
183 actions: &[
184 a("create", Op::PlanWork, "Ask an agent for a plan; read it with get until ready"),
185 a("get", Op::GetPlan, "A plan and the issues it proposes"),
186 a("apply", Op::ApplyPlan, "Open its issues; with assign, agents start in dependency order"),
187 ],
188 },
189 Tool {
190 name: "memory",
191 title: "Memory",
192 description: "What the project and its workspace remember for the next agent: how to build, conventions, decisions, traps. Recall before you start; remember one short fact at a time, never a secret.",
193 default_action: None,
194 actions: &[
195 a("recall", Op::Recall, "Search memory, or list it all"),
196 a("remember", Op::Remember, "Save one fact"),
197 ],
198 },
199 Tool {
200 name: "workflow",
201 title: "Workflows",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97202 description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Deployments wherever they run (reported from any CI, made by jobs with an `environment:`, or built on g1t.page), their statuses and environments, and reporting your own. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step203 default_action: None,
204 actions: &[
205 a("list", Op::ListWorkflows, "Workflows on the default branch"),
206 a("list_runs", Op::ListWorkflowRuns, "Runs, newest first"),
207 a("get_run", Op::GetWorkflowRun, "One run with its jobs and steps"),
208 a("job_logs", Op::GetJobLogs, "A job's log after a sequence number"),
209 a("dispatch", Op::DispatchWorkflow, "Run a workflow_dispatch workflow"),
210 a("cancel", Op::CancelWorkflowRun, "Cancel a run"),
211 a("rerun", Op::RerunWorkflowRun, "Run a finished run again"),
212 a("update", Op::UpdateWorkflow, "Turn a workflow on or off"),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97213 a("list_deployments", Op::Deployments(DeploymentsOp::ListDeployments), "Deployments wherever they run, newest first, filtered"),
214 a("get_deployment", Op::Deployments(DeploymentsOp::GetDeployment), "One deployment with every status it has had"),
215 a("create_deployment", Op::Deployments(DeploymentsOp::CreateDeployment), "Report a deployment of a ref to an environment"),
216 a("deployment_statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), "A deployment's statuses, newest first"),
217 a("create_deployment_status", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), "Report where a deployment is: in_progress, success, failure"),
218 a("list_environments", Op::Deployments(DeploymentsOp::ListEnvironments), "Environments with their current and latest deployments"),
219 a("get_environment", Op::Deployments(DeploymentsOp::GetEnvironment), "One environment by name"),
Fast pages, required checks on the branch, self-hosted runners, honest incidents220 a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"),
221 a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"),
222 a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"),
223 a("list_runner_groups", Op::ListRunnerGroups, "A workspace's runner groups"),
224 a("create_runner_group", Op::CreateRunnerGroup, "Make a group, for some repositories"),
225 a("update_runner_group", Op::UpdateRunnerGroup, "Rename a group or change its repositories"),
226 a("delete_runner_group", Op::DeleteRunnerGroup, "Delete a group; its runners join the default"),
227 a("get_runner_settings", Op::GetRunnerSettings, "Where agent work runs; whether forks may use runners"),
228 a("update_runner_settings", Op::UpdateRunnerSettings, "Change them"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step229 ],
230 },
231 Tool {
232 name: "secret",
233 title: "Secrets and variables",
234 description: "A repository's or workspace's secrets and variables, read by workflows and deployments. Secret values are never returned.",
235 default_action: None,
236 actions: &[
237 a("list_secrets", Op::ListActionsSecrets, "Secrets, without values"),
238 a("set_secret", Op::SetActionsSecret, "Add or change a secret"),
239 a("delete_secret", Op::DeleteActionsSecret, "Remove a secret"),
240 a("list_variables", Op::ListActionsVariables, "Variables, with values"),
241 a("set_variable", Op::SetActionsVariable, "Add or change a variable"),
242 a("delete_variable", Op::DeleteActionsVariable, "Remove a variable"),
243 ],
244 },
245 Tool {
246 name: "webhook",
247 title: "Webhooks",
248 description: "HTTPS addresses sent signed events as they happen, for a repository or a whole workspace.",
249 default_action: None,
250 actions: &[
251 a("list", Op::ListWebhooks, "Webhooks, without secrets"),
252 a("create", Op::CreateWebhook, "Register one; a ping is sent"),
253 a("update", Op::UpdateWebhook, "Change address, events or active"),
254 a("delete", Op::DeleteWebhook, "Remove one"),
255 a("ping", Op::PingWebhook, "Send a ping"),
256 a("list_deliveries", Op::ListWebhookDeliveries, "Latest deliveries"),
257 a("redeliver", Op::RedeliverWebhook, "Send a delivery again"),
258 ],
259 },
260 Tool {
261 name: "access",
262 title: "Who has access",
263 description: "Who has access to a repository and with which role (read, triage, write, maintain, admin), outside collaborators, and a workspace's base permission.",
264 default_action: None,
265 actions: &[
266 a("list_collaborators", Op::ListCollaborators, "Everyone with a role, and pending invitations"),
267 a("get_permission", Op::GetCollaboratorPermission, "One person's role and capabilities"),
268 a("add_collaborator", Op::AddCollaborator, "Give someone a role, by username or email"),
269 a("update_collaborator", Op::UpdateCollaborator, "Change a direct role"),
270 a("remove_collaborator", Op::RemoveCollaborator, "Take away a direct role"),
271 a("list_invitations", Op::ListRepoInvitations, "Pending invitations to a repository"),
272 a("revoke_invitation", Op::RevokeRepoInvitation, "Withdraw one"),
273 a("set_base_permission", Op::SetBasePermission, "What every member gets on each repository"),
274 a("list_outside_collaborators", Op::ListOutsideCollaborators, "People with roles who are not members"),
275 ],
276 },
277 Tool {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar278 name: "team",
279 title: "Teams",
280 description: "Teams: groups of a workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. Name one by `workspace` and its slug (`team`). Any member may create a team; the workspace's owners and the team's maintainers manage it. A secret team is seen only by its people and the owners.",
281 default_action: None,
282 actions: &[
283 a("list", Op::ListTeams, "A workspace's teams you can see"),
284 a("get", Op::GetTeam, "One team"),
285 a("create", Op::CreateTeam, "Create a team; you become its maintainer"),
286 a("update", Op::UpdateTeam, "Change its name, slug, description, visibility, parent or notifications"),
287 a("delete", Op::DeleteTeam, "Delete it; its child teams move up"),
288 a("list_members", Op::ListTeamMembers, "Its people and their roles, child teams' with include_child_teams"),
289 a("set_member", Op::SetTeamMember, "Add a member of the workspace, or change their role"),
290 a("remove_member", Op::RemoveTeamMember, "Take someone out of it"),
291 a("list_child_teams", Op::ListChildTeams, "The teams nested under it"),
292 a("list_repos", Op::ListTeamRepos, "The repositories it has a role on"),
293 a("set_repo", Op::SetTeamRepo, "Give it a role on a repository"),
294 a("remove_repo", Op::RemoveTeamRepo, "Take its role on a repository away"),
295 a("set_review_assignment", Op::SetTeamReviewAssignment, "Whom it picks when asked to review"),
296 a("list_user_teams", Op::ListUserTeams, "The teams someone is in"),
297 ],
298 },
299 Tool {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step300 name: "workspace",
301 title: "Workspaces",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97302 description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, set rulesets that hold across its repositories, read and change its projects (what each is, where it runs, its links), and keep your own pinned projects at the top of its sidebar.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step303 default_action: None,
304 actions: &[
Merge branch 'worktree-agent-ad7c6d88d93adc817'305 a("get", Op::GetWorkspace, "A workspace's details and settings"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step306 a("create", Op::CreateWorkspace, "Create a workspace"),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member307 a("delete", Op::DeleteWorkspace, "Delete a workspace and everything in it (support can restore it for 30 days)"),
Merge branch 'worktree-agent-ad7c6d88d93adc817'308 a("update", Op::UpdateWorkspace, "Change its name, description, base permission or who may create teams"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step309 a("list_invites", Op::ListWorkspaceInvites, "Its invites"),
310 a("invite_member", Op::InviteMember, "Invite an email address"),
311 a("revoke_invite", Op::RevokeWorkspaceInvite, "Revoke a pending invite"),
312 a("list_integrations", Op::ListIntegrations, "Model providers, alert sources, trackers"),
313 a("connect_integration", Op::ConnectIntegration, "Connect one"),
AI Gateway: OpenAI's format, open models, and your own providers314 a("update_integration", Op::UpdateIntegration, "Change one: rotate its key, choose its AI Gateway models"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step315 a("disconnect_integration", Op::DisconnectIntegration, "Remove one"),
316 a("test_integration", Op::TestIntegration, "Check its credentials"),
317 a("get_model_routes", Op::GetModelRoutes, "Where each kind of work's model requests go"),
318 a("set_model_routes", Op::SetModelRoutes, "Replace them"),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97319 a("list_projects", Op::ListProjects, "Its projects you can see: what each is, where it runs, its links"),
320 a("get_project", Op::GetProject, "One project"),
321 a("update_project", Op::UpdateProject, "Change a project's name, description, kind, where it runs or its links"),
API: pinned projects over REST and MCP322 a("list_pinned_projects", Op::ListPinnedProjects, "Your pinned projects in it, in your order"),
323 a("pin_project", Op::PinProject, "Pin a project, at a position or the end"),
324 a("unpin_project", Op::UnpinProject, "Unpin a project"),
325 a("reorder_pinned_projects", Op::ReorderPinnedProjects, "Put your pins in a new order"),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge326 a("list_rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), "Its rulesets, which hold across its repositories"),
327 a("get_ruleset", Op::Rules(RulesOp::GetWorkspaceRuleset), "One of its rulesets"),
328 a("create_ruleset", Op::Rules(RulesOp::CreateWorkspaceRuleset), "Create a ruleset for some or all of its repositories"),
329 a("update_ruleset", Op::Rules(RulesOp::UpdateWorkspaceRuleset), "Change one of its rulesets"),
330 a("delete_ruleset", Op::Rules(RulesOp::DeleteWorkspaceRuleset), "Delete one of its rulesets"),
331 a("rule_evaluations", Op::Rules(RulesOp::ListWorkspaceRuleEvaluations), "How rules judged changes across its repositories"),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step332 ],
333 },
334 Tool {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit335 name: "billing",
336 title: "Billing",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens337 description: "A workspace's billing: its usage by product, project and day, its budget (the monthly spend limit, alerts and whether usage pauses at it), its AI credit, its invoices, and its AI Gateway requests. Amounts are whole millionths of a dollar (`_micros`), or cents (`_cents`) where named. Members read it; changing the budget and buying credit are for owners, as people, and never for g1t's agents.",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit338 default_action: Some("usage"),
339 actions: &[
340 a("usage", Op::GetUsage, "Usage over a range of days, by product, meter, project and day, and what paid for it"),
341 a("budget", Op::GetBudget, "The monthly spend limit, what was spent, alerts and whether usage pauses at the limit"),
342 a("set_budget", Op::SetBudget, "Change the spend limit, alerts, pausing or the alert webhook"),
343 a("ai_credit", Op::GetAiCredit, "AI credit left, its grants, auto-reload and how to buy more"),
344 a("buy_ai_credit", Op::BuyAiCredit, "A payment page to buy AI credit, for a person to open"),
345 a("invoices", Op::ListInvoices, "Every invoice, the itemised usage invoices, and the next one so far"),
346 a("billing_details", Op::GetBillingDetails, "Who invoices are made out to and the payment method on file"),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens347 a("gateway_requests", Op::ListGatewayRequests, "Recent AI Gateway requests: model, tokens, cost, status and token"),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit348 ],
349 },
350 Tool {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar351 name: "security",
352 title: "Security",
353 description: "A repository's security: secret scanning alerts and push protection bypasses, custom secret patterns, code scanning alerts and SARIF uploads, vulnerability alerts, the dependency graph and its SBOM, dependency review, settings, and a workspace's overview. Fix an alert with g1t. Findings are shown to those who can change the code only. Give `repo` (owner/name), or `workspace` for lists across one.",
354 default_action: Some("secret_alerts"),
355 actions: &[
356 a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"),
357 a("secret_alert", Op::Security(SecurityOp::GetSecretAlert), "One secret alert, with where it was found and its bypass requests"),
358 a("update_secret_alert", Op::Security(SecurityOp::UpdateSecretAlert), "Dismiss a secret alert with a reason, or reopen it"),
359 a("secret_locations", Op::Security(SecurityOp::ListSecretLocations), "Every file, line and commit a secret is in"),
360 a("bypass", Op::Security(SecurityOp::BypassPushProtection), "Push past push protection with a reason, or ask to"),
361 a("check_validity", Op::Security(SecurityOp::CheckSecretValidity), "Ask a secret's issuer whether it still works"),
362 a("bypass_requests", Op::Security(SecurityOp::ListBypassRequests), "A workspace's push protection bypass requests"),
363 a("review_bypass", Op::Security(SecurityOp::ReviewBypassRequest), "Approve, deny or cancel a bypass request"),
364 a("patterns", Op::Security(SecurityOp::ListCustomPatterns), "Custom secret patterns of a repository or workspace"),
365 a("create_pattern", Op::Security(SecurityOp::CreateCustomPattern), "Create a custom secret pattern, as a draft or published"),
366 a("update_pattern", Op::Security(SecurityOp::UpdateCustomPattern), "Change, publish or unpublish a custom pattern"),
367 a("delete_pattern", Op::Security(SecurityOp::DeleteCustomPattern), "Delete a custom pattern"),
368 a("dry_run_pattern", Op::Security(SecurityOp::DryRunCustomPattern), "Run a pattern over the default branch without saving it"),
369 a("code_alerts", Op::Security(SecurityOp::ListCodeAlerts), "Code scanning alerts; by state, severity, tool, rule_id"),
370 a("code_alert", Op::Security(SecurityOp::GetCodeAlert), "One code scanning alert by number"),
371 a("update_code_alert", Op::Security(SecurityOp::UpdateCodeAlert), "Dismiss a code scanning alert with a reason, or reopen it"),
372 a("analyses", Op::Security(SecurityOp::ListAnalyses), "Code scanning analyses, newest first"),
373 a("upload_sarif", Op::Security(SecurityOp::UploadSarif), "Upload a SARIF file, gzipped and base64-encoded"),
374 a("sarif_upload", Op::Security(SecurityOp::GetSarifUpload), "Whether a SARIF upload was read, and its analyses"),
375 a("vulnerability_alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), "Vulnerable dependencies; by state, severity, ecosystem, package"),
376 a("vulnerability_alert", Op::Security(SecurityOp::GetVulnerabilityAlert), "One vulnerability alert"),
377 a("update_vulnerability_alert", Op::Security(SecurityOp::UpdateVulnerabilityAlert), "Dismiss a vulnerability alert with a reason, or reopen it"),
378 a("fix", Op::Security(SecurityOp::FixAlert), "Put g1t on an issue to fix an alert"),
379 a("dependency_graph", Op::Security(SecurityOp::GetDependencyGraph), "Every package the lockfiles resolve, direct or transitive"),
380 a("sbom", Op::Security(SecurityOp::GetSbom), "The dependency graph as an SPDX 2.3 document"),
381 a("compare_dependencies", Op::Security(SecurityOp::CompareDependencies), "What changes in dependencies between base...head"),
382 a("settings", Op::Security(SecurityOp::GetSettings), "A repository's security settings"),
383 a("update_settings", Op::Security(SecurityOp::UpdateSettings), "Change when checks fail and dependency review's policy"),
384 a("workspace_settings", Op::Security(SecurityOp::GetWorkspaceSettings), "A workspace's delegated bypass and validity checks"),
385 a("update_workspace_settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), "Turn delegated bypass or validity checks on or off"),
386 a("overview", Op::Security(SecurityOp::GetOverview), "A workspace's alerts, trends and coverage"),
387 ],
388 },
389 Tool {
API: notifications over REST and MCP, with notifications scopes390 name: "notifications",
391 title: "Notifications",
392 description: "Your inbox: what needs you, and what you follow. One thread per issue, pull request, workflow or deployment, with why you were told (`reason`): an agent waiting on you, a review asked of you, an assignment, a mention, your work's checks, or what you subscribe to and watch. Mark threads read or done once handled, and choose what you hear of with subscribe, unsubscribe and watch. Your own: a personal token.",
393 default_action: Some("list"),
394 actions: &[
395 a("list", Op::ListNotifications, "Unread threads, latest first; all, a view, a reason, a repository"),
396 a("get", Op::GetNotificationThread, "One thread with its recent activity and your subscription"),
397 a("mark_read", Op::MarkThreadRead, "Mark a thread read, or unread"),
398 a("mark_all_read", Op::MarkNotificationsRead, "Mark everything read up to a time, or one repository's"),
399 a("done", Op::MarkThreadDone, "Mark a thread done; new activity brings it back"),
400 a("save", Op::SaveThread, "Save a thread, or unsave it"),
401 a("snooze", Op::SnoozeThread, "Snooze a thread until a time, or bring it back"),
402 a("subscription", Op::GetThreadSubscription, "Your subscription to an issue or pull request"),
403 a("subscribe", Op::SetThreadSubscription, "Subscribe to an issue or pull request, or ignore it"),
404 a("unsubscribe", Op::DeleteThreadSubscription, "Unsubscribe until you comment or are mentioned"),
405 a("watching", Op::GetRepoSubscription, "How you watch a repository"),
406 a("watch", Op::SetRepoSubscription, "Watch a repository: participating, all, ignore or custom"),
407 a("unwatch", Op::DeleteRepoSubscription, "Stop watching a repository"),
408 a("watched", Op::ListWatchedRepos, "Repositories you watch other than the default way"),
409 ],
410 },
411 Tool {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step412 name: "account",
413 title: "Your account",
414 description: "Who this token acts as and its workspaces (`whoami`), your email addresses, your invites, and invitations to repositories waiting for you.",
415 default_action: Some("whoami"),
416 actions: &[
417 a("whoami", Op::Whoami, "Who the token acts as, and its workspaces"),
418 a("list_emails", Op::ListEmails, "Your addresses"),
419 a("add_email", Op::AddEmail, "Add an address"),
420 a("remove_email", Op::RemoveEmail, "Remove an address"),
421 a("update_email_settings", Op::UpdateEmailSettings, "Primary, backup and privacy"),
422 a("list_invites", Op::ListInvites, "Your invites to g1t"),
423 a("create_invite", Op::CreateInvite, "Make an invite"),
424 a("revoke_invite", Op::RevokeInvite, "Revoke one"),
425 a("list_repository_invitations", Op::ListMyRepoInvitations, "Invitations to repositories for you"),
426 a("accept_repository_invitation", Op::AcceptRepoInvitation, "Accept one"),
427 a("decline_repository_invitation", Op::DeclineRepoInvitation, "Decline one"),
428 ],
429 },
430];
431
432/// Operations that cannot be undone, or reach beyond g1t's own records:
433/// clients ask before running a tool that has any of them.
434fn destructive(op: Op) -> bool {
435 matches!(
436 op,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar437 Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection)
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge438 | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset)
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar439 | Op::DeleteWorkspace
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily440 | Op::UpdateWorkspace
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step441 | Op::DeleteRepo
442 | Op::PurgeRepo
443 | Op::TransferRepo
444 | Op::SetRepoVisibility
445 | Op::RemoveEmail
446 | Op::RemoveCollaborator
447 | Op::DisconnectIntegration
AI Gateway: OpenAI's format, open models, and your own providers448 | Op::UpdateIntegration
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step449 | Op::DeleteWebhook
450 | Op::DeleteActionsSecret
451 | Op::DeleteActionsVariable
452 | Op::SetActionsSecret
453 | Op::SetActionsVariable
454 | Op::SetModelRoutes
455 | Op::SetBasePermission
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar456 | Op::DeleteTeam
457 | Op::RemoveTeamRepo
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step458 | Op::MergePullRequest
Fast pages, required checks on the branch, self-hosted runners, honest incidents459 | Op::RemoveRunner
460 | Op::DeleteRunnerGroup
461 | Op::UpdateRunnerSettings
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step462 )
463}
464
465/// Whether an operation only reads.
466pub fn reads_only(op: Op) -> bool {
467 NO_SCOPE.contains(&op.name())
468 || scope_for(op.name()).is_some_and(|scope| scope.level() == Level::Read)
469}
470
471/// What decides which actions a caller sees.
472pub enum Gate<'a> {
473 /// No limit beyond the person's own role.
474 Everything,
475 /// A g1t agent's token: the operations its run lists.
476 Agent(&'a AgentScope),
477 /// An access token with scopes.
478 Token(&'a TokenAccess),
479}
480
481impl Gate<'_> {
482 pub fn allows(&self, op: Op) -> bool {
483 match self {
484 Gate::Everything => true,
485 Gate::Agent(scope) => op.allowed_by(scope) && !NEVER.contains(&op.name()),
486 Gate::Token(access) => {
487 if NO_SCOPE.contains(&op.name()) {
488 return true;
489 }
490 match scope_for(op.name()) {
491 Some(scope) => access.allows(scope),
492 None => access.scopes.is_none(),
493 }
494 }
495 }
496 }
497}
498
499impl Tool {
500 pub fn by_name(name: &str) -> Option<&'static Tool> {
501 TOOLS.iter().find(|tool| tool.name == name)
502 }
503
504 pub fn action(&self, name: &str) -> Option<&'static Action> {
505 // The tools are 'static; find through TOOLS to keep the lifetime.
506 TOOLS
507 .iter()
508 .find(|tool| tool.name == self.name)
509 .and_then(|tool| tool.actions.iter().find(|action| action.name == name))
510 }
511
512 pub fn visible(&self, gate: &Gate) -> Vec<&'static Action> {
513 TOOLS
514 .iter()
515 .find(|tool| tool.name == self.name)
516 .map(|tool| tool.actions.iter().filter(|action| gate.allows(action.op)).collect())
517 .unwrap_or_default()
518 }
519
520 /// The flat input schema of the actions given.
521 pub fn input_schema(&self, actions: &[&Action]) -> Value {
522 let mut properties = Map::new();
523 let lines: Vec<String> = actions
524 .iter()
525 .map(|action| {
526 let required: Vec<String> = action.op.required();
527 if required.is_empty() {
528 format!("{}: {}.", action.name, action.summary)
529 } else {
530 format!("{} ({}): {}.", action.name, required.join(", "), action.summary)
531 }
532 })
533 .collect();
534 let mut action_schema = json!({
535 "type": "string",
536 "enum": actions.iter().map(|action| action.name).collect::<Vec<_>>(),
537 "description": lines.join("\n"),
538 });
539 if let Some(default) = self.default_action.filter(|name| actions.iter().any(|action| action.name == *name)) {
540 action_schema["default"] = json!(default);
541 }
542 properties.insert("action".to_owned(), action_schema);
543 for action in actions {
544 for (name, schema) in action.op.properties() {
545 merge_property(&mut properties, name, schema);
546 }
547 }
548 let mut required = vec![];
549 if self.default_action.is_none() {
550 required.push("action");
551 }
552 let mut schema = json!({ "type": "object", "properties": properties });
553 if !required.is_empty() {
554 schema["required"] = json!(required);
555 }
556 schema
557 }
558
559 /// The input schema keyed by action: one `oneOf` branch per action,
560 /// each with its own fields and the ones it needs.
561 pub fn discriminated(&self, actions: &[&Action]) -> Value {
562 let branches: Vec<Value> = actions
563 .iter()
564 .map(|action| {
565 let mut properties = Map::new();
566 properties.insert("action".to_owned(), json!({ "const": action.name }));
567 properties.extend(action.op.properties());
568 let mut required = vec![Value::String("action".to_owned())];
569 // The default action may leave `action` out.
570 if self.default_action == Some(action.name) {
571 required.clear();
572 }
573 required.extend(action.op.required().into_iter().map(Value::String));
574 json!({
575 "title": action.name,
576 "description": action.summary,
577 "type": "object",
578 "properties": properties,
579 "required": required,
580 })
581 })
582 .collect();
583 json!({ "type": "object", "oneOf": branches })
584 }
585
586 /// MCP's hints about the actions given: whether the tool only reads,
587 /// whether it can destroy something, and whether calling it twice is
588 /// the same as once.
589 pub fn annotations(&self, actions: &[&Action]) -> Value {
590 let read_only = actions.iter().all(|action| reads_only(action.op));
591 json!({
592 "title": self.title,
593 "readOnlyHint": read_only,
594 "destructiveHint": !read_only && actions.iter().any(|action| destructive(action.op)),
595 "idempotentHint": read_only,
596 "openWorldHint": false,
597 })
598 }
599
600 /// The tool as `tools/list` gives it, for a caller behind `gate`, or
601 /// `None` when it may use none of its actions.
602 pub fn listed(&self, gate: &Gate) -> Option<Value> {
603 let actions = self.visible(gate);
604 if actions.is_empty() {
605 return None;
606 }
607 Some(json!({
608 "name": self.name,
609 "title": self.title,
610 "description": self.description,
611 "inputSchema": self.input_schema(&actions),
612 "annotations": self.annotations(&actions),
613 }))
614 }
615}
616
617/// Adds a property to a tool's flat schema. The first action to use a name
618/// describes it; a later one with other allowed values adds them.
619fn merge_property(properties: &mut Map<String, Value>, name: String, schema: Value) {
620 match properties.get_mut(&name) {
621 None => {
622 properties.insert(name, schema);
623 }
624 Some(existing) => {
625 if let (Some(Value::Array(had)), Some(Value::Array(more))) =
626 (existing.get("enum").cloned(), schema.get("enum"))
627 {
628 let mut merged = had;
629 for value in more {
630 if !merged.contains(value) {
631 merged.push(value.clone());
632 }
633 }
634 existing["enum"] = Value::Array(merged);
635 }
636 // Different kinds of value under one name: say less, accept both.
637 if existing.get("type") != schema.get("type")
638 && let Some(fields) = existing.as_object_mut()
639 {
640 fields.remove("type");
641 fields.remove("items");
642 }
643 }
644 }
645}
646
647/// What a call to a tool runs: the operation its action names, or why not.
648pub fn resolve(tool: &Tool, arguments: &Value) -> Result<Op, String> {
649 let names = || {
650 tool.actions
651 .iter()
652 .map(|action| action.name)
653 .collect::<Vec<_>>()
654 .join(", ")
655 };
656 let Some(name) = arguments["action"].as_str().or(tool.default_action) else {
657 return Err(format!("Give an action: one of {}.", names()));
658 };
659 let Some(action) = tool.action(name) else {
660 return Err(format!("{} has no action {name}. Its actions: {}.", tool.name, names()));
661 };
662 let missing: Vec<String> = action
663 .op
664 .required()
665 .into_iter()
666 .filter(|field| arguments.get(field).is_none_or(Value::is_null))
667 .collect();
668 if !missing.is_empty() {
669 return Err(format!("{}.{name} needs {}.", tool.name, missing.join(", ")));
670 }
671 Ok(action.op)
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use g1t_contracts::scopes::{Preset, Scope};
678
679 fn listed(gate: &Gate) -> Vec<Value> {
680 TOOLS.iter().filter_map(|tool| tool.listed(gate)).collect()
681 }
682
683 fn token(scopes: Option<Vec<Scope>>) -> TokenAccess {
684 TokenAccess {
685 token_id: "tok_1".to_owned(),
686 scopes: scopes.map(|scopes| scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
687 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens688 name: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step689 }
690 }
691
692 #[test]
693 fn every_operation_is_exactly_one_action_of_one_tool() {
694 for op in Op::ALL {
695 let count = TOOLS
696 .iter()
697 .flat_map(|tool| tool.actions.iter())
698 .filter(|action| action.op == op)
699 .count();
700 assert_eq!(count, 1, "{} is {count} actions", op.name());
701 }
702 for tool in TOOLS {
703 let mut names = std::collections::HashSet::new();
704 for action in tool.actions {
705 assert!(names.insert(action.name), "{}.{} twice", tool.name, action.name);
706 }
707 if let Some(default) = tool.default_action {
708 assert!(tool.action(default).is_some(), "{}", tool.name);
709 }
710 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit711 assert!(TOOLS.len() <= 17, "{} tools", TOOLS.len());
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step712 }
713
714 #[test]
715 fn every_operation_needs_exactly_one_scope_or_none() {
716 use g1t_contracts::scopes::OPERATIONS;
717 for op in Op::ALL {
718 let mapped = OPERATIONS.iter().filter(|(name, _)| *name == op.name()).count();
719 let free = NO_SCOPE.contains(&op.name());
720 assert_eq!(mapped + usize::from(free), 1, "{}", op.name());
721 }
722 for (name, _) in OPERATIONS {
723 assert!(Op::by_name(name).is_some(), "{name} is not an operation");
724 }
725 }
726
727 #[test]
728 fn each_tool_schema_is_valid_with_one_branch_per_action() {
729 for tool in TOOLS {
730 let actions: Vec<&Action> = tool.actions.iter().collect();
731 let flat = tool.input_schema(&actions);
732 assert_eq!(flat["type"], "object");
733 assert!(flat.get("oneOf").is_none(), "no oneOf at the top level");
734 let listed: Vec<&str> = flat["properties"]["action"]["enum"]
735 .as_array()
736 .unwrap()
737 .iter()
738 .map(|name| name.as_str().unwrap())
739 .collect();
740 assert_eq!(listed, tool.actions.iter().map(|action| action.name).collect::<Vec<_>>());
741 for action in tool.actions {
742 for field in action.op.required() {
743 assert!(flat["properties"].get(&field).is_some(), "{}.{}: {field}", tool.name, action.name);
744 }
745 }
746 let keyed = tool.discriminated(&actions);
747 let branches = keyed["oneOf"].as_array().unwrap();
748 assert_eq!(branches.len(), tool.actions.len());
749 for (branch, action) in branches.iter().zip(tool.actions) {
750 assert_eq!(branch["properties"]["action"]["const"], action.name);
751 for field in branch["required"].as_array().unwrap() {
752 assert!(branch["properties"].get(field.as_str().unwrap()).is_some(), "{}.{}: {field}", tool.name, action.name);
753 }
754 }
755 // A well-formed JSON Schema object throughout.
756 let text = serde_json::to_string(&flat).unwrap();
757 assert!(serde_json::from_str::<Value>(&text).is_ok());
758 }
759 }
760
761 #[test]
762 fn a_read_only_token_sees_read_actions_only() {
763 let access = token(Preset::ReadOnly.scopes());
764 let gate = Gate::Token(&access);
765 for tool in TOOLS {
766 for action in tool.visible(&gate) {
767 assert!(reads_only(action.op), "{}.{}", tool.name, action.name);
768 }
769 }
770 let tools = listed(&gate);
771 for tool in &tools {
772 assert_eq!(tool["annotations"]["readOnlyHint"], true, "{}", tool["name"]);
773 assert_eq!(tool["annotations"]["destructiveHint"], false);
774 }
775 let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar776 assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels"]));
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step777 // Nothing of the agent tool is a read.
778 assert!(!tools.iter().any(|tool| tool["name"] == "agent"));
779 }
780
781 #[test]
782 fn a_narrow_token_sees_only_its_tools() {
783 let access = token(Some(vec![Scope::IssuesWrite]));
784 let names: Vec<Value> = listed(&Gate::Token(&access)).into_iter().map(|tool| tool["name"].clone()).collect();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar785 // Labels and milestones are the repository's, managed with issues:write.
786 assert_eq!(names, vec![json!("repository"), json!("issue"), json!("plan"), json!("account")]);
API: notifications over REST and MCP, with notifications scopes787 // Notifications are a resource of their own: reading them lists
788 // only what reads.
789 let reader = token(Some(vec![Scope::NotificationsRead]));
790 let tools = listed(&Gate::Token(&reader));
791 let notifications = tools.iter().find(|tool| tool["name"] == "notifications").unwrap();
792 assert_eq!(
793 notifications["inputSchema"]["properties"]["action"]["enum"],
794 json!(["list", "get", "subscription", "watching", "watched"])
795 );
796 assert_eq!(notifications["annotations"]["readOnlyHint"], true);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step797 let full = token(None);
798 assert_eq!(listed(&Gate::Token(&full)).len(), TOOLS.len());
799 assert_eq!(listed(&Gate::Everything).len(), TOOLS.len());
800 }
801
802 #[test]
803 fn a_tool_that_can_destroy_says_so() {
804 let tools = listed(&Gate::Everything);
805 let repository = tools.iter().find(|tool| tool["name"] == "repository").unwrap();
806 assert_eq!(repository["annotations"]["destructiveHint"], true);
807 assert_eq!(repository["annotations"]["readOnlyHint"], false);
808 let memory = tools.iter().find(|tool| tool["name"] == "memory").unwrap();
809 assert_eq!(memory["annotations"]["destructiveHint"], false);
810 }
811
812 #[test]
813 fn calls_resolve_to_their_operation_or_say_what_is_missing() {
814 let issue = Tool::by_name("issue").unwrap();
815 assert_eq!(resolve(issue, &json!({ "action": "get", "repo": "a/b", "number": 1 })), Ok(Op::GetIssue));
816 assert_eq!(resolve(issue, &json!({ "action": "get", "repo": "a/b" })), Err("issue.get needs number.".to_owned()));
817 assert!(resolve(issue, &json!({})).unwrap_err().starts_with("Give an action"));
818 assert!(resolve(issue, &json!({ "action": "explode" })).unwrap_err().contains("no action explode"));
819 let search = Tool::by_name("search").unwrap();
820 assert_eq!(resolve(search, &json!({ "query": "x" })), Ok(Op::Search));
821 let account = Tool::by_name("account").unwrap();
822 assert_eq!(resolve(account, &json!({})), Ok(Op::Whoami));
823 }
824
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar825 #[test]
826 fn teams_are_one_tool_and_a_workspace_reader_sees_only_its_reads() {
827 let team = Tool::by_name("team").unwrap();
828 let names: Vec<&str> = team.actions.iter().map(|action| action.name).collect();
829 assert_eq!(
830 names,
831 [
832 "list",
833 "get",
834 "create",
835 "update",
836 "delete",
837 "list_members",
838 "set_member",
839 "remove_member",
840 "list_child_teams",
841 "list_repos",
842 "set_repo",
843 "remove_repo",
844 "set_review_assignment",
845 "list_user_teams",
846 ]
847 );
848 let reader = token(Some(vec![Scope::WorkspaceRead]));
849 let tools = listed(&Gate::Token(&reader));
850 let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
851 assert_eq!(
852 listed_team["inputSchema"]["properties"]["action"]["enum"],
853 json!(["list", "get", "list_members", "list_child_teams", "list_repos", "list_user_teams"])
854 );
855 assert_eq!(listed_team["annotations"]["readOnlyHint"], true);
856 // A team's role on a repository is who has access.
857 let admin = token(Some(vec![Scope::WorkspaceAdmin]));
858 let tools = listed(&Gate::Token(&admin));
859 let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
860 let actions = listed_team["inputSchema"]["properties"]["action"]["enum"].as_array().unwrap();
861 assert!(actions.contains(&json!("set_review_assignment")) && !actions.contains(&json!("set_repo")));
862 let access = token(Some(vec![Scope::AccessAdmin]));
863 let tools = listed(&Gate::Token(&access));
864 let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
865 assert_eq!(listed_team["inputSchema"]["properties"]["action"]["enum"], json!(["set_repo", "remove_repo"]));
866 // Both kinds of role a schema names are offered.
867 let roles = &listed(&Gate::Everything).into_iter().find(|tool| tool["name"] == "team").unwrap()["inputSchema"]
868 ["properties"]["role"]["enum"];
869 for role in ["member", "maintainer", "read", "admin"] {
870 assert!(roles.as_array().unwrap().contains(&json!(role)), "{role}");
871 }
872 assert_eq!(
873 resolve(team, &json!({ "action": "set_repo", "workspace": "acme", "team": "backend", "repo": "rocket" })),
874 Err("team.set_repo needs role.".to_owned())
875 );
876 }
877
878 #[test]
879 fn reviewers_and_code_owners_are_actions_of_their_tools() {
880 let pull = Tool::by_name("pull_request").unwrap();
881 assert_eq!(
882 resolve(pull, &json!({ "action": "request_reviewers", "repo": "a/b", "number": 1, "team_reviewers": ["backend"] })),
883 Ok(Op::RequestReviewers)
884 );
885 assert_eq!(pull.action("remove_requested_reviewers").map(|action| action.op), Some(Op::RemoveRequestedReviewers));
886 let repository = Tool::by_name("repository").unwrap();
887 assert_eq!(resolve(repository, &json!({ "action": "codeowners", "repo": "a/b" })), Ok(Op::GetCodeownersErrors));
888 assert!(reads_only(Op::GetCodeownersErrors));
889 assert!(!reads_only(Op::RequestReviewers));
890 }
891
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step892 /// How much smaller `tools/list` is than one tool per operation. Run
893 /// with `--nocapture` to see the numbers.
894 #[test]
895 fn the_tool_list_is_much_smaller_than_one_tool_per_operation() {
896 let before: Vec<Value> = Op::ALL
897 .into_iter()
898 .map(|op| json!({ "name": op.name(), "description": op.description(), "inputSchema": op.input() }))
899 .collect();
900 let after = listed(&Gate::Everything);
901 let before_bytes = serde_json::to_string(&json!({ "tools": before })).unwrap().len();
902 let after_bytes = serde_json::to_string(&json!({ "tools": after })).unwrap().len();
903 let agent = token(Preset::Agent.scopes());
904 let agent_bytes = serde_json::to_string(&json!({ "tools": listed(&Gate::Token(&agent)) })).unwrap().len();
905 let read = token(Preset::ReadOnly.scopes());
906 let read_bytes = serde_json::to_string(&json!({ "tools": listed(&Gate::Token(&read)) })).unwrap().len();
907 println!(
908 "tools/list: before {} tools, {before_bytes} bytes (~{} tokens); after {} tools, {after_bytes} bytes (~{} tokens); agent preset {agent_bytes} bytes (~{} tokens); read only {read_bytes} bytes (~{} tokens)",
909 before.len(),
910 before_bytes / 4,
911 after.len(),
912 after_bytes / 4,
913 agent_bytes / 4,
914 read_bytes / 4,
915 );
916 assert!(after_bytes * 2 < before_bytes, "{after_bytes} vs {before_bytes}");
917 }
918}

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