Skip to content

Commit

The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.

The MCP server drops its agent, plan and customize tools, search's context and entity actions, workspace model routes, billing's gateway_requests and security's fix. Its instructions now describe g1t as the place an agent you bring works: they name all 18 remaining tools (search, repository, issue, pull_request, memory, workflow, observability, package, secret, webhook, access, team, workspace, billing, security, notifications, account, artifact), and a test keeps that list in step with the tools. Recording a session on a pull request, memory recall and remember, and opening a pull request stay. The API reference is regenerated: 413 operations, down from 453.

syntaqxcommitted Parent4ffd59cBrowse files
14 files+126−32340/14 viewed
+7−31
66 //! Reading is for the workspace's members, a workspace's own token
77 //! included. Changing the budget and buying AI credit are for its owners,
88 //! as people: signed in or with a personal access token. A workspace's
9−//! token and g1t's agents never change billing, whatever their scopes say.
9+//! token and a run's token never change billing, whatever their scopes say.
1010
1111 use std::collections::BTreeMap;
1212
102102 }
103103
104104 /// The person who may change a workspace's billing: a person, never a
105−/// workspace's own token or one of g1t's agents. `agent_scoped` is whether
106−/// the request came with an agent's token.
105+/// workspace's own token or a run's token. `agent_scoped` is whether
106+/// the request came with a run's token.
107107 pub(crate) fn person(viewer: &Viewer, agent_scoped: bool) -> std::result::Result<&User, (FailureCode, &'static str)> {
108108 match viewer {
109109 None => Err((FailureCode::Unauthenticated, "This needs a g1t access token.")),
110110 Some(user) if agent_scoped || user.kind == PrincipalKind::Agent => Err((
111111 FailureCode::Forbidden,
112− "g1t's agents never change billing: a workspace's budget and AI credit are for its owners.",
112+ "A run's token never changes billing: a workspace's budget and AI credit are for its owners.",
113113 )),
114114 Some(user) if user.kind != PrincipalKind::User => Err((
115115 FailureCode::Forbidden,
412412 })
413413 }
414414 Op::GetBillingDetails => shaped(g1t_kit::call(billing, "billing_details", &account).await?, &details_json),
415− Op::ListGatewayRequests => {
416− let limit = match &input["limit"] {
417− Value::Null => None,
418− Value::Number(number) => number.as_u64(),
419− Value::String(digits) => digits.trim().parse().ok(),
420− _ => Some(0),
421− };
422− if limit.is_some_and(|limit| !(1..=200).contains(&limit)) {
423− return failed(FailureCode::Invalid, "limit is a number from 1 to 200.");
424− }
425− let before = input["before"].as_str().map(str::trim).filter(|id| !id.is_empty());
426− let page: Outcome<Value> = g1t_kit::call(
427− billing,
428− "gateway_requests",
429− &json!({ "workspace": workspace, "viewer": viewer, "limit": limit, "before": before }),
430− )
431− .await?;
432− shaped(page, &snake)
433− }
434415 _ => failed(FailureCode::Invalid, "Not a billing operation."),
435416 }
436417 }
520501 let agent = User { username: "g1t".into(), kind: PrincipalKind::Agent, ..User::default() };
521502 let (code, message) = person(&Some(agent), false).unwrap_err();
522503 assert_eq!(code, FailureCode::Forbidden);
523− assert!(message.contains("agents never change billing"));
504+ assert!(message.contains("never changes billing"));
524505 let workspace = User { username: "acme".into(), kind: PrincipalKind::Workspace, ..User::default() };
525506 let (code, message) = person(&Some(workspace), false).unwrap_err();
526507 assert_eq!(code, FailureCode::Forbidden);
578559 assert!(shown.get("invoices").is_none() && shown.get("upcoming").is_none());
579560 }
580561
581− const OPS: [Op; 8] = [
562+ const OPS: [Op; 7] = [
582563 Op::GetUsage,
583564 Op::GetBudget,
584565 Op::SetBudget,
586567 Op::BuyAiCredit,
587568 Op::ListInvoices,
588569 Op::GetBillingDetails,
589− Op::ListGatewayRequests,
590570 ];
591571
592572 /// Billing belongs to a workspace, needs someone signed in, and is one
612592 for preset in [Preset::ReadOnly, Preset::Agent] {
613593 let access = token(preset);
614594 let seen: Vec<&str> = tool.visible(&Gate::Token(&access)).iter().map(|action| action.name).collect();
615− assert_eq!(seen, ["usage", "budget", "ai_credit", "invoices", "billing_details", "gateway_requests"], "{}", preset.as_str());
595+ assert_eq!(seen, ["usage", "budget", "ai_credit", "invoices", "billing_details"], "{}", preset.as_str());
616596 }
617− // The AI Gateway's log needs models:read, and nothing of billing's.
618− let models = TokenAccess { scopes: Some(vec!["models:read".into()]), ..token(Preset::Ci) };
619− let seen: Vec<&str> = tool.visible(&Gate::Token(&models)).iter().map(|action| action.name).collect();
620− assert_eq!(seen, ["gateway_requests"]);
621597 let full = TokenAccess::full();
622598 assert_eq!(tool.visible(&Gate::Token(&full)).len(), OPS.len());
623599 }
+0−463
1−//! Customize over REST and MCP: what shapes how a workspace's agents work,
2−//! at `/workspaces/{workspace}/customize/…` and as the MCP `customize` tool.
3−//! Marketplaces the workspace reads plugins from, what Discover lists, the
4−//! plugins it installed and the agents they brought, and its connectors
5−//! (MCP servers connected once, then given to agents), with their keys and
6−//! grants. Also the bundle a job's Claude Code loads on a runner.
7−//!
8−//! The agents service keeps all of it and decides who may do what
9−//! (`services/agents/src/customize-rpc.ts`): members see, owners change,
10−//! and the bundle is read with a job's agent token or a workspace's. The
11−//! bundle holds connectors' secrets: the API hands it on and never keeps
12−//! or logs it. OAuth sign-in to a connector needs a browser, so it stays
13−//! on the site.
14−
15−use g1t_contracts::rules::actor_kind;
16−use g1t_contracts::{FailureCode, Outcome, Viewer};
17−use serde_json::{Value, json};
18−use worker::Result;
19−
20−use crate::operations::Services;
21−
22−/// One operation on a workspace's Customize.
23−#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24−pub enum CustomizeOp {
25− ListMarketplaces,
26− AddMarketplace,
27− RemoveMarketplace,
28− RefreshMarketplace,
29− Discover,
30− ListPlugins,
31− GetPlugin,
32− InstallPlugin,
33− SetPluginEnabled,
34− UninstallPlugin,
35− UpdatePlugin,
36− ListPluginAgents,
37− ListConnectors,
38− GetConnector,
39− AddConnector,
40− SetConnectorKey,
41− RemoveConnector,
42− RefreshConnector,
43− GrantConnector,
44− RevokeConnector,
45− Bundle,
46−}
47−
48−impl CustomizeOp {
49− /// Every one: `Op::ALL` lists each as `Op::Customize(…)`, which a test
50− /// checks against this.
51− #[cfg(test)]
52− pub const ALL: [CustomizeOp; 21] = [
53− CustomizeOp::ListMarketplaces,
54− CustomizeOp::AddMarketplace,
55− CustomizeOp::RemoveMarketplace,
56− CustomizeOp::RefreshMarketplace,
57− CustomizeOp::Discover,
58− CustomizeOp::ListPlugins,
59− CustomizeOp::GetPlugin,
60− CustomizeOp::InstallPlugin,
61− CustomizeOp::SetPluginEnabled,
62− CustomizeOp::UninstallPlugin,
63− CustomizeOp::UpdatePlugin,
64− CustomizeOp::ListPluginAgents,
65− CustomizeOp::ListConnectors,
66− CustomizeOp::GetConnector,
67− CustomizeOp::AddConnector,
68− CustomizeOp::SetConnectorKey,
69− CustomizeOp::RemoveConnector,
70− CustomizeOp::RefreshConnector,
71− CustomizeOp::GrantConnector,
72− CustomizeOp::RevokeConnector,
73− CustomizeOp::Bundle,
74− ];
75−
76− pub fn name(self) -> &'static str {
77− match self {
78− CustomizeOp::ListMarketplaces => "list_marketplaces",
79− CustomizeOp::AddMarketplace => "add_marketplace",
80− CustomizeOp::RemoveMarketplace => "remove_marketplace",
81− CustomizeOp::RefreshMarketplace => "refresh_marketplace",
82− CustomizeOp::Discover => "discover",
83− CustomizeOp::ListPlugins => "list_plugins",
84− CustomizeOp::GetPlugin => "get_plugin",
85− CustomizeOp::InstallPlugin => "install_plugin",
86− CustomizeOp::SetPluginEnabled => "set_plugin_enabled",
87− CustomizeOp::UninstallPlugin => "uninstall_plugin",
88− CustomizeOp::UpdatePlugin => "update_plugin",
89− CustomizeOp::ListPluginAgents => "list_plugin_agents",
90− CustomizeOp::ListConnectors => "list_connectors",
91− CustomizeOp::GetConnector => "get_connector",
92− CustomizeOp::AddConnector => "add_connector",
93− CustomizeOp::SetConnectorKey => "set_connector_key",
94− CustomizeOp::RemoveConnector => "remove_connector",
95− CustomizeOp::RefreshConnector => "refresh_connector",
96− CustomizeOp::GrantConnector => "grant_connector",
97− CustomizeOp::RevokeConnector => "revoke_connector",
98− CustomizeOp::Bundle => "customize_bundle",
99− }
100− }
101−
102− /// The agents service method it passes through to.
103− fn method(self) -> &'static str {
104− match self {
105− CustomizeOp::ListMarketplaces => "marketplaces",
106− CustomizeOp::AddMarketplace => "add_marketplace",
107− CustomizeOp::RemoveMarketplace => "remove_marketplace",
108− CustomizeOp::RefreshMarketplace => "refresh_marketplace",
109− CustomizeOp::Discover => "discover",
110− CustomizeOp::ListPlugins => "plugin_installs",
111− CustomizeOp::GetPlugin => "plugin",
112− CustomizeOp::InstallPlugin => "install_plugin",
113− CustomizeOp::SetPluginEnabled => "set_plugin_enabled",
114− CustomizeOp::UninstallPlugin => "uninstall_plugin",
115− CustomizeOp::UpdatePlugin => "update_plugin",
116− CustomizeOp::ListPluginAgents => "plugin_agents",
117− CustomizeOp::ListConnectors => "connectors",
118− CustomizeOp::GetConnector => "connector",
119− CustomizeOp::AddConnector => "add_connector",
120− CustomizeOp::SetConnectorKey => "set_connector_key",
121− CustomizeOp::RemoveConnector => "remove_connector",
122− CustomizeOp::RefreshConnector => "refresh_connector",
123− CustomizeOp::GrantConnector => "grant_connector",
124− CustomizeOp::RevokeConnector => "revoke_connector",
125− CustomizeOp::Bundle => "customize_bundle",
126− }
127− }
128−
129− /// For the API reference.
130− pub fn title(self) -> &'static str {
131− match self {
132− CustomizeOp::ListMarketplaces => "List marketplaces",
133− CustomizeOp::AddMarketplace => "Add a marketplace",
134− CustomizeOp::RemoveMarketplace => "Remove a marketplace",
135− CustomizeOp::RefreshMarketplace => "Refresh a marketplace",
136− CustomizeOp::Discover => "Discover plugins and skills",
137− CustomizeOp::ListPlugins => "List installed plugins",
138− CustomizeOp::GetPlugin => "Get a plugin",
139− CustomizeOp::InstallPlugin => "Install a plugin",
140− CustomizeOp::SetPluginEnabled => "Switch a plugin on or off",
141− CustomizeOp::UninstallPlugin => "Uninstall a plugin",
142− CustomizeOp::UpdatePlugin => "Update a plugin",
143− CustomizeOp::ListPluginAgents => "List plugin agents",
144− CustomizeOp::ListConnectors => "List connectors",
145− CustomizeOp::GetConnector => "Get a connector",
146− CustomizeOp::AddConnector => "Add a connector",
147− CustomizeOp::SetConnectorKey => "Set a connector's key",
148− CustomizeOp::RemoveConnector => "Remove a connector",
149− CustomizeOp::RefreshConnector => "Refresh a connector's tools",
150− CustomizeOp::GrantConnector => "Give a connector to agents",
151− CustomizeOp::RevokeConnector => "Take a connector back",
152− CustomizeOp::Bundle => "Get the runner bundle",
153− }
154− }
155−
156− pub fn description(self) -> &'static str {
157− match self {
158− CustomizeOp::ListMarketplaces => "The marketplaces a workspace reads plugins from, in the open plugin format (`.claude-plugin/marketplace.json`): each with its `name` (the `@marketplace` part of a plugin's ref), `source` (`github` with `repo`, `g1t` with `repo`, `git` or `url` with `url`, or `builtin`), `builtin` (every workspace has it and it cannot be removed: claude-plugins-official, anthropic-agent-skills and g1t), `about`, `owner`, how many `plugins` it lists, `fetched_at` and `problem` (why its manifest could not be read, when it could not), who added it and when, and `can_remove`. Members only.",
159− CustomizeOp::AddMarketplace => "Add a marketplace by `address`: `owner/repo` on GitHub, `workspace/name` for a repository on this g1t, a git address, or a link to a `marketplace.json`. `name` overrides the one its manifest gives. Its manifest is read at once; a marketplace that cannot be read is refused with `422`, saying why. At most 20 a workspace. Owners only. Recorded in the audit log.",
160− CustomizeOp::RemoveMarketplace => "Remove a marketplace by its name (`marketplace`). Plugins installed from it stay installed, but get no updates and cannot be installed again until it is added back. Built-in marketplaces cannot be removed (`409`). Owners only. Recorded in the audit log.",
161− CustomizeOp::RefreshMarketplace => "Read a marketplace's manifest again now, by its name (`marketplace`), and return it as list_marketplaces gives it. Marketplaces are refreshed on their own in the background; this is for seeing a change at once. Owners only.",
162− CustomizeOp::Discover => "What a workspace can add, from every marketplace it reads: `plugins`, one per listing (`ref` as `name@marketplace`, `display_name`, `description`, `version`, `author`, `category`, `tags`, how g1t would `fetch` it, and `availability`: `available`, `installed` with `enabled`, `unavailable` with `why` for sources g1t cannot read such as npm or a command, or `soon` for g1t's own announced ones), `skills`, one per skill inside those plugins (`ref` as `plugin:skill`, with `added` when the library already has it), and the `marketplaces` themselves. Members only.",
163− CustomizeOp::ListPlugins => "The plugins a workspace installed, switched off ones included: each with its `ref` (`name@marketplace`), `name`, `marketplace`, `display_name`, `description`, `version`, the commit it was read at (`sha`), how it is fetched (`fetch`), what it brought (`components`: skills, agents, commands, mcp_servers, and whether it carries hooks, lsp, bin or output_styles, which only Claude Code on a runner uses), `problems` met when installing, `enabled`, who installed it and when, and `update` when the marketplace lists a newer version. Members only.",
164− CustomizeOp::GetPlugin => "One plugin's page by `ref` (`name@marketplace`, URL-encoded in a path: `@` as `%40`), installed or not: its marketplace `listing`, its `install` if any, its `components` and `readme` read from its source, its `manifest`, its subagents in full (`agents`) for hiring from, `problems`, and the marketplace it is listed in (`source`). Reads the plugin's files, so it takes a moment. Members only.",
165− CustomizeOp::InstallPlugin => "Install a plugin by `ref` (`name@marketplace`) from a marketplace the workspace reads, at the commit its listing names, which it stays pinned to. Its skills and commands join the workspace's library as `plugin:skill`, its subagents become templates to hire from, and the remote MCP servers of its `.mcp.json` become connectors, unconnected until an owner signs in or gives a key; its hooks, LSP servers, output styles and binaries reach Claude Code on runners and never g1t's own agents. A plugin g1t cannot fetch (npm, a command, a git host g1t does not read) is refused with `422`. At most 100 a workspace. Owners only. Recorded in the audit log.",
166− CustomizeOp::SetPluginEnabled => "Switch an installed plugin on or off by `ref`: `enabled` false keeps it installed but takes its skills, agents and connectors away from agents and leaves it out of runners' bundles; true brings them back. Owners only. Recorded in the audit log.",
167− CustomizeOp::UninstallPlugin => "Uninstall a plugin by `ref`: its skills leave the library, its agent templates go, and the connectors it brought are removed with their secrets. Agents hired from its templates stay. Owners only. Recorded in the audit log.",
168− CustomizeOp::UpdatePlugin => "Update an installed plugin by `ref`: read it again at the commit its marketplace now lists and pin it there. Its skills get new versions and keep their attachments, skills it no longer has are archived, its agent templates and components are rewritten, and a server it already brought keeps its connector; new remote servers become connectors. A plugin already at its marketplace's commit comes back unchanged. list_plugins shows `update` when there is one to take. Owners only. Recorded in the audit log.",
169− CustomizeOp::ListPluginAgents => "The subagents installed plugins brought, as templates to hire an agent from: each with its `ref` (`plugin:agent`), `plugin`, `marketplace`, `name`, `description`, the system prompt as `prompt`, and its `model`, `skills` and `tools`. Members only.",
170− CustomizeOp::ListConnectors => "A workspace's connectors: MCP servers connected once and given to agents. Each has its `id` (`con_…`), `name` (tools are offered to agents as `<name>__<tool>`), `title`, `url`, `category`, `auth` (`none`, `header` or `oauth`), `state` (`connected`, `needs_sign_in`, `needs_key` or `problem`, with `problem` saying why), `from` (the directory, a plugin's server, an address, or one agent's own server), its `tools` as last listed and `checked_at`, `header_name` for a key (the key itself is never returned), who it is `signed_in_as` for OAuth, its `grants` (where it reaches: every agent, a team or one agent), and who added it. g1t's own integrations (GitHub, trackers, alert sources, model providers) are list_integrations. Members only.",
171− CustomizeOp::GetConnector => "One connector by `id`, in the shape list_connectors gives. Members only.",
172− CustomizeOp::AddConnector => "Add a connector. `from` says where it comes from: `{\"kind\": \"directory\", \"id\"}` for one of the directory's remote MCP servers, `{\"kind\": \"plugin\", \"ref\", \"server\"}` for a server an installed plugin names, or `{\"kind\": \"address\", \"name\", \"url\"}` for any MCP server over HTTP (`name` is 2 to 32 lowercase letters, digits and hyphens). `auth` is `none`, `header` (give `header` with the header's `name` and `value`, kept sealed and never returned) or `oauth` (an owner then signs in on the site: sign-in needs a browser, so it has no route). Its tools are listed as soon as it can be reached. At most 50 a workspace. Owners only. Recorded in the audit log.",
173− CustomizeOp::SetConnectorKey => "Put a key on a connector by `id`: `value` is the header's value, kept sealed and never returned, and `header_name` the header it is sent as (the one it had, or Authorization, when left out). Makes the connector use header auth, lists its tools again, and returns it. Owners only. Recorded in the audit log.",
174− CustomizeOp::RemoveConnector => "Remove a connector by `id`, with its secrets and grants. Agents that had it lose its tools at their next turn. Owners only. Recorded in the audit log.",
175− CustomizeOp::RefreshConnector => "Ask a connector's server for its tools again now, by `id`, and return the connector with its `tools`, `checked_at` and `state` as they are now. Owners only.",
176− CustomizeOp::GrantConnector => "Give a connector by `id` to agents: `scope` is `workspace` (every agent), `team` (with `target`, the team's slug) or `agent` (with `target`, the agent's handle). Returns the connector with its `grants`. Owners only. Recorded in the audit log.",
177− CustomizeOp::RevokeConnector => "Take a connector back from where one grant reaches, by the connector's `id` and the grant's id (`grant`). Returns the connector with its `grants`. Owners only. Recorded in the audit log.",
178− CustomizeOp::Bundle => "What a job's Claude Code loads from the workspace's Customize, read once per job with the job's agent token (or a workspace's token; a person's token is refused): `plugins` to fetch at their pinned commit (`ref`, `name`, `fetch`, `version`; switched-off ones left out), the library `skills` that reach the job's agent as files (each `name` with its `files`: `path`, `content`, `encoding` utf8 or base64; a plugin's skills come with the plugin), and the `connectors` that reach it and are connected, each with its `name`, `url` and the `headers` that sign it in. Secrets are in it: it is for the runner, and g1t never keeps or logs it. See self-hosted runners, \"What a run gets from Customize\".",
179− }
180− }
181−
182− pub fn input(self) -> Value {
183− let workspace = json!({ "type": "string", "description": "The workspace's slug." });
184− let marketplace = json!({ "type": "string", "description": "The marketplace's name, as list_marketplaces gives it." });
185− let plugin_ref = json!({ "type": "string", "description": "The plugin as name@marketplace, such as commit-commands@claude-plugins-official." });
186− let id = json!({ "type": "string", "description": "The connector's id (con_…), from list_connectors." });
187− let (properties, required): (Value, &[&str]) = match self {
188− CustomizeOp::ListMarketplaces
189− | CustomizeOp::Discover
190− | CustomizeOp::ListPlugins
191− | CustomizeOp::ListPluginAgents
192− | CustomizeOp::ListConnectors
193− | CustomizeOp::Bundle => (json!({ "workspace": workspace }), &["workspace"]),
194− CustomizeOp::AddMarketplace => (
195− json!({
196− "workspace": workspace,
197− "address": { "type": "string", "description": "owner/repo on GitHub, workspace/name for a repository here, a git address, or a link to a marketplace.json." },
198− "name": { "type": "string", "description": "What to call it here. The name its manifest gives when left out." },
199− }),
200− &["workspace", "address"],
201− ),
202− CustomizeOp::RemoveMarketplace | CustomizeOp::RefreshMarketplace => {
203− (json!({ "workspace": workspace, "marketplace": marketplace }), &["workspace", "marketplace"])
204− }
205− CustomizeOp::GetPlugin | CustomizeOp::InstallPlugin | CustomizeOp::UninstallPlugin | CustomizeOp::UpdatePlugin => {
206− (json!({ "workspace": workspace, "ref": plugin_ref }), &["workspace", "ref"])
207− }
208− CustomizeOp::SetPluginEnabled => (
209− json!({
210− "workspace": workspace,
211− "ref": plugin_ref,
212− "enabled": { "type": "boolean", "description": "True to switch it on, false to switch it off." },
213− }),
214− &["workspace", "ref", "enabled"],
215− ),
216− CustomizeOp::GetConnector | CustomizeOp::RemoveConnector | CustomizeOp::RefreshConnector => (json!({ "workspace": workspace, "id": id }), &["workspace", "id"]),
217− CustomizeOp::AddConnector => (
218− json!({
219− "workspace": workspace,
220− "from": {
221− "type": "object",
222− "description": "Where it comes from: {\"kind\": \"directory\", \"id\"}, {\"kind\": \"plugin\", \"ref\", \"server\"} or {\"kind\": \"address\", \"name\", \"url\"}.",
223− "properties": {
224− "kind": { "type": "string", "enum": ["directory", "plugin", "address"] },
225− "id": { "type": "string", "description": "For directory: the directory entry's id." },
226− "ref": { "type": "string", "description": "For plugin: the installed plugin, as name@marketplace." },
227− "server": { "type": "string", "description": "For plugin: the server's name in its .mcp.json." },
228− "name": { "type": "string", "description": "For address: 2 to 32 lowercase letters, digits and hyphens." },
229− "url": { "type": "string", "description": "For address: the MCP server's https address." },
230− },
231− "required": ["kind"],
232− },
233− "auth": { "type": "string", "enum": ["none", "header", "oauth"], "description": "How it signs in. none when left out." },
234− "header": {
235− "type": ["object", "null"],
236− "description": "For header auth: the header's name and value. The value is kept sealed and never returned.",
237− "properties": {
238− "name": { "type": "string" },
239− "value": { "type": "string" },
240− },
241− "required": ["name", "value"],
242− },
243− }),
244− &["workspace", "from"],
245− ),
246− CustomizeOp::SetConnectorKey => (
247− json!({
248− "workspace": workspace,
249− "id": id,
250− "header_name": { "type": "string", "description": "The header the key is sent as. The one it had, or Authorization, when left out." },
251− "value": { "type": "string", "description": "The key. Kept sealed; never returned." },
252− }),
253− &["workspace", "id", "value"],
254− ),
255− CustomizeOp::GrantConnector => (
256− json!({
257− "workspace": workspace,
258− "id": id,
259− "scope": { "type": "string", "enum": ["workspace", "team", "agent"], "description": "Every agent, a team's agents, or one agent." },
260− "target": { "type": "string", "description": "The team's slug or the agent's handle; nothing for workspace." },
261− }),
262− &["workspace", "id", "scope"],
263− ),
264− CustomizeOp::RevokeConnector => (
265− json!({
266− "workspace": workspace,
267− "id": id,
268− "grant": { "type": "string", "description": "The grant's id, from the connector's grants." },
269− }),
270− &["workspace", "id", "grant"],
271− ),
272− };
273− json!({ "type": "object", "properties": properties, "required": required })
274− }
275−}
276−
277−fn text(input: &Value, key: &str) -> String {
278− input[key].as_str().map(str::trim).unwrap_or_default().to_owned()
279−}
280−
281−/// A boolean as sent: JSON, or a word from a form.
282−fn flag(value: &Value) -> Option<bool> {
283− match value {
284− Value::Bool(flag) => Some(*flag),
285− Value::String(word) => match word.trim().to_ascii_lowercase().as_str() {
286− "true" | "1" => Some(true),
287− "false" | "0" => Some(false),
288− _ => None,
289− },
290− _ => None,
291− }
292−}
293−
294−/// What an operation asks the agents service beyond the workspace and the
295−/// viewer, read from the input, or why the input will not do.
296−fn arguments(op: CustomizeOp, input: &Value) -> std::result::Result<Value, String> {
297− let mut args = json!({});
298− let need = |key: &str, what: &str| {
299− let value = text(input, key);
300− if value.is_empty() {
301− Err(what.to_owned())
302− } else {
303− Ok(value)
304− }
305− };
306− match op {
307− CustomizeOp::ListMarketplaces
308− | CustomizeOp::Discover
309− | CustomizeOp::ListPlugins
310− | CustomizeOp::ListPluginAgents
311− | CustomizeOp::ListConnectors
312− | CustomizeOp::Bundle => {}
313− CustomizeOp::AddMarketplace => {
314− args["address"] = json!(need("address", "Give address: owner/repo, workspace/name, a git address or a link to marketplace.json.")?);
315− if let Some(name) = input["name"].as_str().map(str::trim).filter(|name| !name.is_empty()) {
316− args["name"] = json!(name);
317− }
318− }
319− CustomizeOp::RemoveMarketplace | CustomizeOp::RefreshMarketplace => {
320− args["name"] = json!(need("marketplace", "Name the marketplace.")?);
321− }
322− CustomizeOp::GetPlugin | CustomizeOp::InstallPlugin | CustomizeOp::UninstallPlugin | CustomizeOp::UpdatePlugin => {
323− args["ref"] = json!(need("ref", "Name the plugin as name@marketplace.")?);
324− }
325− CustomizeOp::SetPluginEnabled => {
326− args["ref"] = json!(need("ref", "Name the plugin as name@marketplace.")?);
327− args["enabled"] = json!(flag(&input["enabled"]).ok_or("enabled is true or false.")?);
328− }
329− CustomizeOp::GetConnector | CustomizeOp::RemoveConnector | CustomizeOp::RefreshConnector => {
330− args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
331− }
332− CustomizeOp::AddConnector => {
333− let from = &input["from"];
334− if !matches!(from["kind"].as_str(), Some("directory" | "plugin" | "address")) {
335− return Err("from.kind is directory, plugin or address.".to_owned());
336− }
337− let auth = match input["auth"].as_str().map(str::trim) {
338− None | Some("") | Some("none") => "none",
339− Some(auth @ ("header" | "oauth")) => auth,
340− Some(_) => return Err("auth is none, header or oauth.".to_owned()),
341− };
342− let header = match &input["header"] {
343− Value::Null => Value::Null,
344− header if header["name"].is_string() && header["value"].is_string() => {
345− json!({ "name": header["name"], "value": header["value"] })
346− }
347− _ => return Err("header has a name and a value.".to_owned()),
348− };
349− if auth == "header" && header.is_null() {
350− return Err("Header auth needs header: its name and value.".to_owned());
351− }
352− args["input"] = json!({ "from": from, "auth": auth, "header": header });
353− }
354− CustomizeOp::SetConnectorKey => {
355− args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
356− args["value"] = json!(need("value", "Give value: the key.")?);
357− if let Some(name) = input["header_name"].as_str().map(str::trim).filter(|name| !name.is_empty()) {
358− args["header_name"] = json!(name);
359− }
360− }
361− CustomizeOp::GrantConnector => {
362− args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
363− let scope = text(input, "scope");
364− if !matches!(scope.as_str(), "workspace" | "team" | "agent") {
365− return Err("scope is workspace, team or agent.".to_owned());
366− }
367− if scope != "workspace" {
368− args["target"] = json!(need("target", "Give target: the team's slug or the agent's handle.")?);
369− }
370− args["scope"] = json!(scope);
371− }
372− CustomizeOp::RevokeConnector => {
373− args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
374− args["grant"] = json!(need("grant", "Name the grant by its id.")?);
375− }
376− }
377− Ok(args)
378−}
379−
380−pub async fn run(op: CustomizeOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
381− let workspace = text(input, "workspace").to_lowercase();
382− if workspace.is_empty() {
383− return Ok(Outcome::fail(FailureCode::Invalid, "Name the workspace."));
384− }
385− let actor = viewer.clone().unwrap_or_default();
386− // The bundle carries connectors' secrets: only a run's own token, or
387− // the workspace's, reads it, never a person's. The agents service
388− // checks this too.
389− if op == CustomizeOp::Bundle && actor_kind(&actor) == "person" {
390− return Ok(Outcome::fail(
391− FailureCode::Forbidden,
392− "The bundle is read with a job's agent token or a workspace's token, not a person's.",
393− ));
394− }
395− let mut args = match arguments(op, input) {
396− Ok(args) => args,
397− Err(why) => return Ok(Outcome::fail(FailureCode::Invalid, &why)),
398− };
399− args["workspace"] = json!(workspace);
400− args["viewer"] = json!(actor);
401− g1t_kit::call(&services.agents, op.method(), &args).await
402−}
403−
404−#[cfg(test)]
405−mod tests {
406− use super::*;
407− use g1t_contracts::scopes::{Level, scope_for};
408−
409− #[test]
410− fn each_operation_is_described_and_scoped() {
411− for op in CustomizeOp::ALL {
412− assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Customize(op)), "{}", op.name());
413− assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name());
414− assert!(op.input()["required"].as_array().unwrap().contains(&json!("workspace")), "{}", op.name());
415− let scope = scope_for(op.name()).unwrap();
416− let changes = !matches!(
417− op,
418− CustomizeOp::ListMarketplaces
419− | CustomizeOp::Discover
420− | CustomizeOp::ListPlugins
421− | CustomizeOp::GetPlugin
422− | CustomizeOp::ListPluginAgents
423− | CustomizeOp::ListConnectors
424− | CustomizeOp::GetConnector
425− | CustomizeOp::Bundle
426− );
427− let expected = match op {
428− CustomizeOp::Bundle => Level::Run,
429− _ if changes => Level::Admin,
430− _ => Level::Read,
431− };
432− assert_eq!(scope.level(), expected, "{}", op.name());
433− }
434− assert_eq!(scope_for("customize_bundle").unwrap().as_str(), "agents:run");
435− }
436−
437− #[test]
438− fn inputs_are_checked_before_the_call() {
439− let bad = |op, input: Value| arguments(op, &input).unwrap_err();
440− assert!(bad(CustomizeOp::AddMarketplace, json!({})).starts_with("Give address"));
441− assert!(bad(CustomizeOp::SetPluginEnabled, json!({ "ref": "a@b" })).contains("enabled"));
442− assert!(bad(CustomizeOp::AddConnector, json!({ "from": { "kind": "elsewhere" } })).contains("from.kind"));
443− assert!(bad(CustomizeOp::AddConnector, json!({ "from": { "kind": "address", "name": "x", "url": "https://x" }, "auth": "header" })).contains("header"));
444− assert!(bad(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "team" })).starts_with("Give target"));
445− assert!(bad(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "everyone" })).contains("scope"));
446−
447− let ok = |op, input: Value| arguments(op, &input).unwrap();
448− assert_eq!(ok(CustomizeOp::SetPluginEnabled, json!({ "ref": "a@b", "enabled": "false" })), json!({ "ref": "a@b", "enabled": false }));
449− assert_eq!(ok(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "workspace", "target": "ignored" })), json!({ "id": "con_1", "scope": "workspace" }));
450− assert_eq!(
451− ok(CustomizeOp::AddConnector, json!({ "from": { "kind": "directory", "id": "linear" } })),
452− json!({ "input": { "from": { "kind": "directory", "id": "linear" }, "auth": "none", "header": null } })
453− );
454− assert_eq!(
455− ok(CustomizeOp::SetConnectorKey, json!({ "id": "con_1", "value": "k", "header_name": " X-Api-Key " })),
456− json!({ "id": "con_1", "value": "k", "header_name": "X-Api-Key" })
457− );
458− assert_eq!(ok(CustomizeOp::GetPlugin, json!({ "ref": "commit-commands@claude-plugins-official" }))["ref"], "commit-commands@claude-plugins-official");
459− // The path names a marketplace as `marketplace`; the agents service as `name`.
460− assert_eq!(ok(CustomizeOp::RemoveMarketplace, json!({ "marketplace": "flagon-tools" })), json!({ "name": "flagon-tools" }));
461− assert!(bad(CustomizeOp::RefreshMarketplace, json!({ "name": "flagon-tools" })).starts_with("Name the marketplace"));
462− }
463−}
+1−1
618618 None => Err(Outcome::fail(FailureCode::Unauthenticated, "This needs a g1t access token.")),
619619 Some(user) if user.kind == PrincipalKind::Workspace => Err(Outcome::fail(
620620 FailureCode::Forbidden,
621− "Artifacts belong to people: use a personal access token, or a g1t agent's.",
621+ "Artifacts belong to people: use a personal access token.",
622622 )),
623623 Some(user) => Ok(user.clone()),
624624 }
+6−174
1313 mod billing;
1414 mod blobs;
1515 mod checks;
16−mod customize;
1716 mod deployments;
1817 mod telemetry;
1918 mod deploy_keys;
4342 mod token_policy;
4443 mod toolkit;
4544
46−use g1t_contracts::billing::{FinishRunArgs, RunTokens};
4745 use g1t_contracts::identity::{
4846 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
4947 };
5048 use g1t_contracts::work::{
51− CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, ReportPlanArgs,
52− ReportQueueArgs, ReportReviewArgs,
49+ CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
50+ ReportQueueArgs,
5351 };
54−use g1t_contracts::identity::AgentScope;
5552 use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
5653 use g1t_kit::wire;
5754 use serde_json::{Value, json};
425422 fn outcome_as_given(answered: Value) -> Result<Response> {
426423 match serde_json::from_value::<Outcome<Value>>(answered)? {
427424 Outcome::Ok(value) => Response::from_json(&value),
428− Outcome::Fail(refused) => failure(&refused),
429− }
430−}
431−
432−/// A sandbox reporting the review its agent wrote. As with checks, the
433−/// run's own token is the credential.
434−async fn report_review(
435− request: &mut Request,
436− services: &Services,
437− run_id: &str,
438−) -> Result<Response> {
439− let body = json_body(request).await;
440− let reported: Outcome<bool> = g1t_kit::call(
441− &services.work,
442− "report_review",
443− &ReportReviewArgs {
444− run_id: run_id.to_owned(),
445− token: body["token"].as_str().unwrap_or_default().to_owned(),
446− verdict: serde_json::from_value(body["verdict"].clone()).unwrap_or(None),
447− body: body["body"].as_str().unwrap_or_default().to_owned(),
448− comments: serde_json::from_value(body["comments"].clone()).unwrap_or_default(),
449− model: body["model"].as_str().map(str::to_owned),
450− error: body["error"].as_str().map(str::to_owned),
451− },
452− )
453− .await?;
454− match reported {
455− Outcome::Ok(_) => reply(&json!({ "recorded": true })),
456− Outcome::Fail(refused) => failure(&refused),
457− }
458−}
459−
460−/// A sandbox reporting the plan its agent wrote. As with checks, the
461−/// plan's own token is the credential.
462−async fn report_plan(
463− request: &mut Request,
464− services: &Services,
465− plan_id: &str,
466−) -> Result<Response> {
467− let body = json_body(request).await;
468− let reported: Outcome<bool> = g1t_kit::call(
469− &services.work,
470− "report_plan",
471− &ReportPlanArgs {
472− plan_id: plan_id.to_owned(),
473− token: body["token"].as_str().unwrap_or_default().to_owned(),
474− summary: body["summary"].as_str().unwrap_or_default().to_owned(),
475− issues: serde_json::from_value(body["issues"].clone()).unwrap_or_default(),
476− error: body["error"].as_str().map(str::to_owned),
477− },
478− )
479− .await?;
480− match reported {
481− Outcome::Ok(_) => reply(&json!({ "recorded": true })),
482− Outcome::Fail(refused) => failure(&refused),
483− }
484−}
485−
486−/// A sandbox reporting what its agent's run cost, so that the workspace
487−/// it worked for is charged. As with checks, the run's own token is the
488−/// credential.
489−async fn report_usage(
490− request: &mut Request,
491− services: &Services,
492− run_id: &str,
493−) -> Result<Response> {
494− let body = json_body(request).await;
495− let charged: Outcome<bool> = g1t_kit::call(
496− &services.billing,
497− "finish_run",
498− &FinishRunArgs {
499− run_id: run_id.to_owned(),
500− token: body["token"].as_str().unwrap_or_default().to_owned(),
501− cost_usd: body["cost_usd"].as_f64().unwrap_or_default(),
502− turns: body["turns"].as_u64().unwrap_or_default() as u32,
503− // What the harness counted; the agent rate is charged on no
504− // fewer, on a workspace's own model key too.
505− tokens: body.get("tokens").filter(|t| t.is_object()).map(|t| RunTokens {
506− input: t["input"].as_u64().unwrap_or_default(),
507− output: t["output"].as_u64().unwrap_or_default(),
508− cache_read: t["cache_read"].as_u64().unwrap_or_default(),
509− cache_write: t["cache_write"].as_u64().unwrap_or_default(),
510− }),
511− },
512− )
513− .await?;
514− match charged {
515− Outcome::Ok(_) => reply(&json!({ "recorded": true })),
516425 Outcome::Fail(refused) => failure(&refused),
517426 }
518427 }
647556 );
648557 }
649558 services.audit = audit::AuditContext::of(&request, on_mcp);
650− // An agent's token: what it may do comes with it, on the composite
651− // identity identity resolved it to.
559+ // A run's token: what it may do comes with it, on the composite
560+ // identity identity resolved it to. Any other token of g1t's own
561+ // principal (the agent tokens g1t no longer issues) does nothing.
652562 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
653563 services.scope = Some(acting.scope.clone());
654564 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
655− let header = request.headers().get("authorization")?.unwrap_or_default();
656− let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default();
657− let scope: Option<AgentScope> = g1t_kit::call(
658− &services.identity,
659− "agent_scope",
660− &TokenArgs {
661− token: token.to_owned(),
662− },
663− )
664− .await?;
665− // A scope is what lets an agent's token do anything at all.
666− let Some(scope) = scope else {
667− return fail(FailureCode::Unauthenticated, "Invalid access token.");
668− };
669− services.scope = Some(scope);
565+ return fail(FailureCode::Unauthenticated, "Invalid access token.");
670566 }
671567 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
672568 return Ok(response);
783679 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
784680 Response::from_json(&spec)
785681 }
786− Outcome::Fail(refused) => failure(&refused),
787− };
788− }
789− // A sandbox reporting its agent run's steps, cost and end. As with
790− // checks, the run's own token, in the body, is the credential.
791− ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/report") => {
792− let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/report");
793− let mut body = json_body(&mut request).await;
794− if !body.is_object() {
795− body = json!({});
796− }
797− body["runId"] = json!(run_id);
798− let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?;
799− return match reported {
800− Outcome::Ok(status) => reply(&json!({ "status": status })),
801− Outcome::Fail(refused) => failure(&refused),
802− };
803− }
804− // What a run's agent learned, as memory candidates; the same token.
805− ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/learned") => {
806− let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/learned");
807− let body = json_body(&mut request).await;
808− let learned = json!({
809− "runId": run_id,
810− "token": body["token"].as_str().unwrap_or_default(),
811− "items": body["items"].as_array().cloned().unwrap_or_default(),
812− });
813− let captured: Outcome<Value> = g1t_kit::call(&services.work, "report_learned", &learned).await?;
814− return match captured {
815− Outcome::Ok(captured) => reply(&captured),
816682 Outcome::Fail(refused) => failure(&refused),
817683 };
818684 }
819− // How sure a run's agent is of its change; the same token.
820− ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/confidence") => {
821− let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/confidence");
822− let body = json_body(&mut request).await;
823− let said = json!({
824− "runId": run_id,
825− "token": body["token"].as_str().unwrap_or_default(),
826− "confidence": body["confidence"].as_str().unwrap_or_default(),
827− "uncertainAbout": body["uncertain_about"]
828− .as_array()
829− .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>())
830− .unwrap_or_default(),
831− });
832− let recorded: Outcome<Value> = g1t_kit::call(&services.work, "report_confidence", &said).await?;
833− return match recorded {
834− Outcome::Ok(recorded) => reply(&json!({ "recorded": recorded })),
835− Outcome::Fail(refused) => failure(&refused),
836− };
837− }
838685 ("POST", path) if path.starts_with("/checks/") => {
839686 let run_id = path.trim_start_matches("/checks/").to_owned();
840687 return report_checks(&mut request, &services, &run_id).await;
841− }
842− ("POST", path) if path.starts_with("/runs/") && path.ends_with("/usage") => {
843− let run_id = path
844− .trim_start_matches("/runs/")
845− .trim_end_matches("/usage")
846− .to_owned();
847− return report_usage(&mut request, &services, &run_id).await;
848− }
849− ("POST", path) if path.starts_with("/plans/") => {
850− let plan_id = path.trim_start_matches("/plans/").to_owned();
851− return report_plan(&mut request, &services, &plan_id).await;
852− }
853− ("POST", path) if path.starts_with("/reviews/") => {
854− let run_id = path.trim_start_matches("/reviews/").to_owned();
855− return report_review(&mut request, &services, &run_id).await;
856688 }
857689 _ => {}
858690 }
+21−6
1010
1111 const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
1212
13−const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers.
14−Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, security, webhook, access, workspace, customize, account, notifications. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs.
13+const INSTRUCTIONS: &str = "g1t is where your code, its history and the work around it live: repositories, issues, pull requests, workflows, deployments and packages. You bring your own agent; these tools are how it reads and changes what is on g1t, as the person or token it acts for. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers.
14+Tools are resources, each with an `action`: search, repository, issue, pull_request, memory, workflow, observability, package, secret, webhook, access, team, workspace, billing, security, notifications, account, artifact. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs.
1515 Find a repository: account whoami lists your workspaces; repository list or search finds one.
16−Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again.
17−Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money.
16+Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Push your commits, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again.
17+Record your session: as you work, pull_request record_session appends your prompts, reasoning and the tools you ran to the pull request's session, so reviewers see why the change is what it is; pull_request read_session reads it back.
18+Memory: memory recall before you start to learn how the project builds and what it has decided; when you learn something the next agent needs, memory remember it, one short fact at a time (scope project or workspace). Never a secret.
1819 Security: security code_alerts, vulnerability_alerts and secret_alerts show what to fix; fix it in your pull request, which the Code scanning and Dependency review checks judge.
19−When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret.";
20+Production: observability overview and errors show what a project's running software did; follow a request or trace to its logs.";
2021
2122 fn result(id: &Value, value: Value) -> Value {
2223 json!({ "jsonrpc": "2.0", "id": id, "result": value })
2627 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
2728 }
2829
29−/// What decides the actions a caller sees: an agent's run scope, an
30+/// What decides the actions a caller sees: a run's scope, an
3031 /// access token's scopes, or nothing beyond the person's role.
3132 fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> {
3233 if let Some(scope) = &services.scope {
209210 },
210211 }
211212 }
213+
214+#[cfg(test)]
215+mod tests {
216+ use super::*;
217+
218+ /// The instructions name every tool, in the order `tools/list` gives
219+ /// them, so that no tool goes unmentioned.
220+ #[test]
221+ fn the_instructions_list_every_tool() {
222+ let names: Vec<&str> = TOOLS.iter().map(|tool| tool.name).collect();
223+ let listed = format!("each with an `action`: {}.", names.join(", "));
224+ assert!(INSTRUCTIONS.contains(&listed), "{listed}");
225+ }
226+}
+6−86
1010 use crate::about::AboutOp;
1111 use crate::run_artifacts::ArtifactsOp;
1212 use crate::archives::ArchiveOp;
13−use crate::customize::CustomizeOp;
1413 use crate::deploy_keys::DeployKeysOp;
1514 use crate::mirrors::MirrorsOp;
1615 use crate::deployments::DeploymentsOp;
102101 ),
103102 (
104103 "Billing",
105− "A workspace's usage, its budget, its AI credit, its invoices and its AI Gateway requests. Members read them; owners change the budget and buy credit, as people. g1t's agents never change billing.",
104+ "A workspace's usage, its budget, its AI credit and its invoices. Members read them; owners change the budget and buy credit, as people. A run's token never changes billing.",
106105 &[
107106 Op::GetUsage,
108107 Op::GetBudget,
111110 Op::BuyAiCredit,
112111 Op::ListInvoices,
113112 Op::GetBillingDetails,
114− Op::ListGatewayRequests,
115113 ],
116114 ),
117115 (
264262 Op::Security(SecurityOp::ListAnalyses),
265263 Op::Security(SecurityOp::UploadSarif),
266264 Op::Security(SecurityOp::GetSarifUpload),
267− Op::Security(SecurityOp::FixAlert),
268265 ],
269266 ),
270267 (
351348 Op::UpdateIssue,
352349 Op::CloseIssue,
353350 Op::ReopenIssue,
354− Op::AssignIssue,
355− Op::Delegate,
356351 Op::AddComment,
357352 Op::EditComment,
358353 Op::DeleteComment,
377372 Op::UpdateMilestone,
378373 Op::DeleteMilestone,
379374 ],
380− ),
381− (
382− "Plans",
383− "An outcome turned into the issues that would get there, with the order they must merge in.",
384− &[Op::PlanWork, Op::GetPlan, Op::ApplyPlan],
385375 ),
386376 (
387377 "Pull requests",
401391 Op::ClosePullRequest,
402392 Op::ReopenPullRequest,
403393 Op::GetMergeQueue,
404− Op::MessageAgent,
405− Op::AnswerMessage,
406− Op::TakeMessages,
407394 ],
408395 ),
409396 (
413400 ),
414401 (
415402 "Memory",
416− "What agents and people learned that the next agent should know, for one project or across a workspace. Members and g1t's agents only; never a secret.",
403+ "What agents and people learned that the next agent should know, for one project or across a workspace. Members only; never a secret.",
417404 &[Op::Remember, Op::Recall],
418405 ),
419406 (
422409 &[Op::Search],
423410 ),
424411 (
425− "Context",
426− "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
427− &[Op::SearchContext, Op::GetEntity],
428− ),
429− (
430412 "Actions",
431413 "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
432414 &[
548530 ),
549531 (
550532 "Runners",
551− "Self-hosted runners: your own machines, which run your workflow jobs (and, if you choose, your agents' work) for $0 of g1t compute. They register with a short-lived token and only ever connect out.",
533+ "Self-hosted runners: your own machines, which run your workflow jobs for $0 of g1t compute. They register with a short-lived token and only ever connect out.",
552534 &[
553535 Op::ListRunners,
554536 Op::CreateRunnerRegistrationToken,
559541 Op::DeleteRunnerGroup,
560542 Op::GetRunnerSettings,
561543 Op::UpdateRunnerSettings,
562− ],
563− ),
564− (
565− "Agents' computers",
566− "Each workspace agent has a computer of its own on g1t cloud: a persistent home its sessions run commands on, which sleeps when idle and is saved as it was. Its state and recent commands, and waking, sleeping and resetting it.",
567− &[
568− Op::GetAgentComputer,
569− Op::WakeAgentComputer,
570− Op::SleepAgentComputer,
571− Op::ResetAgentComputer,
572− Op::ListAgentComputerCommands,
573544 ],
574545 ),
575546 (
587558 ),
588559 (
589560 "Integrations",
590− "A workspace's connections to outside systems: model providers, alert sources and issue trackers.",
561+ "A workspace's connections to outside systems: alert sources and issue trackers.",
591562 &[
592563 Op::ListIntegrations,
593564 Op::ConnectIntegration,
594565 Op::UpdateIntegration,
595566 Op::DisconnectIntegration,
596567 Op::TestIntegration,
597− Op::GetModelRoutes,
598− Op::SetModelRoutes,
599568 Op::GetContext,
600569 Op::ImportIssue,
601− ],
602− ),
603− (
604− "Customize",
605− "What shapes how a workspace's agents work, in the formats Claude Code reads: the marketplaces it reads plugins from, what Discover lists, the plugins it installed (their skills, subagents and remote MCP servers reach g1t's agents; the rest reaches Claude Code on a runner), and its connectors: MCP servers connected once, then given to every agent, a team or one agent. The bundle is what a job's Claude Code loads on a runner.",
606− &[
607− Op::Customize(CustomizeOp::ListMarketplaces),
608− Op::Customize(CustomizeOp::AddMarketplace),
609− Op::Customize(CustomizeOp::RemoveMarketplace),
610− Op::Customize(CustomizeOp::RefreshMarketplace),
611− Op::Customize(CustomizeOp::Discover),
612− Op::Customize(CustomizeOp::ListPlugins),
613− Op::Customize(CustomizeOp::GetPlugin),
614− Op::Customize(CustomizeOp::InstallPlugin),
615− Op::Customize(CustomizeOp::SetPluginEnabled),
616− Op::Customize(CustomizeOp::UninstallPlugin),
617− Op::Customize(CustomizeOp::UpdatePlugin),
618− Op::Customize(CustomizeOp::ListPluginAgents),
619− Op::Customize(CustomizeOp::ListConnectors),
620− Op::Customize(CustomizeOp::GetConnector),
621− Op::Customize(CustomizeOp::AddConnector),
622− Op::Customize(CustomizeOp::SetConnectorKey),
623− Op::Customize(CustomizeOp::RemoveConnector),
624− Op::Customize(CustomizeOp::RefreshConnector),
625− Op::Customize(CustomizeOp::GrantConnector),
626− Op::Customize(CustomizeOp::RevokeConnector),
627− Op::Customize(CustomizeOp::Bundle),
628570 ],
629571 ),
630572 (
707649 Op::UpdateRepoSettings => "Update repository settings",
708650 Op::ListCheckNames => "List check names",
709651 Op::GetMergeQueue => "Get the merge queue",
710− Op::MessageAgent => "Message an agent",
711− Op::AnswerMessage => "Answer a message",
712− Op::TakeMessages => "Take new messages",
713652 Op::Remember => "Remember something",
714653 Op::Recall => "Recall memory",
715− Op::SearchContext => "Search the context hub",
716− Op::GetEntity => "Get a catalog entry",
717654 Op::Search => "Search g1t",
718655 Op::ListIssues => "List issues",
719656 Op::GetIssue => "Get an issue",
721658 Op::UpdateIssue => "Update an issue",
722659 Op::CloseIssue => "Close an issue",
723660 Op::ReopenIssue => "Reopen an issue",
724− Op::AssignIssue => "Assign an issue to g1t",
725− Op::Delegate => "Put an agent on it",
726− Op::PlanWork => "Plan work",
727− Op::GetPlan => "Get a plan",
728− Op::ApplyPlan => "Apply a plan",
729661 Op::ListLabels => "List labels",
730662 Op::CreateLabel => "Create a label",
731663 Op::UpdateLabel => "Update a label",
764696 Op::TestIntegration => "Test an integration",
765697 Op::GetContext => "Look up a ticket",
766698 Op::ImportIssue => "Import an issue",
767− Op::GetModelRoutes => "Get model routes",
768− Op::SetModelRoutes => "Set model routes",
769699 Op::ListWebhooks => "List webhooks",
770700 Op::CreateWebhook => "Create a webhook",
771701 Op::UpdateWebhook => "Update a webhook",
789719 Op::DeleteActionsVariable => "Delete a variable",
790720 Op::ListRunners => "List self-hosted runners",
791721 Op::ListRunnerGroups => "List runner groups",
792− Op::GetAgentComputer => "Get an agent's computer",
793− Op::WakeAgentComputer => "Wake an agent's computer",
794− Op::SleepAgentComputer => "Put an agent's computer to sleep",
795− Op::ResetAgentComputer => "Reset an agent's computer",
796− Op::ListAgentComputerCommands => "List an agent's computer's commands",
797722 Op::GetRunnerSettings => "Get runner settings",
798723 Op::CreateRunnerRegistrationToken => "Create a runner registration token",
799724 Op::RemoveRunner => "Remove a self-hosted runner",
838763 Op::BuyAiCredit => "Buy AI credit",
839764 Op::ListInvoices => "List a workspace's invoices",
840765 Op::GetBillingDetails => "Get a workspace's billing details",
841− Op::ListGatewayRequests => "List a workspace's AI Gateway requests",
842766 Op::PinProject => "Pin a project",
843767 Op::UnpinProject => "Unpin a project",
844768 Op::ReorderPinnedProjects => "Reorder your pinned projects",
876800 Op::Mirrors(op) => op.title(),
877801 Op::Packages(op) => op.title(),
878802 Op::Folios(op) => op.title(),
879− Op::Customize(op) => op.title(),
880803 }
881804 }
882805
883806 /// Why an operation can be refused with `402 payment_required`, if it
884−/// can: the ones that start an agent, when the workspace has no credit,
885−/// and the ones that make a repository private in a workspace, when a free
886−/// workspace's private storage has no room for it.
807+/// can: the ones that make a repository private in a workspace, when a
808+/// free workspace's private storage has no room for it.
887809 fn may_need_payment(op: Op) -> Option<&'static str> {
888810 match op {
889− Op::AssignIssue | Op::PlanWork | Op::ApplyPlan => Some("The workspace has no agent credit."),
890− Op::WakeAgentComputer => Some("The workspace's plan refuses compute: it is paused, or over its limit."),
891811 Op::UpdateRepo | Op::SetRepoVisibility | Op::TransferRepo => Some(
892812 "A free workspace's private storage has no room for this private repository.",
893813 ),
+24−538
A large diff (1,040 lines) is not shown until you ask for it.
+0−1732
A large diff (1,782 lines) is not shown until you ask for it.
+0−5
133133 Op::Packages(_) => return as_is,
134134 // Artifacts are shaped by the API itself, in `snake_case`.
135135 Op::Folios(_) => return as_is,
136− // Agents' computers travel in `snake_case` from the agents service.
137− Op::GetAgentComputer | Op::WakeAgentComputer | Op::SleepAgentComputer | Op::ResetAgentComputer | Op::ListAgentComputerCommands => return as_is,
138136 // Built by the API itself, in `snake_case`.
139137 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
140138 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
174172 through::<work::Issue>(op, sent)
175173 }
176174 Op::GetIssue => through::<work::IssueDetail>(op, sent),
177− Op::Delegate => through::<work::Delegated>(op, sent),
178175 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
179176 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
180177 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
188185 | Op::ReopenPullRequest
189186 | Op::ConvertPullRequestToDraft
190187 | Op::MergePullRequest
191− | Op::AssignIssue
192188 | Op::RequestReviewers
193189 | Op::RemoveRequestedReviewers => {
194190 through::<work::Pull>(op, sent)
208204 Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
209205 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
210206 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
211− Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
212207 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
213208 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
214209 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
+0−96
55 use crate::about::AboutOp;
66 use crate::run_artifacts::ArtifactsOp;
77 use crate::archives::ArchiveOp;
8−use crate::customize::CustomizeOp;
98 use crate::deploy_keys::DeployKeysOp;
109 use crate::mirrors::MirrorsOp;
1110 use crate::deployments::DeploymentsOp;
291290 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
292291 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
293292 // The AI Gateway's log of a workspace's requests.
294− route("GET", "/workspaces/:workspace/gateway/requests", Op::ListGatewayRequests, &[("limit", "limit"), ("before", "before")]),
295293 // Code owners: the CODEOWNERS file, checked.
296294 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
297295 // Security alerts: secrets and vulnerable dependencies.
335333 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
336334 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
337335 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
338− route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
339336 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
340337 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
341338 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
540537 &[],
541538 ),
542539 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
543− route(
544− "POST",
545− "/repos/:owner/:name/pulls/:number/messages",
546− Op::MessageAgent,
547− &[],
548− ),
549− route(
550− "POST",
551− "/repos/:owner/:name/pulls/:number/messages/take",
552− Op::TakeMessages,
553− &[],
554− ),
555− route(
556− "POST",
557− "/repos/:owner/:name/messages/:id/answer",
558− Op::AnswerMessage,
559− &[],
560− ),
561540 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
562541 route(
563542 "GET",
615594 "POST",
616595 "/repos/:owner/:name/issues/:number/reopen",
617596 Op::ReopenIssue,
618− &[],
619− ),
620− route(
621− "POST",
622− "/repos/:owner/:name/issues/:number/assign",
623− Op::AssignIssue,
624597 &[],
625598 ),
626599 route(
627600 "POST",
628601 "/repos/:owner/:name/issues/import",
629602 Op::ImportIssue,
630− &[],
631− ),
632− route(
633− "POST",
634− "/repos/:owner/:name/issues/delegate",
635− Op::Delegate,
636603 &[],
637604 ),
638605 route(
640607 "/repos/:owner/:name/context",
641608 Op::GetContext,
642609 &[("reference", "reference")],
643− ),
644− route(
645− "GET",
646− "/workspaces/:workspace/context/search",
647− Op::SearchContext,
648− &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
649− ),
650− route(
651− "GET",
652− "/workspaces/:workspace/context/:kind/:id",
653− Op::GetEntity,
654− &[],
655610 ),
656611 route(
657612 "GET",
747702 "POST",
748703 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
749704 Op::RedeliverWebhook,
750− &[],
751− ),
752− route(
753− "GET",
754− "/workspaces/:workspace/model-routes",
755− Op::GetModelRoutes,
756− &[],
757− ),
758− route(
759− "PUT",
760− "/workspaces/:workspace/model-routes",
761− Op::SetModelRoutes,
762705 &[],
763706 ),
764707 route(
779722 Op::TestIntegration,
780723 &[],
781724 ),
782− // Customize: marketplaces, Discover, plugins and connectors. A plugin's
783− // ref is `name@marketplace`, URL-encoded (`@` as `%40`).
784− route("GET", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), &[]),
785− route("POST", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::AddMarketplace), &[]),
786− route("DELETE", "/workspaces/:workspace/customize/marketplaces/:marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), &[]),
787− route("POST", "/workspaces/:workspace/customize/marketplaces/:marketplace/refresh", Op::Customize(CustomizeOp::RefreshMarketplace), &[]),
788− route("GET", "/workspaces/:workspace/customize/discover", Op::Customize(CustomizeOp::Discover), &[]),
789− route("GET", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::ListPlugins), &[]),
790− route("POST", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::InstallPlugin), &[]),
791− route("GET", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::GetPlugin), &[]),
792− route("PATCH", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::SetPluginEnabled), &[]),
793− route("DELETE", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::UninstallPlugin), &[]),
794− route("POST", "/workspaces/:workspace/customize/plugins/:ref/update", Op::Customize(CustomizeOp::UpdatePlugin), &[]),
795− route("GET", "/workspaces/:workspace/customize/agents", Op::Customize(CustomizeOp::ListPluginAgents), &[]),
796− route("GET", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::ListConnectors), &[]),
797− route("POST", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::AddConnector), &[]),
798− route("GET", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::GetConnector), &[]),
799− route("PATCH", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::SetConnectorKey), &[]),
800− route("DELETE", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::RemoveConnector), &[]),
801− route("POST", "/workspaces/:workspace/customize/connectors/:id/refresh", Op::Customize(CustomizeOp::RefreshConnector), &[]),
802− route("POST", "/workspaces/:workspace/customize/connectors/:id/grants", Op::Customize(CustomizeOp::GrantConnector), &[]),
803− route("DELETE", "/workspaces/:workspace/customize/connectors/:id/grants/:grant", Op::Customize(CustomizeOp::RevokeConnector), &[]),
804− route("GET", "/workspaces/:workspace/customize/bundle", Op::Customize(CustomizeOp::Bundle), &[]),
805725 route(
806726 "GET",
807727 "/repos/:owner/:name/actions/workflows",
1019939 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
1020940 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
1021941 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
1022− // A workspace agent's own computer.
1023− route("GET", "/workspaces/:workspace/agents/:agent/computer", Op::GetAgentComputer, &[]),
1024− route("POST", "/workspaces/:workspace/agents/:agent/computer/wake", Op::WakeAgentComputer, &[]),
1025− route("POST", "/workspaces/:workspace/agents/:agent/computer/sleep", Op::SleepAgentComputer, &[]),
1026− route("POST", "/workspaces/:workspace/agents/:agent/computer/reset", Op::ResetAgentComputer, &[]),
1027− route("GET", "/workspaces/:workspace/agents/:agent/computer/commands", Op::ListAgentComputerCommands, &[]),
1028− route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
1029− route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
1030− route(
1031− "POST",
1032− "/repos/:owner/:name/plans/:plan/apply",
1033− Op::ApplyPlan,
1034− &[],
1035− ),
1036942 route(
1037943 "POST",
1038944 "/repos/:owner/:name/issues/:number/comments",
11491055 matches!(
11501056 self.op,
11511057 Op::DeleteComment
1152− | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
11531058 )
11541059 }
11551060
15431448 assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit);
15441449 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
15451450 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1546− assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests);
15471451 }
15481452
15491453 #[test]
+1−14
5252 ListVulnerabilityAlerts,
5353 GetVulnerabilityAlert,
5454 UpdateVulnerabilityAlert,
55− FixAlert,
5655 GetDependencyGraph,
5756 GetSbom,
5857 CompareDependencies,
7574 /// Every one: `Op::ALL` lists each as `Op::Security(…)`, which a test
7675 /// checks against this.
7776 #[cfg(test)]
78− pub const ALL: [SecurityOp; 39] = [
77+ pub const ALL: [SecurityOp; 38] = [
7978 SecurityOp::ListSecretAlerts,
8079 SecurityOp::GetSecretAlert,
8180 SecurityOp::UpdateSecretAlert,
9897 SecurityOp::ListVulnerabilityAlerts,
9998 SecurityOp::GetVulnerabilityAlert,
10099 SecurityOp::UpdateVulnerabilityAlert,
101− SecurityOp::FixAlert,
102100 SecurityOp::GetDependencyGraph,
103101 SecurityOp::GetSbom,
104102 SecurityOp::CompareDependencies,
141139 SecurityOp::ListVulnerabilityAlerts => "list_vulnerability_alerts",
142140 SecurityOp::GetVulnerabilityAlert => "get_vulnerability_alert",
143141 SecurityOp::UpdateVulnerabilityAlert => "update_vulnerability_alert",
144− SecurityOp::FixAlert => "fix_security_alert",
145142 SecurityOp::GetDependencyGraph => "get_dependency_graph",
146143 SecurityOp::GetSbom => "get_sbom",
147144 SecurityOp::CompareDependencies => "compare_dependencies",
186183 SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts",
187184 SecurityOp::GetVulnerabilityAlert => "Get a vulnerability alert",
188185 SecurityOp::UpdateVulnerabilityAlert => "Dismiss or reopen a vulnerability alert",
189− SecurityOp::FixAlert => "Fix an alert with g1t",
190186 SecurityOp::GetDependencyGraph => "Get the dependency graph",
191187 SecurityOp::GetSbom => "Export an SBOM",
192188 SecurityOp::CompareDependencies => "Compare dependencies",
230226 SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts: a package a lockfile resolves with a known advisory, open first and worst first, with the security update g1t opened for it. In a repository, or (with workspace) across a workspace. Filter by state, severity, ecosystem and package.",
231227 SecurityOp::GetVulnerabilityAlert => "Get one vulnerability alert by id (vul_…).",
232228 SecurityOp::UpdateVulnerabilityAlert => "Dismiss a vulnerability alert (state dismissed, with a reason: fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used, and an optional comment) or reopen it (state open).",
233− SecurityOp::FixAlert => "Put g1t on an issue to fix an alert: a code scanning alert (cod_…), a vulnerable dependency (vul_…) or a secret in the code (sec_…; rotating it stays with you). Its pull request lands through the repository's required checks. The agent's run is charged as agent usage. Returns the issue, and whether the agent started.",
234229 SecurityOp::GetDependencyGraph => "Get the dependency graph: every package the lockfiles on the default branch resolve, per lockfile, with whether it is direct or transitive (where the lockfile says), for development, its license when recorded, its package URL and its open vulnerability alerts.",
235230 SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.",
236231 SecurityOp::CompareDependencies => "Compare the dependencies at two commits, branches or tags (basehead, as base...head): what was added and removed per lockfile, with the known vulnerabilities of what was added and whether it passes the repository's dependency review policy. Needs the g1t plan on a private repository.",
433428 }),
434429 &["repo", "id", "state"],
435430 ),
436− SecurityOp::FixAlert => (
437− json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: cod_…, vul_… or sec_…" } }),
438− &["repo", "id"],
439− ),
440431 SecurityOp::GetDependencyGraph | SecurityOp::GetSbom | SecurityOp::GetSettings => (json!({ "repo": repo() }), &["repo"]),
441432 SecurityOp::CompareDependencies => (
442433 json!({
10261017 },
10271018 Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
10281019 }
1029− }
1030− SecurityOp::FixAlert => {
1031− let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1032− pass(services, "fix_alert", &FixAlertArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default() }).await
10331020 }
10341021 SecurityOp::GetDependencyGraph => {
10351022 let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
+59−82
2121 use crate::about::AboutOp;
2222 use crate::run_artifacts::ArtifactsOp;
2323 use crate::archives::ArchiveOp;
24−use crate::customize::CustomizeOp;
2524 use crate::deploy_keys::DeployKeysOp;
2625 use crate::mirrors::MirrorsOp;
2726 use crate::deployments::DeploymentsOp;
6059 Tool {
6160 name: "search",
6261 title: "Search",
63− 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.",
62+ 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. `ticket` reads a Jira, Linear or Sentry item the work refers to.",
6463 default_action: Some("code"),
6564 actions: &[
6665 a("code", Op::Search, "Search all of g1t: repositories, code, issues, pull requests, people"),
67− a("context", Op::SearchContext, "Search a workspace's context hub by meaning"),
68− a("entity", Op::GetEntity, "One catalog entry and its relations"),
6966 a("ticket", Op::GetContext, "A Jira, Linear or Sentry item the work refers to, as it is now"),
7067 ],
7168 },
189186 a("reopen", Op::ReopenPullRequest, "Reopen a closed one"),
190187 a("merge", Op::MergePullRequest, "Land it, or join the merge queue"),
191188 a("merge_queue", Op::GetMergeQueue, "The repository's merge queue"),
192− ],
193− },
194− Tool {
195− name: "agent",
196− title: "g1t agents",
197− 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.",
198− default_action: None,
199− actions: &[
200− a("delegate", Op::Delegate, "Open an issue and put an agent on it in one step"),
201− a("assign", Op::AssignIssue, "Put an agent on an existing issue"),
202− a("message", Op::MessageAgent, "Tell the agent on a pull request something, or ask another agent"),
203− a("answer", Op::AnswerMessage, "Answer a question or handoff sent to you"),
204− a("take_messages", Op::TakeMessages, "For a g1t agent: messages not seen yet"),
205− a("computer", Op::GetAgentComputer, "A workspace agent's own computer: state, disk, recent commands"),
206− a("wake_computer", Op::WakeAgentComputer, "Wake a workspace agent's computer (metered until it sleeps)"),
207− a("sleep_computer", Op::SleepAgentComputer, "Save its home and put it to sleep"),
208− a("reset_computer", Op::ResetAgentComputer, "Wipe its home; memory and artifacts are kept"),
209− a("computer_commands", Op::ListAgentComputerCommands, "The commands it ran most recently, with output"),
210− ],
211− },
212− Tool {
213− name: "plan",
214− title: "Plans",
215− description: "Turn an outcome into issues: an agent proposes them with what done means and their dependencies; nothing opens until you apply the plan.",
216− default_action: None,
217− actions: &[
218− a("create", Op::PlanWork, "Ask an agent for a plan; read it with get until ready"),
219− a("get", Op::GetPlan, "A plan and the issues it proposes"),
220− a("apply", Op::ApplyPlan, "Open its issues; with assign, agents start in dependency order"),
221189 ],
222190 },
223191 Tool {
233201 Tool {
234202 name: "workflow",
235203 title: "Workflows",
236− description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. 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; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions, repository dispatch, and the workflows a workspace runs in its repositories without a file in them. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. 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.",
204+ description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Runs' artifacts: listing, a download link, deleting, and how long they are kept. 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; environments' protection rules, approving or rejecting the jobs they hold, approving a pull request's run from outside, the token's default permissions, repository dispatch, and the workflows a workspace runs in its repositories without a file in them. Checks on commits: statuses, check runs (a g1t Actions job is one) and check suites, to read where a commit stands or report on it from CI or an integration. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), and their groups.",
237205 default_action: None,
238206 actions: &[
239207 a("list", Op::ListWorkflows, "Workflows on the default branch"),
296264 a("create_runner_group", Op::CreateRunnerGroup, "Make a group, for some repositories"),
297265 a("update_runner_group", Op::UpdateRunnerGroup, "Rename a group or change its repositories"),
298266 a("delete_runner_group", Op::DeleteRunnerGroup, "Delete a group; its runners join the default"),
299− a("get_runner_settings", Op::GetRunnerSettings, "Where agent work runs; whether forks may use runners"),
267+ a("get_runner_settings", Op::GetRunnerSettings, "Where checks and the merge queue run; whether forks may use runners"),
300268 a("update_runner_settings", Op::UpdateRunnerSettings, "Change them"),
301269 ],
302270 },
418386 Tool {
419387 name: "workspace",
420388 title: "Workspaces",
421− 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.",
389+ description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations, 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.",
422390 default_action: None,
423391 actions: &[
424392 a("get", Op::GetWorkspace, "A workspace's details and settings"),
433401 a("list_invites", Op::ListWorkspaceInvites, "Its invites"),
434402 a("invite_member", Op::InviteMember, "Invite someone by username or email address"),
435403 a("revoke_invite", Op::RevokeWorkspaceInvite, "Revoke a pending invite"),
436− a("list_integrations", Op::ListIntegrations, "Model providers, alert sources, trackers"),
404+ a("list_integrations", Op::ListIntegrations, "Alert sources and issue trackers"),
437405 a("connect_integration", Op::ConnectIntegration, "Connect one"),
438− a("update_integration", Op::UpdateIntegration, "Change one: rotate its key, choose its AI Gateway models"),
406+ a("update_integration", Op::UpdateIntegration, "Change one: rotate its key or its settings"),
439407 a("disconnect_integration", Op::DisconnectIntegration, "Remove one"),
440408 a("test_integration", Op::TestIntegration, "Check its credentials"),
441− a("get_model_routes", Op::GetModelRoutes, "Where each kind of work's model requests go"),
442− a("set_model_routes", Op::SetModelRoutes, "Replace them"),
443409 a("list_projects", Op::ListProjects, "Its projects you can see: what each is, where it runs, its links"),
444410 a("get_project", Op::GetProject, "One project"),
445411 a("update_project", Op::UpdateProject, "Change a project's name, description, kind, where it runs or its links"),
462428 ],
463429 },
464430 Tool {
465− name: "customize",
466− title: "Customize",
467− description: "What shapes how a workspace's agents work, in the formats Claude Code reads: the marketplaces it reads plugins from (`.claude-plugin/marketplace.json`), what Discover lists, the plugins it installed as `name@marketplace` (their skills join the library as `plugin:skill`, their subagents become templates, their remote MCP servers become connectors), and its connectors: MCP servers connected once with a key or a sign-in, then given to every agent, a team or one agent. Members read; owners change. `bundle` is what a job's Claude Code loads on a runner, for the job's own token.",
468− default_action: None,
469− actions: &[
470− a("list_marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), "The marketplaces it reads plugins from"),
471− a("add_marketplace", Op::Customize(CustomizeOp::AddMarketplace), "Add one by owner/repo, workspace/name, a git address or a marketplace.json link"),
472− a("remove_marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), "Remove one; its installed plugins stay"),
473− a("refresh_marketplace", Op::Customize(CustomizeOp::RefreshMarketplace), "Read its manifest again now"),
474− a("discover", Op::Customize(CustomizeOp::Discover), "Every plugin and skill it can add, with availability"),
475− a("list_plugins", Op::Customize(CustomizeOp::ListPlugins), "Installed plugins: pinned commit, components, updates"),
476− a("get_plugin", Op::Customize(CustomizeOp::GetPlugin), "One plugin's listing, install, components, README and agents"),
477− a("install_plugin", Op::Customize(CustomizeOp::InstallPlugin), "Install one at its listed commit"),
478− a("set_plugin_enabled", Op::Customize(CustomizeOp::SetPluginEnabled), "Switch an installed one on or off"),
479− a("uninstall_plugin", Op::Customize(CustomizeOp::UninstallPlugin), "Uninstall one, with its skills, templates and connectors"),
480− a("update_plugin", Op::Customize(CustomizeOp::UpdatePlugin), "Read one again at its marketplace's current commit"),
481− a("list_plugin_agents", Op::Customize(CustomizeOp::ListPluginAgents), "Subagents plugins brought, as templates to hire from"),
482− a("list_connectors", Op::Customize(CustomizeOp::ListConnectors), "MCP servers connected to the workspace, with state, tools and grants"),
483− a("get_connector", Op::Customize(CustomizeOp::GetConnector), "One connector"),
484− a("add_connector", Op::Customize(CustomizeOp::AddConnector), "Add one from the directory, a plugin's server or an address"),
485− a("set_connector_key", Op::Customize(CustomizeOp::SetConnectorKey), "Put a key on one; never returned"),
486− a("remove_connector", Op::Customize(CustomizeOp::RemoveConnector), "Remove one with its secrets and grants"),
487− a("refresh_connector", Op::Customize(CustomizeOp::RefreshConnector), "List its tools again now"),
488− a("grant_connector", Op::Customize(CustomizeOp::GrantConnector), "Give it to every agent, a team or one agent"),
489− a("revoke_connector", Op::Customize(CustomizeOp::RevokeConnector), "Take one grant back"),
490− a("bundle", Op::Customize(CustomizeOp::Bundle), "What a job's Claude Code loads: plugins, library skills, connectors with headers"),
491− ],
492− },
493− Tool {
494431 name: "billing",
495432 title: "Billing",
496− 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.",
433+ 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 and its invoices. 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 a run's token.",
497434 default_action: Some("usage"),
498435 actions: &[
499436 a("usage", Op::GetUsage, "Usage over a range of days, by product, meter, project and day, and what paid for it"),
503440 a("buy_ai_credit", Op::BuyAiCredit, "A payment page to buy AI credit, for a person to open"),
504441 a("invoices", Op::ListInvoices, "Every invoice, the itemised usage invoices, and the next one so far"),
505442 a("billing_details", Op::GetBillingDetails, "Who invoices are made out to and the payment method on file"),
506− a("gateway_requests", Op::ListGatewayRequests, "Recent AI Gateway requests: model, tokens, cost, status and token"),
507443 ],
508444 },
509445 Tool {
510446 name: "security",
511447 title: "Security",
512− 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, a workspace's overview, security advisories and private vulnerability reports. Fix an alert with g1t. Findings are shown to those who can change the code only; published advisories to anyone who can see the repository. Give `repo` (owner/name), or `workspace` for lists across one.",
448+ 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, a workspace's overview, security advisories and private vulnerability reports. Findings are shown to those who can change the code only; published advisories to anyone who can see the repository. Give `repo` (owner/name), or `workspace` for lists across one.",
513449 default_action: Some("secret_alerts"),
514450 actions: &[
515451 a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"),
534470 a("vulnerability_alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), "Vulnerable dependencies; by state, severity, ecosystem, package"),
535471 a("vulnerability_alert", Op::Security(SecurityOp::GetVulnerabilityAlert), "One vulnerability alert"),
536472 a("update_vulnerability_alert", Op::Security(SecurityOp::UpdateVulnerabilityAlert), "Dismiss a vulnerability alert with a reason, or reopen it"),
537− a("fix", Op::Security(SecurityOp::FixAlert), "Put g1t on an issue to fix an alert"),
538473 a("dependency_graph", Op::Security(SecurityOp::GetDependencyGraph), "Every package the lockfiles resolve, direct or transitive"),
539474 a("sbom", Op::Security(SecurityOp::GetSbom), "The dependency graph as an SPDX 2.3 document"),
540475 a("compare_dependencies", Op::Security(SecurityOp::CompareDependencies), "What changes in dependencies between base...head"),
556491 Tool {
557492 name: "notifications",
558493 title: "Notifications",
559− 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.",
494+ 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`): 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.",
560495 default_action: Some("list"),
561496 actions: &[
562497 a("list", Op::ListNotifications, "Unread threads, latest first; all, a view, a reason, a repository"),
667602 | Op::DeleteActionsVariable
668603 | Op::SetActionsSecret
669604 | Op::SetActionsVariable
670− | Op::SetModelRoutes
671605 | Op::SetBasePermission
672606 | Op::DeleteTeam
673607 | Op::RemoveTeamRepo
676610 | Op::DeleteRunnerGroup
677611 | Op::UpdateRunnerSettings
678612 | Op::Folios(FoliosOp::SetAccess | FoliosOp::Purge)
679− | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
680613 )
681614 }
682615
690623 pub enum Gate<'a> {
691624 /// No limit beyond the person's own role.
692625 Everything,
693− /// A g1t agent's token: the operations its run lists.
626+ /// A run's token (a checks, merge queue or deploy job): the
627+ /// operations its run lists.
694628 Agent(&'a AgentScope),
695629 /// An access token with scopes.
696630 Token(&'a TokenAccess),
894828 use super::*;
895829 use g1t_contracts::scopes::{Preset, Scope};
896830
831+ /// Operations g1t no longer has, which the scopes table in
832+ /// g1t-contracts still names until it is trimmed.
833+ const RETIRED: &[&str] = &[
834+ "message_agent",
835+ "answer_message",
836+ "take_messages",
837+ "search_context",
838+ "get_entity",
839+ "assign_issue",
840+ "delegate",
841+ "plan_work",
842+ "get_plan",
843+ "apply_plan",
844+ "get_model_routes",
845+ "set_model_routes",
846+ "get_agent_computer",
847+ "wake_agent_computer",
848+ "sleep_agent_computer",
849+ "reset_agent_computer",
850+ "list_agent_computer_commands",
851+ "list_gateway_requests",
852+ "fix_security_alert",
853+ "list_marketplaces",
854+ "add_marketplace",
855+ "remove_marketplace",
856+ "refresh_marketplace",
857+ "discover",
858+ "list_plugins",
859+ "get_plugin",
860+ "install_plugin",
861+ "set_plugin_enabled",
862+ "uninstall_plugin",
863+ "update_plugin",
864+ "list_plugin_agents",
865+ "list_connectors",
866+ "get_connector",
867+ "add_connector",
868+ "set_connector_key",
869+ "remove_connector",
870+ "refresh_connector",
871+ "grant_connector",
872+ "revoke_connector",
873+ "customize_bundle",
874+ ];
875+
897876 fn listed(gate: &Gate) -> Vec<Value> {
898877 TOOLS.iter().filter_map(|tool| tool.listed(gate)).collect()
899878 }
928907 assert!(tool.action(default).is_some(), "{}", tool.name);
929908 }
930909 }
931− assert!(TOOLS.len() <= 21, "{} tools", TOOLS.len());
910+ assert!(TOOLS.len() <= 18, "{} tools", TOOLS.len());
932911 }
933912
934913 #[test]
940919 assert_eq!(mapped + usize::from(free), 1, "{}", op.name());
941920 }
942921 for (name, _) in OPERATIONS {
943− assert!(Op::by_name(name).is_some(), "{name} is not an operation");
922+ assert!(Op::by_name(name).is_some() || RETIRED.contains(name), "{name} is not an operation");
944923 }
945924 }
946925
994973 }
995974 let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap();
996975 assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels"]));
997− // Nothing of the agent tool is a read.
998− assert!(!tools.iter().any(|tool| tool["name"] == "agent"));
999976 }
1000977
1001978 #[test]
1003980 let access = token(Some(vec![Scope::IssuesWrite]));
1004981 let names: Vec<Value> = listed(&Gate::Token(&access)).into_iter().map(|tool| tool["name"].clone()).collect();
1005982 // Labels and milestones are the repository's, managed with issues:write.
1006− assert_eq!(names, vec![json!("repository"), json!("issue"), json!("plan"), json!("account")]);
983+ assert_eq!(names, vec![json!("repository"), json!("issue"), json!("account")]);
1007984 // Notifications are a resource of their own: reading them lists
1008985 // only what reads.
1009986 let reader = token(Some(vec![Scope::NotificationsRead]));
+1−6
1818 { "binding": "REPOS", "service": "g1t-repos" },
1919 { "binding": "WORK", "service": "g1t-work" },
2020 { "binding": "EVENTS", "service": "g1t-events" },
21− { "binding": "RUNNER", "service": "g1t-runner" },
2221 { "binding": "BILLING", "service": "g1t-billing" },
2322 { "binding": "INTEGRATIONS", "service": "g1t-integrations" },
2423 { "binding": "WEBHOOKS", "service": "g1t-webhooks" },
2827 // Observability: a project's requests, logs, traces, errors and
2928 // metrics, and OTLP sent to /otlp/v1/{traces,logs,metrics}.
3029 { "binding": "TELEMETRY", "service": "g1t-telemetry" },
31− // The context hub: search_context and get_entity.
32− { "binding": "CONTEXT", "service": "g1t-context" },
3330 { "binding": "SEARCH", "service": "g1t-search" },
3431 // Secret and dependency alerts: list_security_alerts and dismissing them.
3532 { "binding": "SECURITY", "service": "g1t-security" },
3835 // Packages: list_packages, their settings, deleting and restoring them.
3936 { "binding": "PACKAGES", "service": "g1t-packages" },
4037 // Artifacts (folios): the artifact tool and /workspaces/{ws}/artifacts.
41− { "binding": "ARTIFACTS", "service": "g1t-artifacts" },
42− // Workspace agents' own computers: /workspaces/{ws}/agents/{agent}/computer.
43− { "binding": "AGENTS", "service": "g1t-agents" }
38+ { "binding": "ARTIFACTS", "service": "g1t-artifacts" }
4439 ],
4540 // GitHub Actions artifacts older runners kept, in chunks, with KV's own
4641 // expiry, read until it passes (and cache
+0−0

Binary or large file; its contents are not shown.