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.
14 files+126−32340/14 viewed
| 6 | 6 | //! Reading is for the workspace's members, a workspace's own token | |
| 7 | 7 | //! included. Changing the budget and buying AI credit are for its owners, | |
| 8 | 8 | //! 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. | |
| 10 | 10 | ||
| 11 | 11 | use std::collections::BTreeMap; | |
| 12 | 12 | ||
| ⋯ | |||
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | 104 | /// 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. | |
| 107 | 107 | pub(crate) fn person(viewer: &Viewer, agent_scoped: bool) -> std::result::Result<&User, (FailureCode, &'static str)> { | |
| 108 | 108 | match viewer { | |
| 109 | 109 | None => Err((FailureCode::Unauthenticated, "This needs a g1t access token.")), | |
| 110 | 110 | Some(user) if agent_scoped || user.kind == PrincipalKind::Agent => Err(( | |
| 111 | 111 | 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.", | |
| 113 | 113 | )), | |
| 114 | 114 | Some(user) if user.kind != PrincipalKind::User => Err(( | |
| 115 | 115 | FailureCode::Forbidden, | |
| ⋯ | |||
| 412 | 412 | }) | |
| 413 | 413 | } | |
| 414 | 414 | 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 | − | } | |
| 434 | 415 | _ => failed(FailureCode::Invalid, "Not a billing operation."), | |
| 435 | 416 | } | |
| 436 | 417 | } | |
| ⋯ | |||
| 520 | 501 | let agent = User { username: "g1t".into(), kind: PrincipalKind::Agent, ..User::default() }; | |
| 521 | 502 | let (code, message) = person(&Some(agent), false).unwrap_err(); | |
| 522 | 503 | assert_eq!(code, FailureCode::Forbidden); | |
| 523 | − | assert!(message.contains("agents never change billing")); | |
| 504 | + | assert!(message.contains("never changes billing")); | |
| 524 | 505 | let workspace = User { username: "acme".into(), kind: PrincipalKind::Workspace, ..User::default() }; | |
| 525 | 506 | let (code, message) = person(&Some(workspace), false).unwrap_err(); | |
| 526 | 507 | assert_eq!(code, FailureCode::Forbidden); | |
| ⋯ | |||
| 578 | 559 | assert!(shown.get("invoices").is_none() && shown.get("upcoming").is_none()); | |
| 579 | 560 | } | |
| 580 | 561 | ||
| 581 | − | const OPS: [Op; 8] = [ | |
| 562 | + | const OPS: [Op; 7] = [ | |
| 582 | 563 | Op::GetUsage, | |
| 583 | 564 | Op::GetBudget, | |
| 584 | 565 | Op::SetBudget, | |
| ⋯ | |||
| 586 | 567 | Op::BuyAiCredit, | |
| 587 | 568 | Op::ListInvoices, | |
| 588 | 569 | Op::GetBillingDetails, | |
| 589 | − | Op::ListGatewayRequests, | |
| 590 | 570 | ]; | |
| 591 | 571 | ||
| 592 | 572 | /// Billing belongs to a workspace, needs someone signed in, and is one | |
| ⋯ | |||
| 612 | 592 | for preset in [Preset::ReadOnly, Preset::Agent] { | |
| 613 | 593 | let access = token(preset); | |
| 614 | 594 | 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()); | |
| 616 | 596 | } | |
| 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"]); | |
| 621 | 597 | let full = TokenAccess::full(); | |
| 622 | 598 | assert_eq!(tool.visible(&Gate::Token(&full)).len(), OPS.len()); | |
| 623 | 599 | } | |
| 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 | − | } |
| 618 | 618 | None => Err(Outcome::fail(FailureCode::Unauthenticated, "This needs a g1t access token.")), | |
| 619 | 619 | Some(user) if user.kind == PrincipalKind::Workspace => Err(Outcome::fail( | |
| 620 | 620 | 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.", | |
| 622 | 622 | )), | |
| 623 | 623 | Some(user) => Ok(user.clone()), | |
| 624 | 624 | } |
| 13 | 13 | mod billing; | |
| 14 | 14 | mod blobs; | |
| 15 | 15 | mod checks; | |
| 16 | − | mod customize; | |
| 17 | 16 | mod deployments; | |
| 18 | 17 | mod telemetry; | |
| 19 | 18 | mod deploy_keys; | |
| ⋯ | |||
| 43 | 42 | mod token_policy; | |
| 44 | 43 | mod toolkit; | |
| 45 | 44 | ||
| 46 | − | use g1t_contracts::billing::{FinishRunArgs, RunTokens}; | |
| 47 | 45 | use g1t_contracts::identity::{ | |
| 48 | 46 | DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs, | |
| 49 | 47 | }; | |
| 50 | 48 | use g1t_contracts::work::{ | |
| 51 | − | CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, ReportPlanArgs, | |
| 52 | − | ReportQueueArgs, ReportReviewArgs, | |
| 49 | + | CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, | |
| 50 | + | ReportQueueArgs, | |
| 53 | 51 | }; | |
| 54 | − | use g1t_contracts::identity::AgentScope; | |
| 55 | 52 | use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer}; | |
| 56 | 53 | use g1t_kit::wire; | |
| 57 | 54 | use serde_json::{Value, json}; | |
| ⋯ | |||
| 425 | 422 | fn outcome_as_given(answered: Value) -> Result<Response> { | |
| 426 | 423 | match serde_json::from_value::<Outcome<Value>>(answered)? { | |
| 427 | 424 | 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 })), | |
| 516 | 425 | Outcome::Fail(refused) => failure(&refused), | |
| 517 | 426 | } | |
| 518 | 427 | } | |
| ⋯ | |||
| 647 | 556 | ); | |
| 648 | 557 | } | |
| 649 | 558 | 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. | |
| 652 | 562 | if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) { | |
| 653 | 563 | services.scope = Some(acting.scope.clone()); | |
| 654 | 564 | } 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."); | |
| 670 | 566 | } | |
| 671 | 567 | if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? { | |
| 672 | 568 | return Ok(response); | |
| ⋯ | |||
| 783 | 679 | with_runtime(&mut spec, &services.addresses.api, oidc::configured(env)); | |
| 784 | 680 | Response::from_json(&spec) | |
| 785 | 681 | } | |
| 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), | |
| 816 | 682 | Outcome::Fail(refused) => failure(&refused), | |
| 817 | 683 | }; | |
| 818 | 684 | } | |
| 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 | − | } | |
| 838 | 685 | ("POST", path) if path.starts_with("/checks/") => { | |
| 839 | 686 | let run_id = path.trim_start_matches("/checks/").to_owned(); | |
| 840 | 687 | 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; | |
| 856 | 688 | } | |
| 857 | 689 | _ => {} | |
| 858 | 690 | } | |
| 10 | 10 | ||
| 11 | 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | 12 | ||
| 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. | |
| 15 | 15 | 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. | |
| 18 | 19 | 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."; | |
| 20 | 21 | ||
| 21 | 22 | fn result(id: &Value, value: Value) -> Value { | |
| 22 | 23 | json!({ "jsonrpc": "2.0", "id": id, "result": value }) | |
| ⋯ | |||
| 26 | 27 | json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } }) | |
| 27 | 28 | } | |
| 28 | 29 | ||
| 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 | |
| 30 | 31 | /// access token's scopes, or nothing beyond the person's role. | |
| 31 | 32 | fn gate<'a>(services: &'a Services, viewer: &'a Viewer) -> Gate<'a> { | |
| 32 | 33 | if let Some(scope) = &services.scope { | |
| ⋯ | |||
| 209 | 210 | }, | |
| 210 | 211 | } | |
| 211 | 212 | } | |
| 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 | + | } | |
| 10 | 10 | use crate::about::AboutOp; | |
| 11 | 11 | use crate::run_artifacts::ArtifactsOp; | |
| 12 | 12 | use crate::archives::ArchiveOp; | |
| 13 | − | use crate::customize::CustomizeOp; | |
| 14 | 13 | use crate::deploy_keys::DeployKeysOp; | |
| 15 | 14 | use crate::mirrors::MirrorsOp; | |
| 16 | 15 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 102 | 101 | ), | |
| 103 | 102 | ( | |
| 104 | 103 | "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.", | |
| 106 | 105 | &[ | |
| 107 | 106 | Op::GetUsage, | |
| 108 | 107 | Op::GetBudget, | |
| ⋯ | |||
| 111 | 110 | Op::BuyAiCredit, | |
| 112 | 111 | Op::ListInvoices, | |
| 113 | 112 | Op::GetBillingDetails, | |
| 114 | − | Op::ListGatewayRequests, | |
| 115 | 113 | ], | |
| 116 | 114 | ), | |
| 117 | 115 | ( | |
| ⋯ | |||
| 264 | 262 | Op::Security(SecurityOp::ListAnalyses), | |
| 265 | 263 | Op::Security(SecurityOp::UploadSarif), | |
| 266 | 264 | Op::Security(SecurityOp::GetSarifUpload), | |
| 267 | − | Op::Security(SecurityOp::FixAlert), | |
| 268 | 265 | ], | |
| 269 | 266 | ), | |
| 270 | 267 | ( | |
| ⋯ | |||
| 351 | 348 | Op::UpdateIssue, | |
| 352 | 349 | Op::CloseIssue, | |
| 353 | 350 | Op::ReopenIssue, | |
| 354 | − | Op::AssignIssue, | |
| 355 | − | Op::Delegate, | |
| 356 | 351 | Op::AddComment, | |
| 357 | 352 | Op::EditComment, | |
| 358 | 353 | Op::DeleteComment, | |
| ⋯ | |||
| 377 | 372 | Op::UpdateMilestone, | |
| 378 | 373 | Op::DeleteMilestone, | |
| 379 | 374 | ], | |
| 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], | |
| 385 | 375 | ), | |
| 386 | 376 | ( | |
| 387 | 377 | "Pull requests", | |
| ⋯ | |||
| 401 | 391 | Op::ClosePullRequest, | |
| 402 | 392 | Op::ReopenPullRequest, | |
| 403 | 393 | Op::GetMergeQueue, | |
| 404 | − | Op::MessageAgent, | |
| 405 | − | Op::AnswerMessage, | |
| 406 | − | Op::TakeMessages, | |
| 407 | 394 | ], | |
| 408 | 395 | ), | |
| 409 | 396 | ( | |
| ⋯ | |||
| 413 | 400 | ), | |
| 414 | 401 | ( | |
| 415 | 402 | "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.", | |
| 417 | 404 | &[Op::Remember, Op::Recall], | |
| 418 | 405 | ), | |
| 419 | 406 | ( | |
| ⋯ | |||
| 422 | 409 | &[Op::Search], | |
| 423 | 410 | ), | |
| 424 | 411 | ( | |
| 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 | − | ( | |
| 430 | 412 | "Actions", | |
| 431 | 413 | "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.", | |
| 432 | 414 | &[ | |
| ⋯ | |||
| 548 | 530 | ), | |
| 549 | 531 | ( | |
| 550 | 532 | "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.", | |
| 552 | 534 | &[ | |
| 553 | 535 | Op::ListRunners, | |
| 554 | 536 | Op::CreateRunnerRegistrationToken, | |
| ⋯ | |||
| 559 | 541 | Op::DeleteRunnerGroup, | |
| 560 | 542 | Op::GetRunnerSettings, | |
| 561 | 543 | 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, | |
| 573 | 544 | ], | |
| 574 | 545 | ), | |
| 575 | 546 | ( | |
| ⋯ | |||
| 587 | 558 | ), | |
| 588 | 559 | ( | |
| 589 | 560 | "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.", | |
| 591 | 562 | &[ | |
| 592 | 563 | Op::ListIntegrations, | |
| 593 | 564 | Op::ConnectIntegration, | |
| 594 | 565 | Op::UpdateIntegration, | |
| 595 | 566 | Op::DisconnectIntegration, | |
| 596 | 567 | Op::TestIntegration, | |
| 597 | − | Op::GetModelRoutes, | |
| 598 | − | Op::SetModelRoutes, | |
| 599 | 568 | Op::GetContext, | |
| 600 | 569 | 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), | |
| 628 | 570 | ], | |
| 629 | 571 | ), | |
| 630 | 572 | ( | |
| ⋯ | |||
| 707 | 649 | Op::UpdateRepoSettings => "Update repository settings", | |
| 708 | 650 | Op::ListCheckNames => "List check names", | |
| 709 | 651 | Op::GetMergeQueue => "Get the merge queue", | |
| 710 | − | Op::MessageAgent => "Message an agent", | |
| 711 | − | Op::AnswerMessage => "Answer a message", | |
| 712 | − | Op::TakeMessages => "Take new messages", | |
| 713 | 652 | Op::Remember => "Remember something", | |
| 714 | 653 | Op::Recall => "Recall memory", | |
| 715 | − | Op::SearchContext => "Search the context hub", | |
| 716 | − | Op::GetEntity => "Get a catalog entry", | |
| 717 | 654 | Op::Search => "Search g1t", | |
| 718 | 655 | Op::ListIssues => "List issues", | |
| 719 | 656 | Op::GetIssue => "Get an issue", | |
| ⋯ | |||
| 721 | 658 | Op::UpdateIssue => "Update an issue", | |
| 722 | 659 | Op::CloseIssue => "Close an issue", | |
| 723 | 660 | 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", | |
| 729 | 661 | Op::ListLabels => "List labels", | |
| 730 | 662 | Op::CreateLabel => "Create a label", | |
| 731 | 663 | Op::UpdateLabel => "Update a label", | |
| ⋯ | |||
| 764 | 696 | Op::TestIntegration => "Test an integration", | |
| 765 | 697 | Op::GetContext => "Look up a ticket", | |
| 766 | 698 | Op::ImportIssue => "Import an issue", | |
| 767 | − | Op::GetModelRoutes => "Get model routes", | |
| 768 | − | Op::SetModelRoutes => "Set model routes", | |
| 769 | 699 | Op::ListWebhooks => "List webhooks", | |
| 770 | 700 | Op::CreateWebhook => "Create a webhook", | |
| 771 | 701 | Op::UpdateWebhook => "Update a webhook", | |
| ⋯ | |||
| 789 | 719 | Op::DeleteActionsVariable => "Delete a variable", | |
| 790 | 720 | Op::ListRunners => "List self-hosted runners", | |
| 791 | 721 | 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", | |
| 797 | 722 | Op::GetRunnerSettings => "Get runner settings", | |
| 798 | 723 | Op::CreateRunnerRegistrationToken => "Create a runner registration token", | |
| 799 | 724 | Op::RemoveRunner => "Remove a self-hosted runner", | |
| ⋯ | |||
| 838 | 763 | Op::BuyAiCredit => "Buy AI credit", | |
| 839 | 764 | Op::ListInvoices => "List a workspace's invoices", | |
| 840 | 765 | Op::GetBillingDetails => "Get a workspace's billing details", | |
| 841 | − | Op::ListGatewayRequests => "List a workspace's AI Gateway requests", | |
| 842 | 766 | Op::PinProject => "Pin a project", | |
| 843 | 767 | Op::UnpinProject => "Unpin a project", | |
| 844 | 768 | Op::ReorderPinnedProjects => "Reorder your pinned projects", | |
| ⋯ | |||
| 876 | 800 | Op::Mirrors(op) => op.title(), | |
| 877 | 801 | Op::Packages(op) => op.title(), | |
| 878 | 802 | Op::Folios(op) => op.title(), | |
| 879 | − | Op::Customize(op) => op.title(), | |
| 880 | 803 | } | |
| 881 | 804 | } | |
| 882 | 805 | ||
| 883 | 806 | /// 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. | |
| 887 | 809 | fn may_need_payment(op: Op) -> Option<&'static str> { | |
| 888 | 810 | 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."), | |
| 891 | 811 | Op::UpdateRepo | Op::SetRepoVisibility | Op::TransferRepo => Some( | |
| 892 | 812 | "A free workspace's private storage has no room for this private repository.", | |
| 893 | 813 | ), | |
A large diff (1,040 lines) is not shown until you ask for it.
A large diff (1,782 lines) is not shown until you ask for it.
| 133 | 133 | Op::Packages(_) => return as_is, | |
| 134 | 134 | // Artifacts are shaped by the API itself, in `snake_case`. | |
| 135 | 135 | 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, | |
| 138 | 136 | // Built by the API itself, in `snake_case`. | |
| 139 | 137 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 140 | 138 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| ⋯ | |||
| 174 | 172 | through::<work::Issue>(op, sent) | |
| 175 | 173 | } | |
| 176 | 174 | Op::GetIssue => through::<work::IssueDetail>(op, sent), | |
| 177 | − | Op::Delegate => through::<work::Delegated>(op, sent), | |
| 178 | 175 | Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent), | |
| 179 | 176 | Op::UpdatePullRequest => through::<work::Pull>(op, sent), | |
| 180 | 177 | Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent), | |
| ⋯ | |||
| 188 | 185 | | Op::ReopenPullRequest | |
| 189 | 186 | | Op::ConvertPullRequestToDraft | |
| 190 | 187 | | Op::MergePullRequest | |
| 191 | − | | Op::AssignIssue | |
| 192 | 188 | | Op::RequestReviewers | |
| 193 | 189 | | Op::RemoveRequestedReviewers => { | |
| 194 | 190 | through::<work::Pull>(op, sent) | |
| ⋯ | |||
| 208 | 204 | Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent), | |
| 209 | 205 | Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent), | |
| 210 | 206 | Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent), | |
| 211 | − | Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent), | |
| 212 | 207 | Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent), | |
| 213 | 208 | Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => { | |
| 214 | 209 | through::<g1t_contracts::accounts::AccountEmails>(op, sent) | |
| 5 | 5 | use crate::about::AboutOp; | |
| 6 | 6 | use crate::run_artifacts::ArtifactsOp; | |
| 7 | 7 | use crate::archives::ArchiveOp; | |
| 8 | − | use crate::customize::CustomizeOp; | |
| 9 | 8 | use crate::deploy_keys::DeployKeysOp; | |
| 10 | 9 | use crate::mirrors::MirrorsOp; | |
| 11 | 10 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 291 | 290 | route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]), | |
| 292 | 291 | route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]), | |
| 293 | 292 | // The AI Gateway's log of a workspace's requests. | |
| 294 | − | route("GET", "/workspaces/:workspace/gateway/requests", Op::ListGatewayRequests, &[("limit", "limit"), ("before", "before")]), | |
| 295 | 293 | // Code owners: the CODEOWNERS file, checked. | |
| 296 | 294 | route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]), | |
| 297 | 295 | // Security alerts: secrets and vulnerable dependencies. | |
| ⋯ | |||
| 335 | 333 | route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]), | |
| 336 | 334 | route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]), | |
| 337 | 335 | 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), &[]), | |
| 339 | 336 | route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]), | |
| 340 | 337 | route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]), | |
| 341 | 338 | route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]), | |
| ⋯ | |||
| 540 | 537 | &[], | |
| 541 | 538 | ), | |
| 542 | 539 | 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 | − | ), | |
| 561 | 540 | route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]), | |
| 562 | 541 | route( | |
| 563 | 542 | "GET", | |
| ⋯ | |||
| 615 | 594 | "POST", | |
| 616 | 595 | "/repos/:owner/:name/issues/:number/reopen", | |
| 617 | 596 | Op::ReopenIssue, | |
| 618 | − | &[], | |
| 619 | − | ), | |
| 620 | − | route( | |
| 621 | − | "POST", | |
| 622 | − | "/repos/:owner/:name/issues/:number/assign", | |
| 623 | − | Op::AssignIssue, | |
| 624 | 597 | &[], | |
| 625 | 598 | ), | |
| 626 | 599 | route( | |
| 627 | 600 | "POST", | |
| 628 | 601 | "/repos/:owner/:name/issues/import", | |
| 629 | 602 | Op::ImportIssue, | |
| 630 | − | &[], | |
| 631 | − | ), | |
| 632 | − | route( | |
| 633 | − | "POST", | |
| 634 | − | "/repos/:owner/:name/issues/delegate", | |
| 635 | − | Op::Delegate, | |
| 636 | 603 | &[], | |
| 637 | 604 | ), | |
| 638 | 605 | route( | |
| ⋯ | |||
| 640 | 607 | "/repos/:owner/:name/context", | |
| 641 | 608 | Op::GetContext, | |
| 642 | 609 | &[("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 | − | &[], | |
| 655 | 610 | ), | |
| 656 | 611 | route( | |
| 657 | 612 | "GET", | |
| ⋯ | |||
| 747 | 702 | "POST", | |
| 748 | 703 | "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver", | |
| 749 | 704 | 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, | |
| 762 | 705 | &[], | |
| 763 | 706 | ), | |
| 764 | 707 | route( | |
| ⋯ | |||
| 779 | 722 | Op::TestIntegration, | |
| 780 | 723 | &[], | |
| 781 | 724 | ), | |
| 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), &[]), | |
| 805 | 725 | route( | |
| 806 | 726 | "GET", | |
| 807 | 727 | "/repos/:owner/:name/actions/workflows", | |
| ⋯ | |||
| 1019 | 939 | route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]), | |
| 1020 | 940 | route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]), | |
| 1021 | 941 | 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 | − | ), | |
| 1036 | 942 | route( | |
| 1037 | 943 | "POST", | |
| 1038 | 944 | "/repos/:owner/:name/issues/:number/comments", | |
| ⋯ | |||
| 1149 | 1055 | matches!( | |
| 1150 | 1056 | self.op, | |
| 1151 | 1057 | Op::DeleteComment | |
| 1152 | − | | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector) | |
| 1153 | 1058 | ) | |
| 1154 | 1059 | } | |
| 1155 | 1060 | ||
| ⋯ | |||
| 1543 | 1448 | assert_eq!(op("POST", "/workspaces/acme/ai_credit/checkout"), Op::BuyAiCredit); | |
| 1544 | 1449 | assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices); | |
| 1545 | 1450 | assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails); | |
| 1546 | − | assert_eq!(op("GET", "/workspaces/acme/gateway/requests"), Op::ListGatewayRequests); | |
| 1547 | 1451 | } | |
| 1548 | 1452 | ||
| 1549 | 1453 | #[test] | |
| 52 | 52 | ListVulnerabilityAlerts, | |
| 53 | 53 | GetVulnerabilityAlert, | |
| 54 | 54 | UpdateVulnerabilityAlert, | |
| 55 | − | FixAlert, | |
| 56 | 55 | GetDependencyGraph, | |
| 57 | 56 | GetSbom, | |
| 58 | 57 | CompareDependencies, | |
| ⋯ | |||
| 75 | 74 | /// Every one: `Op::ALL` lists each as `Op::Security(…)`, which a test | |
| 76 | 75 | /// checks against this. | |
| 77 | 76 | #[cfg(test)] | |
| 78 | − | pub const ALL: [SecurityOp; 39] = [ | |
| 77 | + | pub const ALL: [SecurityOp; 38] = [ | |
| 79 | 78 | SecurityOp::ListSecretAlerts, | |
| 80 | 79 | SecurityOp::GetSecretAlert, | |
| 81 | 80 | SecurityOp::UpdateSecretAlert, | |
| ⋯ | |||
| 98 | 97 | SecurityOp::ListVulnerabilityAlerts, | |
| 99 | 98 | SecurityOp::GetVulnerabilityAlert, | |
| 100 | 99 | SecurityOp::UpdateVulnerabilityAlert, | |
| 101 | − | SecurityOp::FixAlert, | |
| 102 | 100 | SecurityOp::GetDependencyGraph, | |
| 103 | 101 | SecurityOp::GetSbom, | |
| 104 | 102 | SecurityOp::CompareDependencies, | |
| ⋯ | |||
| 141 | 139 | SecurityOp::ListVulnerabilityAlerts => "list_vulnerability_alerts", | |
| 142 | 140 | SecurityOp::GetVulnerabilityAlert => "get_vulnerability_alert", | |
| 143 | 141 | SecurityOp::UpdateVulnerabilityAlert => "update_vulnerability_alert", | |
| 144 | − | SecurityOp::FixAlert => "fix_security_alert", | |
| 145 | 142 | SecurityOp::GetDependencyGraph => "get_dependency_graph", | |
| 146 | 143 | SecurityOp::GetSbom => "get_sbom", | |
| 147 | 144 | SecurityOp::CompareDependencies => "compare_dependencies", | |
| ⋯ | |||
| 186 | 183 | SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts", | |
| 187 | 184 | SecurityOp::GetVulnerabilityAlert => "Get a vulnerability alert", | |
| 188 | 185 | SecurityOp::UpdateVulnerabilityAlert => "Dismiss or reopen a vulnerability alert", | |
| 189 | − | SecurityOp::FixAlert => "Fix an alert with g1t", | |
| 190 | 186 | SecurityOp::GetDependencyGraph => "Get the dependency graph", | |
| 191 | 187 | SecurityOp::GetSbom => "Export an SBOM", | |
| 192 | 188 | SecurityOp::CompareDependencies => "Compare dependencies", | |
| ⋯ | |||
| 230 | 226 | 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.", | |
| 231 | 227 | SecurityOp::GetVulnerabilityAlert => "Get one vulnerability alert by id (vul_…).", | |
| 232 | 228 | 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.", | |
| 234 | 229 | 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.", | |
| 235 | 230 | SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.", | |
| 236 | 231 | 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.", | |
| ⋯ | |||
| 433 | 428 | }), | |
| 434 | 429 | &["repo", "id", "state"], | |
| 435 | 430 | ), | |
| 436 | − | SecurityOp::FixAlert => ( | |
| 437 | − | json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: cod_…, vul_… or sec_…" } }), | |
| 438 | − | &["repo", "id"], | |
| 439 | − | ), | |
| 440 | 431 | SecurityOp::GetDependencyGraph | SecurityOp::GetSbom | SecurityOp::GetSettings => (json!({ "repo": repo() }), &["repo"]), | |
| 441 | 432 | SecurityOp::CompareDependencies => ( | |
| 442 | 433 | json!({ | |
| ⋯ | |||
| 1026 | 1017 | }, | |
| 1027 | 1018 | Outcome::Fail(failure) => Ok(Outcome::Fail(failure)), | |
| 1028 | 1019 | } | |
| 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 | |
| 1033 | 1020 | } | |
| 1034 | 1021 | SecurityOp::GetDependencyGraph => { | |
| 1035 | 1022 | let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) }; | |
| 21 | 21 | use crate::about::AboutOp; | |
| 22 | 22 | use crate::run_artifacts::ArtifactsOp; | |
| 23 | 23 | use crate::archives::ArchiveOp; | |
| 24 | − | use crate::customize::CustomizeOp; | |
| 25 | 24 | use crate::deploy_keys::DeployKeysOp; | |
| 26 | 25 | use crate::mirrors::MirrorsOp; | |
| 27 | 26 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 60 | 59 | Tool { | |
| 61 | 60 | name: "search", | |
| 62 | 61 | 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.", | |
| 64 | 63 | default_action: Some("code"), | |
| 65 | 64 | actions: &[ | |
| 66 | 65 | 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"), | |
| 69 | 66 | a("ticket", Op::GetContext, "A Jira, Linear or Sentry item the work refers to, as it is now"), | |
| 70 | 67 | ], | |
| 71 | 68 | }, | |
| ⋯ | |||
| 189 | 186 | a("reopen", Op::ReopenPullRequest, "Reopen a closed one"), | |
| 190 | 187 | a("merge", Op::MergePullRequest, "Land it, or join the merge queue"), | |
| 191 | 188 | 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"), | |
| 221 | 189 | ], | |
| 222 | 190 | }, | |
| 223 | 191 | Tool { | |
| ⋯ | |||
| 233 | 201 | Tool { | |
| 234 | 202 | name: "workflow", | |
| 235 | 203 | 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.", | |
| 237 | 205 | default_action: None, | |
| 238 | 206 | actions: &[ | |
| 239 | 207 | a("list", Op::ListWorkflows, "Workflows on the default branch"), | |
| ⋯ | |||
| 296 | 264 | a("create_runner_group", Op::CreateRunnerGroup, "Make a group, for some repositories"), | |
| 297 | 265 | a("update_runner_group", Op::UpdateRunnerGroup, "Rename a group or change its repositories"), | |
| 298 | 266 | 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"), | |
| 300 | 268 | a("update_runner_settings", Op::UpdateRunnerSettings, "Change them"), | |
| 301 | 269 | ], | |
| 302 | 270 | }, | |
| ⋯ | |||
| 418 | 386 | Tool { | |
| 419 | 387 | name: "workspace", | |
| 420 | 388 | 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.", | |
| 422 | 390 | default_action: None, | |
| 423 | 391 | actions: &[ | |
| 424 | 392 | a("get", Op::GetWorkspace, "A workspace's details and settings"), | |
| ⋯ | |||
| 433 | 401 | a("list_invites", Op::ListWorkspaceInvites, "Its invites"), | |
| 434 | 402 | a("invite_member", Op::InviteMember, "Invite someone by username or email address"), | |
| 435 | 403 | 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"), | |
| 437 | 405 | 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"), | |
| 439 | 407 | a("disconnect_integration", Op::DisconnectIntegration, "Remove one"), | |
| 440 | 408 | 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"), | |
| 443 | 409 | a("list_projects", Op::ListProjects, "Its projects you can see: what each is, where it runs, its links"), | |
| 444 | 410 | a("get_project", Op::GetProject, "One project"), | |
| 445 | 411 | a("update_project", Op::UpdateProject, "Change a project's name, description, kind, where it runs or its links"), | |
| ⋯ | |||
| 462 | 428 | ], | |
| 463 | 429 | }, | |
| 464 | 430 | 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 { | |
| 494 | 431 | name: "billing", | |
| 495 | 432 | 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.", | |
| 497 | 434 | default_action: Some("usage"), | |
| 498 | 435 | actions: &[ | |
| 499 | 436 | a("usage", Op::GetUsage, "Usage over a range of days, by product, meter, project and day, and what paid for it"), | |
| ⋯ | |||
| 503 | 440 | a("buy_ai_credit", Op::BuyAiCredit, "A payment page to buy AI credit, for a person to open"), | |
| 504 | 441 | a("invoices", Op::ListInvoices, "Every invoice, the itemised usage invoices, and the next one so far"), | |
| 505 | 442 | 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"), | |
| 507 | 443 | ], | |
| 508 | 444 | }, | |
| 509 | 445 | Tool { | |
| 510 | 446 | name: "security", | |
| 511 | 447 | 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.", | |
| 513 | 449 | default_action: Some("secret_alerts"), | |
| 514 | 450 | actions: &[ | |
| 515 | 451 | a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"), | |
| ⋯ | |||
| 534 | 470 | a("vulnerability_alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), "Vulnerable dependencies; by state, severity, ecosystem, package"), | |
| 535 | 471 | a("vulnerability_alert", Op::Security(SecurityOp::GetVulnerabilityAlert), "One vulnerability alert"), | |
| 536 | 472 | 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"), | |
| 538 | 473 | a("dependency_graph", Op::Security(SecurityOp::GetDependencyGraph), "Every package the lockfiles resolve, direct or transitive"), | |
| 539 | 474 | a("sbom", Op::Security(SecurityOp::GetSbom), "The dependency graph as an SPDX 2.3 document"), | |
| 540 | 475 | a("compare_dependencies", Op::Security(SecurityOp::CompareDependencies), "What changes in dependencies between base...head"), | |
| ⋯ | |||
| 556 | 491 | Tool { | |
| 557 | 492 | name: "notifications", | |
| 558 | 493 | 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.", | |
| 560 | 495 | default_action: Some("list"), | |
| 561 | 496 | actions: &[ | |
| 562 | 497 | a("list", Op::ListNotifications, "Unread threads, latest first; all, a view, a reason, a repository"), | |
| ⋯ | |||
| 667 | 602 | | Op::DeleteActionsVariable | |
| 668 | 603 | | Op::SetActionsSecret | |
| 669 | 604 | | Op::SetActionsVariable | |
| 670 | − | | Op::SetModelRoutes | |
| 671 | 605 | | Op::SetBasePermission | |
| 672 | 606 | | Op::DeleteTeam | |
| 673 | 607 | | Op::RemoveTeamRepo | |
| ⋯ | |||
| 676 | 610 | | Op::DeleteRunnerGroup | |
| 677 | 611 | | Op::UpdateRunnerSettings | |
| 678 | 612 | | Op::Folios(FoliosOp::SetAccess | FoliosOp::Purge) | |
| 679 | − | | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector) | |
| 680 | 613 | ) | |
| 681 | 614 | } | |
| 682 | 615 | ||
| ⋯ | |||
| 690 | 623 | pub enum Gate<'a> { | |
| 691 | 624 | /// No limit beyond the person's own role. | |
| 692 | 625 | 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. | |
| 694 | 628 | Agent(&'a AgentScope), | |
| 695 | 629 | /// An access token with scopes. | |
| 696 | 630 | Token(&'a TokenAccess), | |
| ⋯ | |||
| 894 | 828 | use super::*; | |
| 895 | 829 | use g1t_contracts::scopes::{Preset, Scope}; | |
| 896 | 830 | ||
| 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 | + | ||
| 897 | 876 | fn listed(gate: &Gate) -> Vec<Value> { | |
| 898 | 877 | TOOLS.iter().filter_map(|tool| tool.listed(gate)).collect() | |
| 899 | 878 | } | |
| ⋯ | |||
| 928 | 907 | assert!(tool.action(default).is_some(), "{}", tool.name); | |
| 929 | 908 | } | |
| 930 | 909 | } | |
| 931 | − | assert!(TOOLS.len() <= 21, "{} tools", TOOLS.len()); | |
| 910 | + | assert!(TOOLS.len() <= 18, "{} tools", TOOLS.len()); | |
| 932 | 911 | } | |
| 933 | 912 | ||
| 934 | 913 | #[test] | |
| ⋯ | |||
| 940 | 919 | assert_eq!(mapped + usize::from(free), 1, "{}", op.name()); | |
| 941 | 920 | } | |
| 942 | 921 | 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"); | |
| 944 | 923 | } | |
| 945 | 924 | } | |
| 946 | 925 | ||
| ⋯ | |||
| 994 | 973 | } | |
| 995 | 974 | let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap(); | |
| 996 | 975 | 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")); | |
| 999 | 976 | } | |
| 1000 | 977 | ||
| 1001 | 978 | #[test] | |
| ⋯ | |||
| 1003 | 980 | let access = token(Some(vec![Scope::IssuesWrite])); | |
| 1004 | 981 | let names: Vec<Value> = listed(&Gate::Token(&access)).into_iter().map(|tool| tool["name"].clone()).collect(); | |
| 1005 | 982 | // 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")]); | |
| 1007 | 984 | // Notifications are a resource of their own: reading them lists | |
| 1008 | 985 | // only what reads. | |
| 1009 | 986 | let reader = token(Some(vec![Scope::NotificationsRead])); | |
| 18 | 18 | { "binding": "REPOS", "service": "g1t-repos" }, | |
| 19 | 19 | { "binding": "WORK", "service": "g1t-work" }, | |
| 20 | 20 | { "binding": "EVENTS", "service": "g1t-events" }, | |
| 21 | − | { "binding": "RUNNER", "service": "g1t-runner" }, | |
| 22 | 21 | { "binding": "BILLING", "service": "g1t-billing" }, | |
| 23 | 22 | { "binding": "INTEGRATIONS", "service": "g1t-integrations" }, | |
| 24 | 23 | { "binding": "WEBHOOKS", "service": "g1t-webhooks" }, | |
| ⋯ | |||
| 28 | 27 | // Observability: a project's requests, logs, traces, errors and | |
| 29 | 28 | // metrics, and OTLP sent to /otlp/v1/{traces,logs,metrics}. | |
| 30 | 29 | { "binding": "TELEMETRY", "service": "g1t-telemetry" }, | |
| 31 | − | // The context hub: search_context and get_entity. | |
| 32 | − | { "binding": "CONTEXT", "service": "g1t-context" }, | |
| 33 | 30 | { "binding": "SEARCH", "service": "g1t-search" }, | |
| 34 | 31 | // Secret and dependency alerts: list_security_alerts and dismissing them. | |
| 35 | 32 | { "binding": "SECURITY", "service": "g1t-security" }, | |
| ⋯ | |||
| 38 | 35 | // Packages: list_packages, their settings, deleting and restoring them. | |
| 39 | 36 | { "binding": "PACKAGES", "service": "g1t-packages" }, | |
| 40 | 37 | // 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" } | |
| 44 | 39 | ], | |
| 45 | 40 | // GitHub Actions artifacts older runners kept, in chunks, with KV's own | |
| 46 | 41 | // expiry, read until it passes (and cache | |
Binary or large file; its contents are not shown.