Customize is in the public API and the MCP server, and reaches Claude Code on runners. REST has it under /workspaces/{workspace}/customize: the marketplaces a workspace reads (list, add by address, remove, refresh), Discover, its installed plugins (list, one by name@marketplace, install, switch on or off, update in place, uninstall), the agents plugins brought, and its connectors (list, one, add from the directory, a plugin's server or an address, put a key on one, remove, list its tools again, grant to every agent, a team or one agent, and take a grant back), each passing through to the agents service's method; members read with workspace:read and owners change with workspace:admin, OAuth sign-in stays on the site, and the bundle a run loads, GET …/customize/bundle, is agents:run for a job's agent token or a workspace's, never a person's, which the API refuses before asking. The MCP server gets one customize tool with an action per operation, removing a marketplace, uninstalling a plugin and removing a connector marked destructive; the scope tables in Rust and TypeScript carry the operations in the same order; implement, revise, answer, review, plan and update runs' tools credentials may read the bundle; and the reference carries an example and notes for every route, the docs' OpenAPI copy regenerated. On a runner, before Claude Code starts, the harness reads the workspace's bundle with the run's agent token, fetches each switched-on plugin at its pinned commit (GitHub's tarball of the commit, a shallow clone of a repository here, or the marketplace's zip checked against its sha256) into /work/.g1t/plugins/<name> and passes each with --plugin-dir, writes the library's skills that reach the agent as one more plugin, g1t-library, and merges the connectors that reach it into the --mcp-config file beside g1t's own server, their keys kept out of the transcript; one note says what was loaded, and nothing from Customize fails a run. The self-hosted runners guide says what a run gets from Customize and how it appears in Claude Code, the working-with-g1t guide too, and the API and MCP references list the resource.
22 files+1690−376 files not counted0/22 viewed
| 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 | + | } |
| 12 | 12 | mod billing; | |
| 13 | 13 | mod blobs; | |
| 14 | 14 | mod checks; | |
| 15 | + | mod customize; | |
| 15 | 16 | mod deployments; | |
| 16 | 17 | mod deploy_keys; | |
| 17 | 18 | mod limits; |
| 11 | 11 | const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"]; | |
| 12 | 12 | ||
| 13 | 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, 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. | |
| 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. | |
| 15 | 15 | Find a repository: account whoami lists your workspaces; repository list or search finds one. | |
| 16 | 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 | 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. |
| 9 | 9 | ||
| 10 | 10 | use crate::about::AboutOp; | |
| 11 | 11 | use crate::run_artifacts::ArtifactsOp; | |
| 12 | + | use crate::customize::CustomizeOp; | |
| 12 | 13 | use crate::deploy_keys::DeployKeysOp; | |
| 13 | 14 | use crate::mirrors::MirrorsOp; | |
| 14 | 15 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 549 | 550 | ], | |
| 550 | 551 | ), | |
| 551 | 552 | ( | |
| 553 | + | "Customize", | |
| 554 | + | "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.", | |
| 555 | + | &[ | |
| 556 | + | Op::Customize(CustomizeOp::ListMarketplaces), | |
| 557 | + | Op::Customize(CustomizeOp::AddMarketplace), | |
| 558 | + | Op::Customize(CustomizeOp::RemoveMarketplace), | |
| 559 | + | Op::Customize(CustomizeOp::RefreshMarketplace), | |
| 560 | + | Op::Customize(CustomizeOp::Discover), | |
| 561 | + | Op::Customize(CustomizeOp::ListPlugins), | |
| 562 | + | Op::Customize(CustomizeOp::GetPlugin), | |
| 563 | + | Op::Customize(CustomizeOp::InstallPlugin), | |
| 564 | + | Op::Customize(CustomizeOp::SetPluginEnabled), | |
| 565 | + | Op::Customize(CustomizeOp::UninstallPlugin), | |
| 566 | + | Op::Customize(CustomizeOp::UpdatePlugin), | |
| 567 | + | Op::Customize(CustomizeOp::ListPluginAgents), | |
| 568 | + | Op::Customize(CustomizeOp::ListConnectors), | |
| 569 | + | Op::Customize(CustomizeOp::GetConnector), | |
| 570 | + | Op::Customize(CustomizeOp::AddConnector), | |
| 571 | + | Op::Customize(CustomizeOp::SetConnectorKey), | |
| 572 | + | Op::Customize(CustomizeOp::RemoveConnector), | |
| 573 | + | Op::Customize(CustomizeOp::RefreshConnector), | |
| 574 | + | Op::Customize(CustomizeOp::GrantConnector), | |
| 575 | + | Op::Customize(CustomizeOp::RevokeConnector), | |
| 576 | + | Op::Customize(CustomizeOp::Bundle), | |
| 577 | + | ], | |
| 578 | + | ), | |
| 579 | + | ( | |
| 552 | 580 | "Artifacts", | |
| 553 | 581 | "A workspace's docs, slides, designs and dashboards (Artifacts mode): listing and searching the ones you can open, reading and editing their content in Markdown, making, moving, trashing and restoring them, their versions, and who can open them. Each answers for your own role on it. Not workflow runs' artifacts, which are under Actions. Slides, designs and dashboards answer that they are not here yet.", | |
| 554 | 582 | &[ | |
| ⋯ | |||
| 793 | 821 | Op::Mirrors(op) => op.title(), | |
| 794 | 822 | Op::Packages(op) => op.title(), | |
| 795 | 823 | Op::Folios(op) => op.title(), | |
| 824 | + | Op::Customize(op) => op.title(), | |
| 796 | 825 | } | |
| 797 | 826 | } | |
| 798 | 827 | ||
| 29 | 29 | use crate::checks::ChecksOp; | |
| 30 | 30 | use crate::about::AboutOp; | |
| 31 | 31 | use crate::run_artifacts::ArtifactsOp; | |
| 32 | + | use crate::customize::CustomizeOp; | |
| 32 | 33 | use crate::deploy_keys::DeployKeysOp; | |
| 33 | 34 | use crate::mirrors::MirrorsOp; | |
| 34 | 35 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 338 | 339 | /// Artifacts mode's docs, slides, designs and dashboards, kept by the | |
| 339 | 340 | /// artifacts service: folios.rs. | |
| 340 | 341 | Folios(FoliosOp), | |
| 342 | + | /// A workspace's Customize: marketplaces, plugins, connectors and the | |
| 343 | + | /// bundle a runner loads, kept by the agents service: customize.rs. | |
| 344 | + | Customize(CustomizeOp), | |
| 341 | 345 | } | |
| 342 | 346 | ||
| 343 | 347 | fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> { | |
| ⋯ | |||
| 707 | 711 | } | |
| 708 | 712 | ||
| 709 | 713 | impl Op { | |
| 710 | − | pub const ALL: [Op; 350] = [ | |
| 714 | + | pub const ALL: [Op; 371] = [ | |
| 711 | 715 | Op::Whoami, | |
| 712 | 716 | Op::GetWorkspace, | |
| 713 | 717 | Op::CreateWorkspace, | |
| ⋯ | |||
| 994 | 998 | Op::DeployKeys(DeployKeysOp::GetDeployKey), | |
| 995 | 999 | Op::DeployKeys(DeployKeysOp::CreateDeployKey), | |
| 996 | 1000 | Op::DeployKeys(DeployKeysOp::DeleteDeployKey), | |
| 1001 | + | Op::Customize(CustomizeOp::ListMarketplaces), | |
| 1002 | + | Op::Customize(CustomizeOp::AddMarketplace), | |
| 1003 | + | Op::Customize(CustomizeOp::RemoveMarketplace), | |
| 1004 | + | Op::Customize(CustomizeOp::RefreshMarketplace), | |
| 1005 | + | Op::Customize(CustomizeOp::Discover), | |
| 1006 | + | Op::Customize(CustomizeOp::ListPlugins), | |
| 1007 | + | Op::Customize(CustomizeOp::GetPlugin), | |
| 1008 | + | Op::Customize(CustomizeOp::InstallPlugin), | |
| 1009 | + | Op::Customize(CustomizeOp::SetPluginEnabled), | |
| 1010 | + | Op::Customize(CustomizeOp::UninstallPlugin), | |
| 1011 | + | Op::Customize(CustomizeOp::UpdatePlugin), | |
| 1012 | + | Op::Customize(CustomizeOp::ListPluginAgents), | |
| 1013 | + | Op::Customize(CustomizeOp::ListConnectors), | |
| 1014 | + | Op::Customize(CustomizeOp::GetConnector), | |
| 1015 | + | Op::Customize(CustomizeOp::AddConnector), | |
| 1016 | + | Op::Customize(CustomizeOp::SetConnectorKey), | |
| 1017 | + | Op::Customize(CustomizeOp::RemoveConnector), | |
| 1018 | + | Op::Customize(CustomizeOp::RefreshConnector), | |
| 1019 | + | Op::Customize(CustomizeOp::GrantConnector), | |
| 1020 | + | Op::Customize(CustomizeOp::RevokeConnector), | |
| 1021 | + | Op::Customize(CustomizeOp::Bundle), | |
| 997 | 1022 | Op::Mirrors(MirrorsOp::GetMirror), | |
| 998 | 1023 | Op::Mirrors(MirrorsOp::GetHandBackPlan), | |
| 999 | 1024 | Op::Mirrors(MirrorsOp::TakeOver), | |
| ⋯ | |||
| 1276 | 1301 | Op::Mirrors(op) => op.name(), | |
| 1277 | 1302 | Op::Packages(op) => op.name(), | |
| 1278 | 1303 | Op::Folios(op) => op.name(), | |
| 1304 | + | Op::Customize(op) => op.name(), | |
| 1279 | 1305 | } | |
| 1280 | 1306 | } | |
| 1281 | 1307 | ||
| ⋯ | |||
| 1848 | 1874 | Op::Mirrors(op) => op.description(), | |
| 1849 | 1875 | Op::Packages(op) => op.description(), | |
| 1850 | 1876 | Op::Folios(op) => op.description(), | |
| 1877 | + | Op::Customize(op) => op.description(), | |
| 1851 | 1878 | } | |
| 1852 | 1879 | } | |
| 1853 | 1880 | ||
| ⋯ | |||
| 3341 | 3368 | Op::Mirrors(op) => op.input(), | |
| 3342 | 3369 | Op::Packages(op) => op.input(), | |
| 3343 | 3370 | Op::Folios(op) => op.input(), | |
| 3371 | + | Op::Customize(op) => op.input(), | |
| 3344 | 3372 | } | |
| 3345 | 3373 | } | |
| 3346 | 3374 | ||
| ⋯ | |||
| 3419 | 3447 | if let Op::Folios(_) = self { | |
| 3420 | 3448 | return false; | |
| 3421 | 3449 | } | |
| 3450 | + | // So does its Customize. | |
| 3451 | + | if let Op::Customize(_) = self { | |
| 3452 | + | return false; | |
| 3453 | + | } | |
| 3422 | 3454 | if let Op::About(op) = self { | |
| 3423 | 3455 | return op.needs_repo(); | |
| 3424 | 3456 | } | |
| ⋯ | |||
| 4693 | 4725 | } | |
| 4694 | 4726 | } | |
| 4695 | 4727 | let status = current.as_ref().map(|pull| pull.status); | |
| 4696 | − | let mut answer = current.map(|pull| serde_json::to_value(pull)).transpose()?; | |
| 4728 | + | let mut answer = current.map(serde_json::to_value).transpose()?; | |
| 4697 | 4729 | if wanted.as_deref() == Some("open") && status == Some(PullStatus::Closed) { | |
| 4698 | 4730 | match pass(work, "reopen_pull", &pull_action()).await? { | |
| 4699 | 4731 | Outcome::Ok(pull) => answer = Some(pull), | |
| ⋯ | |||
| 5696 | 5728 | Op::Mirrors(op) => crate::mirrors::run(op, services, viewer, input).await, | |
| 5697 | 5729 | Op::Packages(op) => crate::packages::run(op, services, viewer, input).await, | |
| 5698 | 5730 | Op::Folios(op) => crate::folios::run(op, services, viewer, input).await, | |
| 5731 | + | Op::Customize(op) => crate::customize::run(op, services, viewer, input).await, | |
| 5699 | 5732 | Op::ReopenSecurityAlert => { | |
| 5700 | 5733 | let changed: Outcome<AlertChange> = call( | |
| 5701 | 5734 | &services.security, | |
| 4350 | 4350 | }, | |
| 4351 | 4351 | "notes": "Credentials that do not work still answer `200`, with `ok` false and `message` saying what went wrong." | |
| 4352 | 4352 | }, | |
| 4353 | + | "list_marketplaces": { | |
| 4354 | + | "params": { | |
| 4355 | + | "workspace": "flagon-io" | |
| 4356 | + | }, | |
| 4357 | + | "response": [ | |
| 4358 | + | { | |
| 4359 | + | "name": "claude-plugins-official", | |
| 4360 | + | "source": { | |
| 4361 | + | "source": "github", | |
| 4362 | + | "repo": "anthropics/claude-plugins-official" | |
| 4363 | + | }, | |
| 4364 | + | "builtin": true, | |
| 4365 | + | "about": "Official plugins from Anthropic.", | |
| 4366 | + | "owner": "Anthropic", | |
| 4367 | + | "plugins": 42, | |
| 4368 | + | "fetched_at": "2026-10-11T14:02:11.408Z", | |
| 4369 | + | "problem": null, | |
| 4370 | + | "added_by": null, | |
| 4371 | + | "added_at": null, | |
| 4372 | + | "can_remove": false | |
| 4373 | + | }, | |
| 4374 | + | { | |
| 4375 | + | "name": "flagon-tools", | |
| 4376 | + | "source": { | |
| 4377 | + | "source": "g1t", | |
| 4378 | + | "repo": "flagon-io/plugins" | |
| 4379 | + | }, | |
| 4380 | + | "builtin": false, | |
| 4381 | + | "about": "Flagon's own skills and connectors.", | |
| 4382 | + | "owner": "Flagon", | |
| 4383 | + | "plugins": 3, | |
| 4384 | + | "fetched_at": "2026-10-11T14:02:12.117Z", | |
| 4385 | + | "problem": null, | |
| 4386 | + | "added_by": "syntaqx", | |
| 4387 | + | "added_at": "2026-10-09T18:20:03.512Z", | |
| 4388 | + | "can_remove": true | |
| 4389 | + | } | |
| 4390 | + | ], | |
| 4391 | + | "notes": "Every workspace reads `claude-plugins-official`, `anthropic-agent-skills` and `g1t`, which are `builtin` and cannot be removed. `source` is the marketplace's source object in the open plugin format: `github` (`repo`), `g1t` (`repo`, a repository here), `git` or `url` (`url`), or `builtin`. `problem` says why the manifest could not be read last time, if it could not. See [Customize](/guides/customize/)." | |
| 4392 | + | }, | |
| 4393 | + | "add_marketplace": { | |
| 4394 | + | "params": { | |
| 4395 | + | "workspace": "flagon-io" | |
| 4396 | + | }, | |
| 4397 | + | "request": { | |
| 4398 | + | "address": "flagon-io/plugins" | |
| 4399 | + | }, | |
| 4400 | + | "response": { | |
| 4401 | + | "name": "flagon-tools", | |
| 4402 | + | "source": { | |
| 4403 | + | "source": "g1t", | |
| 4404 | + | "repo": "flagon-io/plugins" | |
| 4405 | + | }, | |
| 4406 | + | "builtin": false, | |
| 4407 | + | "about": "Flagon's own skills and connectors.", | |
| 4408 | + | "owner": "Flagon", | |
| 4409 | + | "plugins": 3, | |
| 4410 | + | "fetched_at": "2026-10-11T14:05:40.019Z", | |
| 4411 | + | "problem": null, | |
| 4412 | + | "added_by": "syntaqx", | |
| 4413 | + | "added_at": "2026-10-11T14:05:40.019Z", | |
| 4414 | + | "can_remove": true | |
| 4415 | + | }, | |
| 4416 | + | "notes": "`owner/repo` that is a repository on this g1t is read here; otherwise it is read from GitHub. The manifest is read before the marketplace is kept: an address with no `.claude-plugin/marketplace.json`, or one that cannot be read, is refused with `422`. At most 20 marketplaces a workspace." | |
| 4417 | + | }, | |
| 4418 | + | "remove_marketplace": { | |
| 4419 | + | "params": { | |
| 4420 | + | "workspace": "flagon-io", | |
| 4421 | + | "marketplace": "flagon-tools" | |
| 4422 | + | }, | |
| 4423 | + | "notes": "Plugins installed from it stay installed at their pinned commit. A built-in marketplace answers `409`." | |
| 4424 | + | }, | |
| 4425 | + | "refresh_marketplace": { | |
| 4426 | + | "params": { | |
| 4427 | + | "workspace": "flagon-io", | |
| 4428 | + | "marketplace": "flagon-tools" | |
| 4429 | + | }, | |
| 4430 | + | "response": { | |
| 4431 | + | "name": "flagon-tools", | |
| 4432 | + | "source": { | |
| 4433 | + | "source": "g1t", | |
| 4434 | + | "repo": "flagon-io/plugins" | |
| 4435 | + | }, | |
| 4436 | + | "builtin": false, | |
| 4437 | + | "about": "Flagon's own skills and connectors.", | |
| 4438 | + | "owner": "Flagon", | |
| 4439 | + | "plugins": 4, | |
| 4440 | + | "fetched_at": "2026-10-11T15:12:08.330Z", | |
| 4441 | + | "problem": null, | |
| 4442 | + | "added_by": "syntaqx", | |
| 4443 | + | "added_at": "2026-10-09T18:20:03.512Z", | |
| 4444 | + | "can_remove": true | |
| 4445 | + | } | |
| 4446 | + | }, | |
| 4447 | + | "discover": { | |
| 4448 | + | "params": { | |
| 4449 | + | "workspace": "flagon-io" | |
| 4450 | + | }, | |
| 4451 | + | "response": { | |
| 4452 | + | "plugins": [ | |
| 4453 | + | { | |
| 4454 | + | "ref": "commit-commands@claude-plugins-official", | |
| 4455 | + | "name": "commit-commands", | |
| 4456 | + | "marketplace": "claude-plugins-official", | |
| 4457 | + | "display_name": "Commit commands", | |
| 4458 | + | "description": "Commands for committing, pushing and opening pull requests.", | |
| 4459 | + | "version": "1.0.0", | |
| 4460 | + | "author": "Anthropic", | |
| 4461 | + | "homepage": null, | |
| 4462 | + | "category": "productivity", | |
| 4463 | + | "tags": [ | |
| 4464 | + | "git" | |
| 4465 | + | ], | |
| 4466 | + | "keywords": [], | |
| 4467 | + | "fetch": { | |
| 4468 | + | "kind": "github", | |
| 4469 | + | "repo": "anthropics/claude-plugins-official", | |
| 4470 | + | "ref": null, | |
| 4471 | + | "sha": null, | |
| 4472 | + | "path": "plugins/commit-commands" | |
| 4473 | + | }, | |
| 4474 | + | "availability": { | |
| 4475 | + | "kind": "installed", | |
| 4476 | + | "enabled": true | |
| 4477 | + | }, | |
| 4478 | + | "skills": [] | |
| 4479 | + | }, | |
| 4480 | + | { | |
| 4481 | + | "ref": "release-notes@flagon-tools", | |
| 4482 | + | "name": "release-notes", | |
| 4483 | + | "marketplace": "flagon-tools", | |
| 4484 | + | "display_name": "Release notes", | |
| 4485 | + | "description": "Writes release notes from merged pull requests.", | |
| 4486 | + | "version": "0.3.1", | |
| 4487 | + | "author": "Flagon", | |
| 4488 | + | "homepage": "https://g1t.sh/flagon-io/plugins", | |
| 4489 | + | "category": "docs", | |
| 4490 | + | "tags": [], | |
| 4491 | + | "keywords": [ | |
| 4492 | + | "releases" | |
| 4493 | + | ], | |
| 4494 | + | "fetch": { | |
| 4495 | + | "kind": "g1t", | |
| 4496 | + | "repo": "flagon-io/plugins", | |
| 4497 | + | "ref": null, | |
| 4498 | + | "path": "plugins/release-notes" | |
| 4499 | + | }, | |
| 4500 | + | "availability": { | |
| 4501 | + | "kind": "available" | |
| 4502 | + | }, | |
| 4503 | + | "skills": [ | |
| 4504 | + | "release-notes" | |
| 4505 | + | ] | |
| 4506 | + | } | |
| 4507 | + | ], | |
| 4508 | + | "skills": [ | |
| 4509 | + | { | |
| 4510 | + | "ref": "release-notes:release-notes", | |
| 4511 | + | "name": "release-notes", | |
| 4512 | + | "plugin": "release-notes", | |
| 4513 | + | "marketplace": "flagon-tools", | |
| 4514 | + | "description": "Writes release notes from merged pull requests.", | |
| 4515 | + | "category": "docs", | |
| 4516 | + | "author": "Flagon", | |
| 4517 | + | "added": false, | |
| 4518 | + | "availability": { | |
| 4519 | + | "kind": "available" | |
| 4520 | + | } | |
| 4521 | + | } | |
| 4522 | + | ], | |
| 4523 | + | "marketplaces": [ | |
| 4524 | + | { | |
| 4525 | + | "name": "claude-plugins-official", | |
| 4526 | + | "source": { | |
| 4527 | + | "source": "github", | |
| 4528 | + | "repo": "anthropics/claude-plugins-official" | |
| 4529 | + | }, | |
| 4530 | + | "builtin": true, | |
| 4531 | + | "about": "Official plugins from Anthropic.", | |
| 4532 | + | "owner": "Anthropic", | |
| 4533 | + | "plugins": 42, | |
| 4534 | + | "fetched_at": "2026-10-11T14:02:11.408Z", | |
| 4535 | + | "problem": null, | |
| 4536 | + | "added_by": null, | |
| 4537 | + | "added_at": null, | |
| 4538 | + | "can_remove": false | |
| 4539 | + | } | |
| 4540 | + | ] | |
| 4541 | + | }, | |
| 4542 | + | "notes": "`availability.kind` is `available`, `installed` (with `enabled`), `unavailable` (with `why`: an npm or command source, or a git host g1t does not read; such a plugin still works in Claude Code) or `soon` (g1t's own, announced but not released). `fetch` is where g1t reads the plugin from: `github` (at `sha` when the listing pins one, else `ref`), `g1t`, `archive` (a zip at `url`) or `unsupported`. `skills` on a plugin lists the skills its listing names outright, so a skills-only plugin shows them before it is fetched." | |
| 4543 | + | }, | |
| 4544 | + | "list_plugins": { | |
| 4545 | + | "params": { | |
| 4546 | + | "workspace": "flagon-io" | |
| 4547 | + | }, | |
| 4548 | + | "response": [ | |
| 4549 | + | { | |
| 4550 | + | "id": "plg_01kq9f2h4k6m8p0r2t4v6x8z0b", | |
| 4551 | + | "ref": "commit-commands@claude-plugins-official", | |
| 4552 | + | "name": "commit-commands", | |
| 4553 | + | "marketplace": "claude-plugins-official", | |
| 4554 | + | "display_name": "Commit commands", | |
| 4555 | + | "description": "Commands for committing, pushing and opening pull requests.", | |
| 4556 | + | "version": "1.0.0", | |
| 4557 | + | "sha": "9c1f4a6e8b0d2c4f6a8e1b3d5f7a9c1e3b5d7f90", | |
| 4558 | + | "fetch": { | |
| 4559 | + | "kind": "github", | |
| 4560 | + | "repo": "anthropics/claude-plugins-official", | |
| 4561 | + | "ref": null, | |
| 4562 | + | "sha": "9c1f4a6e8b0d2c4f6a8e1b3d5f7a9c1e3b5d7f90", | |
| 4563 | + | "path": "plugins/commit-commands" | |
| 4564 | + | }, | |
| 4565 | + | "components": { | |
| 4566 | + | "skills": [], | |
| 4567 | + | "agents": [], | |
| 4568 | + | "commands": [ | |
| 4569 | + | { | |
| 4570 | + | "name": "commit", | |
| 4571 | + | "path": "commands/commit.md" | |
| 4572 | + | }, | |
| 4573 | + | { | |
| 4574 | + | "name": "commit-push-pr", | |
| 4575 | + | "path": "commands/commit-push-pr.md" | |
| 4576 | + | } | |
| 4577 | + | ], | |
| 4578 | + | "mcp_servers": [], | |
| 4579 | + | "hooks": false, | |
| 4580 | + | "lsp": false, | |
| 4581 | + | "bin": false, | |
| 4582 | + | "output_styles": false | |
| 4583 | + | }, | |
| 4584 | + | "problems": [], | |
| 4585 | + | "enabled": true, | |
| 4586 | + | "installed_by": "syntaqx", | |
| 4587 | + | "installed_at": "2026-10-10T09:14:52.207Z", | |
| 4588 | + | "update": { | |
| 4589 | + | "version": "1.1.0", | |
| 4590 | + | "sha": "e3b5d7f90a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b" | |
| 4591 | + | } | |
| 4592 | + | } | |
| 4593 | + | ], | |
| 4594 | + | "notes": "`sha` is the commit the plugin was read at, which it stays pinned to; `update` is set when the marketplace now lists a newer version. `components` is what installing extracted: `skills` and `commands` joined the library as `plugin:name`, `agents` are templates to hire from, `mcp_servers` with `remote` true became connectors (stdio ones are for Claude Code only), and `hooks`, `lsp`, `bin` and `output_styles` say what only Claude Code on a runner uses." | |
| 4595 | + | }, | |
| 4596 | + | "get_plugin": { | |
| 4597 | + | "params": { | |
| 4598 | + | "workspace": "flagon-io", | |
| 4599 | + | "ref": "release-notes%40flagon-tools" | |
| 4600 | + | }, | |
| 4601 | + | "response": { | |
| 4602 | + | "listing": { | |
| 4603 | + | "ref": "release-notes@flagon-tools", | |
| 4604 | + | "name": "release-notes", | |
| 4605 | + | "marketplace": "flagon-tools", | |
| 4606 | + | "display_name": "Release notes", | |
| 4607 | + | "description": "Writes release notes from merged pull requests.", | |
| 4608 | + | "version": "0.3.1", | |
| 4609 | + | "author": "Flagon", | |
| 4610 | + | "homepage": "https://g1t.sh/flagon-io/plugins", | |
| 4611 | + | "category": "docs", | |
| 4612 | + | "tags": [], | |
| 4613 | + | "keywords": [ | |
| 4614 | + | "releases" | |
| 4615 | + | ], | |
| 4616 | + | "fetch": { | |
| 4617 | + | "kind": "g1t", | |
| 4618 | + | "repo": "flagon-io/plugins", | |
| 4619 | + | "ref": null, | |
| 4620 | + | "path": "plugins/release-notes" | |
| 4621 | + | }, | |
| 4622 | + | "availability": { | |
| 4623 | + | "kind": "available" | |
| 4624 | + | }, | |
| 4625 | + | "skills": [ | |
| 4626 | + | "release-notes" | |
| 4627 | + | ] | |
| 4628 | + | }, | |
| 4629 | + | "install": null, | |
| 4630 | + | "components": { | |
| 4631 | + | "skills": [ | |
| 4632 | + | { | |
| 4633 | + | "name": "release-notes", | |
| 4634 | + | "description": "Write release notes from the pull requests merged since the last tag.", | |
| 4635 | + | "path": "skills/release-notes/SKILL.md" | |
| 4636 | + | } | |
| 4637 | + | ], | |
| 4638 | + | "agents": [ | |
| 4639 | + | { | |
| 4640 | + | "name": "release-writer", | |
| 4641 | + | "description": "Drafts a release's notes and asks a maintainer to check them.", | |
| 4642 | + | "path": "agents/release-writer.md" | |
| 4643 | + | } | |
| 4644 | + | ], | |
| 4645 | + | "commands": [], | |
| 4646 | + | "mcp_servers": [ | |
| 4647 | + | { | |
| 4648 | + | "name": "linear", | |
| 4649 | + | "type": "http", | |
| 4650 | + | "url": "https://mcp.linear.app/mcp", | |
| 4651 | + | "remote": true | |
| 4652 | + | } | |
| 4653 | + | ], | |
| 4654 | + | "hooks": false, | |
| 4655 | + | "lsp": false, | |
| 4656 | + | "bin": false, | |
| 4657 | + | "output_styles": false | |
| 4658 | + | }, | |
| 4659 | + | "readme": "# Release notes\n\nWrites release notes from merged pull requests…", | |
| 4660 | + | "manifest": { | |
| 4661 | + | "name": "release-notes", | |
| 4662 | + | "version": "0.3.1", | |
| 4663 | + | "license": "MIT", | |
| 4664 | + | "repository": "https://g1t.sh/flagon-io/plugins" | |
| 4665 | + | }, | |
| 4666 | + | "agents": [ | |
| 4667 | + | { | |
| 4668 | + | "name": "release-writer", | |
| 4669 | + | "description": "Drafts a release's notes and asks a maintainer to check them.", | |
| 4670 | + | "tools": [ | |
| 4671 | + | "Read", | |
| 4672 | + | "Grep" | |
| 4673 | + | ], | |
| 4674 | + | "model": null, | |
| 4675 | + | "skills": [ | |
| 4676 | + | "release-notes" | |
| 4677 | + | ], | |
| 4678 | + | "prompt": "You write release notes…" | |
| 4679 | + | } | |
| 4680 | + | ], | |
| 4681 | + | "problems": [], | |
| 4682 | + | "source": { | |
| 4683 | + | "marketplace": "flagon-tools", | |
| 4684 | + | "owner": "Flagon" | |
| 4685 | + | } | |
| 4686 | + | }, | |
| 4687 | + | "notes": "In the path, `ref` is URL-encoded: `release-notes%40flagon-tools`. `install` is null until the workspace installs it. `components`, `readme`, `manifest` and `agents` are read from the plugin's source at the commit the listing names, so this takes a moment; for an `unavailable` plugin they are null or empty and `problems` says why." | |
| 4688 | + | }, | |
| 4689 | + | "install_plugin": { | |
| 4690 | + | "params": { | |
| 4691 | + | "workspace": "flagon-io" | |
| 4692 | + | }, | |
| 4693 | + | "request": { | |
| 4694 | + | "ref": "release-notes@flagon-tools" | |
| 4695 | + | }, | |
| 4696 | + | "response": { | |
| 4697 | + | "id": "plg_01kq9g3j5l7n9q1s3u5w7y9a1c", | |
| 4698 | + | "ref": "release-notes@flagon-tools", | |
| 4699 | + | "name": "release-notes", | |
| 4700 | + | "marketplace": "flagon-tools", | |
| 4701 | + | "display_name": "Release notes", | |
| 4702 | + | "description": "Writes release notes from merged pull requests.", | |
| 4703 | + | "version": "0.3.1", | |
| 4704 | + | "sha": "4f6a8e1b3d5f7a9c1e3b5d7f90a1c3e5b7d9f1a3", | |
| 4705 | + | "fetch": { | |
| 4706 | + | "kind": "g1t", | |
| 4707 | + | "repo": "flagon-io/plugins", | |
| 4708 | + | "ref": null, | |
| 4709 | + | "path": "plugins/release-notes" | |
| 4710 | + | }, | |
| 4711 | + | "components": { | |
| 4712 | + | "skills": [ | |
| 4713 | + | { | |
| 4714 | + | "name": "release-notes", | |
| 4715 | + | "description": "Write release notes from the pull requests merged since the last tag.", | |
| 4716 | + | "path": "skills/release-notes/SKILL.md" | |
| 4717 | + | } | |
| 4718 | + | ], | |
| 4719 | + | "agents": [ | |
| 4720 | + | { | |
| 4721 | + | "name": "release-writer", | |
| 4722 | + | "description": "Drafts a release's notes and asks a maintainer to check them.", | |
| 4723 | + | "path": "agents/release-writer.md" | |
| 4724 | + | } | |
| 4725 | + | ], | |
| 4726 | + | "commands": [], | |
| 4727 | + | "mcp_servers": [ | |
| 4728 | + | { | |
| 4729 | + | "name": "linear", | |
| 4730 | + | "type": "http", | |
| 4731 | + | "url": "https://mcp.linear.app/mcp", | |
| 4732 | + | "remote": true | |
| 4733 | + | } | |
| 4734 | + | ], | |
| 4735 | + | "hooks": false, | |
| 4736 | + | "lsp": false, | |
| 4737 | + | "bin": false, | |
| 4738 | + | "output_styles": false | |
| 4739 | + | }, | |
| 4740 | + | "problems": [], | |
| 4741 | + | "enabled": true, | |
| 4742 | + | "installed_by": "syntaqx", | |
| 4743 | + | "installed_at": "2026-10-11T14:20:31.904Z", | |
| 4744 | + | "update": null | |
| 4745 | + | }, | |
| 4746 | + | "notes": "Its skill is now `release-notes:release-notes` in the library, `release-writer` is a template under Agents, and the `linear` server is a connector waiting for an owner to sign in (`list_connectors` shows it as `needs_sign_in`, with `from.kind` `plugin`). A plugin already installed answers `409`; one whose source g1t cannot fetch answers `422`." | |
| 4747 | + | }, | |
| 4748 | + | "set_plugin_enabled": { | |
| 4749 | + | "params": { | |
| 4750 | + | "workspace": "flagon-io", | |
| 4751 | + | "ref": "release-notes%40flagon-tools" | |
| 4752 | + | }, | |
| 4753 | + | "request": { | |
| 4754 | + | "enabled": false | |
| 4755 | + | }, | |
| 4756 | + | "response": { | |
| 4757 | + | "id": "plg_01kq9g3j5l7n9q1s3u5w7y9a1c", | |
| 4758 | + | "ref": "release-notes@flagon-tools", | |
| 4759 | + | "name": "release-notes", | |
| 4760 | + | "marketplace": "flagon-tools", | |
| 4761 | + | "display_name": "Release notes", | |
| 4762 | + | "description": "Writes release notes from merged pull requests.", | |
| 4763 | + | "version": "0.3.1", | |
| 4764 | + | "sha": "4f6a8e1b3d5f7a9c1e3b5d7f90a1c3e5b7d9f1a3", | |
| 4765 | + | "fetch": { | |
| 4766 | + | "kind": "g1t", | |
| 4767 | + | "repo": "flagon-io/plugins", | |
| 4768 | + | "ref": null, | |
| 4769 | + | "path": "plugins/release-notes" | |
| 4770 | + | }, | |
| 4771 | + | "components": { | |
| 4772 | + | "skills": [ | |
| 4773 | + | { | |
| 4774 | + | "name": "release-notes", | |
| 4775 | + | "description": "Write release notes from the pull requests merged since the last tag.", | |
| 4776 | + | "path": "skills/release-notes/SKILL.md" | |
| 4777 | + | } | |
| 4778 | + | ], | |
| 4779 | + | "agents": [], | |
| 4780 | + | "commands": [], | |
| 4781 | + | "mcp_servers": [], | |
| 4782 | + | "hooks": false, | |
| 4783 | + | "lsp": false, | |
| 4784 | + | "bin": false, | |
| 4785 | + | "output_styles": false | |
| 4786 | + | }, | |
| 4787 | + | "problems": [], | |
| 4788 | + | "enabled": false, | |
| 4789 | + | "installed_by": "syntaqx", | |
| 4790 | + | "installed_at": "2026-10-11T14:20:31.904Z", | |
| 4791 | + | "update": null | |
| 4792 | + | }, | |
| 4793 | + | "notes": "A switched-off plugin stays installed and keeps its pinned commit, but agents lose its skills, templates and connectors until it is switched on again, and runners' bundles leave it out." | |
| 4794 | + | }, | |
| 4795 | + | "uninstall_plugin": { | |
| 4796 | + | "params": { | |
| 4797 | + | "workspace": "flagon-io", | |
| 4798 | + | "ref": "release-notes%40flagon-tools" | |
| 4799 | + | }, | |
| 4800 | + | "notes": "Its skills leave the library, its templates go, and the connectors it brought are removed with their secrets. Agents hired from its templates stay as they are." | |
| 4801 | + | }, | |
| 4802 | + | "update_plugin": { | |
| 4803 | + | "params": { | |
| 4804 | + | "workspace": "flagon-io", | |
| 4805 | + | "ref": "commit-commands%40claude-plugins-official" | |
| 4806 | + | }, | |
| 4807 | + | "response": { | |
| 4808 | + | "id": "plg_01kq9f2h4k6m8p0r2t4v6x8z0b", | |
| 4809 | + | "ref": "commit-commands@claude-plugins-official", | |
| 4810 | + | "name": "commit-commands", | |
| 4811 | + | "marketplace": "claude-plugins-official", | |
| 4812 | + | "display_name": "Commit commands", | |
| 4813 | + | "description": "Commands for committing, pushing and opening pull requests.", | |
| 4814 | + | "version": "1.1.0", | |
| 4815 | + | "sha": "e3b5d7f90a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b", | |
| 4816 | + | "fetch": { | |
| 4817 | + | "kind": "github", | |
| 4818 | + | "repo": "anthropics/claude-plugins-official", | |
| 4819 | + | "ref": null, | |
| 4820 | + | "sha": "e3b5d7f90a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b", | |
| 4821 | + | "path": "plugins/commit-commands" | |
| 4822 | + | }, | |
| 4823 | + | "components": { | |
| 4824 | + | "skills": [], | |
| 4825 | + | "agents": [], | |
| 4826 | + | "commands": [ | |
| 4827 | + | { | |
| 4828 | + | "name": "commit", | |
| 4829 | + | "path": "commands/commit.md" | |
| 4830 | + | }, | |
| 4831 | + | { | |
| 4832 | + | "name": "commit-push-pr", | |
| 4833 | + | "path": "commands/commit-push-pr.md" | |
| 4834 | + | }, | |
| 4835 | + | { | |
| 4836 | + | "name": "clean-gone", | |
| 4837 | + | "path": "commands/clean-gone.md" | |
| 4838 | + | } | |
| 4839 | + | ], | |
| 4840 | + | "mcp_servers": [], | |
| 4841 | + | "hooks": false, | |
| 4842 | + | "lsp": false, | |
| 4843 | + | "bin": false, | |
| 4844 | + | "output_styles": false | |
| 4845 | + | }, | |
| 4846 | + | "problems": [], | |
| 4847 | + | "enabled": true, | |
| 4848 | + | "installed_by": "syntaqx", | |
| 4849 | + | "installed_at": "2026-10-10T09:14:52.207Z", | |
| 4850 | + | "update": null | |
| 4851 | + | }, | |
| 4852 | + | "notes": "The plugin is read again at the commit its marketplace lists now and pinned there: its skills get new versions (agents and teams that had them keep them), skills it no longer has are archived, its templates and `components` are rewritten, and a server it already brought keeps its connector. A plugin already at that commit comes back as it was. Runners fetch the new commit from the next run on." | |
| 4853 | + | }, | |
| 4854 | + | "list_plugin_agents": { | |
| 4855 | + | "params": { | |
| 4856 | + | "workspace": "flagon-io" | |
| 4857 | + | }, | |
| 4858 | + | "response": [ | |
| 4859 | + | { | |
| 4860 | + | "ref": "release-notes:release-writer", | |
| 4861 | + | "plugin": "release-notes", | |
| 4862 | + | "marketplace": "flagon-tools", | |
| 4863 | + | "name": "release-writer", | |
| 4864 | + | "description": "Drafts a release's notes and asks a maintainer to check them.", | |
| 4865 | + | "prompt": "You write release notes…", | |
| 4866 | + | "model": null, | |
| 4867 | + | "skills": [ | |
| 4868 | + | "release-notes" | |
| 4869 | + | ], | |
| 4870 | + | "tools": [ | |
| 4871 | + | "Read", | |
| 4872 | + | "Grep" | |
| 4873 | + | ] | |
| 4874 | + | } | |
| 4875 | + | ], | |
| 4876 | + | "notes": "Each is a subagent file (`agents/<name>.md`) from an installed, switched-on plugin: its front matter as fields and its body as `prompt`. They are listed under Agents → Templates, From plugins, to hire an agent from." | |
| 4877 | + | }, | |
| 4878 | + | "list_connectors": { | |
| 4879 | + | "params": { | |
| 4880 | + | "workspace": "flagon-io" | |
| 4881 | + | }, | |
| 4882 | + | "response": [ | |
| 4883 | + | { | |
| 4884 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 4885 | + | "name": "linear", | |
| 4886 | + | "title": "Linear", | |
| 4887 | + | "url": "https://mcp.linear.app/mcp", | |
| 4888 | + | "category": "work", | |
| 4889 | + | "about": "Issues, projects and cycles in Linear.", | |
| 4890 | + | "auth": "oauth", | |
| 4891 | + | "state": "connected", | |
| 4892 | + | "problem": null, | |
| 4893 | + | "from": { | |
| 4894 | + | "kind": "directory", | |
| 4895 | + | "id": "linear" | |
| 4896 | + | }, | |
| 4897 | + | "tools": [ | |
| 4898 | + | { | |
| 4899 | + | "name": "list_issues", | |
| 4900 | + | "description": "List issues in a team or project.", | |
| 4901 | + | "input_schema": { | |
| 4902 | + | "type": "object", | |
| 4903 | + | "properties": { | |
| 4904 | + | "team": { | |
| 4905 | + | "type": "string" | |
| 4906 | + | } | |
| 4907 | + | } | |
| 4908 | + | } | |
| 4909 | + | } | |
| 4910 | + | ], | |
| 4911 | + | "checked_at": "2026-10-11T14:31:05.772Z", | |
| 4912 | + | "header_name": null, | |
| 4913 | + | "signed_in_as": "chase@flagon.io", | |
| 4914 | + | "signed_in_at": "2026-10-11T14:31:05.772Z", | |
| 4915 | + | "grants": [ | |
| 4916 | + | { | |
| 4917 | + | "id": "cgr_01kq9h5l7n9q1s3u5w7y9a1c3e", | |
| 4918 | + | "scope": "workspace", | |
| 4919 | + | "target": null, | |
| 4920 | + | "label": "Every agent", | |
| 4921 | + | "granted_by": "syntaqx", | |
| 4922 | + | "granted_at": "2026-10-11T14:32:10.004Z" | |
| 4923 | + | } | |
| 4924 | + | ], | |
| 4925 | + | "added_by": "syntaqx", | |
| 4926 | + | "added_at": "2026-10-11T14:30:12.618Z" | |
| 4927 | + | }, | |
| 4928 | + | { | |
| 4929 | + | "id": "con_01kq9j6m8p0r2t4v6x8z0b2d4f", | |
| 4930 | + | "name": "docs-search", | |
| 4931 | + | "title": "docs-search", | |
| 4932 | + | "url": "https://mcp.flagon.dev/docs", | |
| 4933 | + | "category": null, | |
| 4934 | + | "about": null, | |
| 4935 | + | "auth": "header", | |
| 4936 | + | "state": "needs_key", | |
| 4937 | + | "problem": null, | |
| 4938 | + | "from": { | |
| 4939 | + | "kind": "address" | |
| 4940 | + | }, | |
| 4941 | + | "tools": [], | |
| 4942 | + | "checked_at": null, | |
| 4943 | + | "header_name": "X-Api-Key", | |
| 4944 | + | "signed_in_as": null, | |
| 4945 | + | "signed_in_at": null, | |
| 4946 | + | "grants": [], | |
| 4947 | + | "added_by": "syntaqx", | |
| 4948 | + | "added_at": "2026-10-11T14:40:55.130Z" | |
| 4949 | + | } | |
| 4950 | + | ], | |
| 4951 | + | "notes": "`state` is `connected`, `needs_sign_in` (OAuth, not signed in yet: an owner signs in on the site), `needs_key` (header auth without a value yet) or `problem` (with `problem` saying why the server could not be reached). `tools` is what the server listed last, at `checked_at`; agents are offered each as `<name>__<tool>`. Keys and tokens are never returned: `header_name` says only which header a key is sent as. g1t's own integrations (GitHub, Jira, Sentry, model providers…) are [`list_integrations`](/reference/api/integrations/list-integrations/)." | |
| 4952 | + | }, | |
| 4953 | + | "get_connector": { | |
| 4954 | + | "params": { | |
| 4955 | + | "workspace": "flagon-io", | |
| 4956 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d" | |
| 4957 | + | }, | |
| 4958 | + | "response": { | |
| 4959 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 4960 | + | "name": "linear", | |
| 4961 | + | "title": "Linear", | |
| 4962 | + | "url": "https://mcp.linear.app/mcp", | |
| 4963 | + | "category": "work", | |
| 4964 | + | "about": "Issues, projects and cycles in Linear.", | |
| 4965 | + | "auth": "oauth", | |
| 4966 | + | "state": "connected", | |
| 4967 | + | "problem": null, | |
| 4968 | + | "from": { | |
| 4969 | + | "kind": "directory", | |
| 4970 | + | "id": "linear" | |
| 4971 | + | }, | |
| 4972 | + | "tools": [ | |
| 4973 | + | { | |
| 4974 | + | "name": "list_issues", | |
| 4975 | + | "description": "List issues in a team or project.", | |
| 4976 | + | "input_schema": { | |
| 4977 | + | "type": "object", | |
| 4978 | + | "properties": { | |
| 4979 | + | "team": { | |
| 4980 | + | "type": "string" | |
| 4981 | + | } | |
| 4982 | + | } | |
| 4983 | + | } | |
| 4984 | + | } | |
| 4985 | + | ], | |
| 4986 | + | "checked_at": "2026-10-11T14:31:05.772Z", | |
| 4987 | + | "header_name": null, | |
| 4988 | + | "signed_in_as": "chase@flagon.io", | |
| 4989 | + | "signed_in_at": "2026-10-11T14:31:05.772Z", | |
| 4990 | + | "grants": [ | |
| 4991 | + | { | |
| 4992 | + | "id": "cgr_01kq9h5l7n9q1s3u5w7y9a1c3e", | |
| 4993 | + | "scope": "workspace", | |
| 4994 | + | "target": null, | |
| 4995 | + | "label": "Every agent", | |
| 4996 | + | "granted_by": "syntaqx", | |
| 4997 | + | "granted_at": "2026-10-11T14:32:10.004Z" | |
| 4998 | + | } | |
| 4999 | + | ], | |
| 5000 | + | "added_by": "syntaqx", | |
| 5001 | + | "added_at": "2026-10-11T14:30:12.618Z" | |
| 5002 | + | } | |
| 5003 | + | }, | |
| 5004 | + | "add_connector": { | |
| 5005 | + | "params": { | |
| 5006 | + | "workspace": "flagon-io" | |
| 5007 | + | }, | |
| 5008 | + | "request": { | |
| 5009 | + | "from": { | |
| 5010 | + | "kind": "address", | |
| 5011 | + | "name": "docs-search", | |
| 5012 | + | "url": "https://mcp.flagon.dev/docs" | |
| 5013 | + | }, | |
| 5014 | + | "auth": "header", | |
| 5015 | + | "header": { | |
| 5016 | + | "name": "X-Api-Key", | |
| 5017 | + | "value": "fd_live_…" | |
| 5018 | + | } | |
| 5019 | + | }, | |
| 5020 | + | "response": { | |
| 5021 | + | "id": "con_01kq9j6m8p0r2t4v6x8z0b2d4f", | |
| 5022 | + | "name": "docs-search", | |
| 5023 | + | "title": "docs-search", | |
| 5024 | + | "url": "https://mcp.flagon.dev/docs", | |
| 5025 | + | "category": null, | |
| 5026 | + | "about": null, | |
| 5027 | + | "auth": "header", | |
| 5028 | + | "state": "connected", | |
| 5029 | + | "problem": null, | |
| 5030 | + | "from": { | |
| 5031 | + | "kind": "address" | |
| 5032 | + | }, | |
| 5033 | + | "tools": [ | |
| 5034 | + | { | |
| 5035 | + | "name": "search_docs", | |
| 5036 | + | "description": "Search Flagon's documentation.", | |
| 5037 | + | "input_schema": { | |
| 5038 | + | "type": "object", | |
| 5039 | + | "properties": { | |
| 5040 | + | "query": { | |
| 5041 | + | "type": "string" | |
| 5042 | + | } | |
| 5043 | + | }, | |
| 5044 | + | "required": [ | |
| 5045 | + | "query" | |
| 5046 | + | ] | |
| 5047 | + | } | |
| 5048 | + | } | |
| 5049 | + | ], | |
| 5050 | + | "checked_at": "2026-10-11T14:40:56.402Z", | |
| 5051 | + | "header_name": "X-Api-Key", | |
| 5052 | + | "signed_in_as": null, | |
| 5053 | + | "signed_in_at": null, | |
| 5054 | + | "grants": [], | |
| 5055 | + | "added_by": "syntaqx", | |
| 5056 | + | "added_at": "2026-10-11T14:40:55.130Z" | |
| 5057 | + | }, | |
| 5058 | + | "notes": "`from` is `{\"kind\": \"directory\", \"id\"}` for a server in g1t's directory, `{\"kind\": \"plugin\", \"ref\", \"server\"}` for one an installed plugin names in its `.mcp.json`, or `{\"kind\": \"address\", \"name\", \"url\"}` for any MCP server over HTTP. With `auth` `oauth` the connector starts as `needs_sign_in` and an owner signs in on the site. The key's value is sealed under the workspace's `CONNECTORS_KEY` and never returned. A new connector reaches no agent until it is granted: see [`grant_connector`](/reference/api/customize/grant-connector/). At most 50 connectors a workspace." | |
| 5059 | + | }, | |
| 5060 | + | "set_connector_key": { | |
| 5061 | + | "params": { | |
| 5062 | + | "workspace": "flagon-io", | |
| 5063 | + | "id": "con_01kq9j6m8p0r2t4v6x8z0b2d4f" | |
| 5064 | + | }, | |
| 5065 | + | "request": { | |
| 5066 | + | "value": "fd_live_…" | |
| 5067 | + | }, | |
| 5068 | + | "response": { | |
| 5069 | + | "id": "con_01kq9j6m8p0r2t4v6x8z0b2d4f", | |
| 5070 | + | "name": "docs-search", | |
| 5071 | + | "title": "docs-search", | |
| 5072 | + | "url": "https://mcp.flagon.dev/docs", | |
| 5073 | + | "category": null, | |
| 5074 | + | "about": null, | |
| 5075 | + | "auth": "header", | |
| 5076 | + | "state": "connected", | |
| 5077 | + | "problem": null, | |
| 5078 | + | "from": { | |
| 5079 | + | "kind": "address" | |
| 5080 | + | }, | |
| 5081 | + | "tools": [ | |
| 5082 | + | { | |
| 5083 | + | "name": "search_docs", | |
| 5084 | + | "description": "Search Flagon's documentation.", | |
| 5085 | + | "input_schema": { | |
| 5086 | + | "type": "object", | |
| 5087 | + | "properties": { | |
| 5088 | + | "query": { | |
| 5089 | + | "type": "string" | |
| 5090 | + | } | |
| 5091 | + | }, | |
| 5092 | + | "required": [ | |
| 5093 | + | "query" | |
| 5094 | + | ] | |
| 5095 | + | } | |
| 5096 | + | } | |
| 5097 | + | ], | |
| 5098 | + | "checked_at": "2026-10-11T15:02:17.950Z", | |
| 5099 | + | "header_name": "X-Api-Key", | |
| 5100 | + | "signed_in_as": null, | |
| 5101 | + | "signed_in_at": null, | |
| 5102 | + | "grants": [], | |
| 5103 | + | "added_by": "syntaqx", | |
| 5104 | + | "added_at": "2026-10-11T14:40:55.130Z" | |
| 5105 | + | }, | |
| 5106 | + | "notes": "Rotates a key, or gives a `needs_key` connector its first one. `header_name` changes which header it is sent as; left out, the connector keeps its own, or uses `Authorization`. The tools are listed again with the new key, so `state` says at once whether it works." | |
| 5107 | + | }, | |
| 5108 | + | "remove_connector": { | |
| 5109 | + | "params": { | |
| 5110 | + | "workspace": "flagon-io", | |
| 5111 | + | "id": "con_01kq9j6m8p0r2t4v6x8z0b2d4f" | |
| 5112 | + | }, | |
| 5113 | + | "notes": "Its sealed secrets and its grants go with it. Agents that had it lose its tools at their next turn." | |
| 5114 | + | }, | |
| 5115 | + | "refresh_connector": { | |
| 5116 | + | "params": { | |
| 5117 | + | "workspace": "flagon-io", | |
| 5118 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d" | |
| 5119 | + | }, | |
| 5120 | + | "response": { | |
| 5121 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 5122 | + | "name": "linear", | |
| 5123 | + | "title": "Linear", | |
| 5124 | + | "url": "https://mcp.linear.app/mcp", | |
| 5125 | + | "category": "work", | |
| 5126 | + | "about": "Issues, projects and cycles in Linear.", | |
| 5127 | + | "auth": "oauth", | |
| 5128 | + | "state": "connected", | |
| 5129 | + | "problem": null, | |
| 5130 | + | "from": { | |
| 5131 | + | "kind": "directory", | |
| 5132 | + | "id": "linear" | |
| 5133 | + | }, | |
| 5134 | + | "tools": [ | |
| 5135 | + | { | |
| 5136 | + | "name": "list_issues", | |
| 5137 | + | "description": "List issues in a team or project.", | |
| 5138 | + | "input_schema": { | |
| 5139 | + | "type": "object", | |
| 5140 | + | "properties": { | |
| 5141 | + | "team": { | |
| 5142 | + | "type": "string" | |
| 5143 | + | } | |
| 5144 | + | } | |
| 5145 | + | } | |
| 5146 | + | }, | |
| 5147 | + | { | |
| 5148 | + | "name": "create_issue", | |
| 5149 | + | "description": "Create an issue.", | |
| 5150 | + | "input_schema": { | |
| 5151 | + | "type": "object", | |
| 5152 | + | "properties": { | |
| 5153 | + | "title": { | |
| 5154 | + | "type": "string" | |
| 5155 | + | } | |
| 5156 | + | }, | |
| 5157 | + | "required": [ | |
| 5158 | + | "title" | |
| 5159 | + | ] | |
| 5160 | + | } | |
| 5161 | + | } | |
| 5162 | + | ], | |
| 5163 | + | "checked_at": "2026-10-11T15:10:44.207Z", | |
| 5164 | + | "header_name": null, | |
| 5165 | + | "signed_in_as": "chase@flagon.io", | |
| 5166 | + | "signed_in_at": "2026-10-11T14:31:05.772Z", | |
| 5167 | + | "grants": [ | |
| 5168 | + | { | |
| 5169 | + | "id": "cgr_01kq9h5l7n9q1s3u5w7y9a1c3e", | |
| 5170 | + | "scope": "workspace", | |
| 5171 | + | "target": null, | |
| 5172 | + | "label": "Every agent", | |
| 5173 | + | "granted_by": "syntaqx", | |
| 5174 | + | "granted_at": "2026-10-11T14:32:10.004Z" | |
| 5175 | + | } | |
| 5176 | + | ], | |
| 5177 | + | "added_by": "syntaqx", | |
| 5178 | + | "added_at": "2026-10-11T14:30:12.618Z" | |
| 5179 | + | }, | |
| 5180 | + | "notes": "Asks the server for its tools now. A server that cannot be reached, or refuses the sign-in, leaves the connector in `state` `problem` with `problem` saying why; the tools it listed last are kept." | |
| 5181 | + | }, | |
| 5182 | + | "grant_connector": { | |
| 5183 | + | "params": { | |
| 5184 | + | "workspace": "flagon-io", | |
| 5185 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d" | |
| 5186 | + | }, | |
| 5187 | + | "request": { | |
| 5188 | + | "scope": "team", | |
| 5189 | + | "target": "platform" | |
| 5190 | + | }, | |
| 5191 | + | "response": { | |
| 5192 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 5193 | + | "name": "linear", | |
| 5194 | + | "title": "Linear", | |
| 5195 | + | "url": "https://mcp.linear.app/mcp", | |
| 5196 | + | "category": "work", | |
| 5197 | + | "about": "Issues, projects and cycles in Linear.", | |
| 5198 | + | "auth": "oauth", | |
| 5199 | + | "state": "connected", | |
| 5200 | + | "problem": null, | |
| 5201 | + | "from": { | |
| 5202 | + | "kind": "directory", | |
| 5203 | + | "id": "linear" | |
| 5204 | + | }, | |
| 5205 | + | "tools": [], | |
| 5206 | + | "checked_at": "2026-10-11T14:31:05.772Z", | |
| 5207 | + | "header_name": null, | |
| 5208 | + | "signed_in_as": "chase@flagon.io", | |
| 5209 | + | "signed_in_at": "2026-10-11T14:31:05.772Z", | |
| 5210 | + | "grants": [ | |
| 5211 | + | { | |
| 5212 | + | "id": "cgr_01kq9h5l7n9q1s3u5w7y9a1c3e", | |
| 5213 | + | "scope": "workspace", | |
| 5214 | + | "target": null, | |
| 5215 | + | "label": "Every agent", | |
| 5216 | + | "granted_by": "syntaqx", | |
| 5217 | + | "granted_at": "2026-10-11T14:32:10.004Z" | |
| 5218 | + | }, | |
| 5219 | + | { | |
| 5220 | + | "id": "cgr_01kq9k7n9q1s3u5w7y9a1c3e5g", | |
| 5221 | + | "scope": "team", | |
| 5222 | + | "target": "platform", | |
| 5223 | + | "label": "Platform", | |
| 5224 | + | "granted_by": "syntaqx", | |
| 5225 | + | "granted_at": "2026-10-11T15:20:02.561Z" | |
| 5226 | + | } | |
| 5227 | + | ], | |
| 5228 | + | "added_by": "syntaqx", | |
| 5229 | + | "added_at": "2026-10-11T14:30:12.618Z" | |
| 5230 | + | }, | |
| 5231 | + | "notes": "The same reach as a skill's: `workspace` gives it to every agent, `team` to a team's agents (`target` is the team's slug), `agent` to one agent (`target` is its handle, with or without `@`). A grant that is already there is returned as it is. Each agent's Abilities tab shows the connectors that reach it, with a level per tool." | |
| 5232 | + | }, | |
| 5233 | + | "revoke_connector": { | |
| 5234 | + | "params": { | |
| 5235 | + | "workspace": "flagon-io", | |
| 5236 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 5237 | + | "grant": "cgr_01kq9k7n9q1s3u5w7y9a1c3e5g" | |
| 5238 | + | }, | |
| 5239 | + | "response": { | |
| 5240 | + | "id": "con_01kq9h4k6m8p0r2t4v6x8z0b2d", | |
| 5241 | + | "name": "linear", | |
| 5242 | + | "title": "Linear", | |
| 5243 | + | "url": "https://mcp.linear.app/mcp", | |
| 5244 | + | "category": "work", | |
| 5245 | + | "about": "Issues, projects and cycles in Linear.", | |
| 5246 | + | "auth": "oauth", | |
| 5247 | + | "state": "connected", | |
| 5248 | + | "problem": null, | |
| 5249 | + | "from": { | |
| 5250 | + | "kind": "directory", | |
| 5251 | + | "id": "linear" | |
| 5252 | + | }, | |
| 5253 | + | "tools": [], | |
| 5254 | + | "checked_at": "2026-10-11T14:31:05.772Z", | |
| 5255 | + | "header_name": null, | |
| 5256 | + | "signed_in_as": "chase@flagon.io", | |
| 5257 | + | "signed_in_at": "2026-10-11T14:31:05.772Z", | |
| 5258 | + | "grants": [ | |
| 5259 | + | { | |
| 5260 | + | "id": "cgr_01kq9h5l7n9q1s3u5w7y9a1c3e", | |
| 5261 | + | "scope": "workspace", | |
| 5262 | + | "target": null, | |
| 5263 | + | "label": "Every agent", | |
| 5264 | + | "granted_by": "syntaqx", | |
| 5265 | + | "granted_at": "2026-10-11T14:32:10.004Z" | |
| 5266 | + | } | |
| 5267 | + | ], | |
| 5268 | + | "added_by": "syntaqx", | |
| 5269 | + | "added_at": "2026-10-11T14:30:12.618Z" | |
| 5270 | + | } | |
| 5271 | + | }, | |
| 5272 | + | "customize_bundle": { | |
| 5273 | + | "params": { | |
| 5274 | + | "workspace": "flagon-io" | |
| 5275 | + | }, | |
| 5276 | + | "response": { | |
| 5277 | + | "plugins": [ | |
| 5278 | + | { | |
| 5279 | + | "ref": "commit-commands@claude-plugins-official", | |
| 5280 | + | "name": "commit-commands", | |
| 5281 | + | "fetch": { | |
| 5282 | + | "kind": "github", | |
| 5283 | + | "repo": "anthropics/claude-plugins-official", | |
| 5284 | + | "ref": null, | |
| 5285 | + | "sha": "9c1f4a6e8b0d2c4f6a8e1b3d5f7a9c1e3b5d7f90", | |
| 5286 | + | "path": "plugins/commit-commands" | |
| 5287 | + | }, | |
| 5288 | + | "version": "1.0.0" | |
| 5289 | + | } | |
| 5290 | + | ], | |
| 5291 | + | "skills": [ | |
| 5292 | + | { | |
| 5293 | + | "name": "house-style", | |
| 5294 | + | "plugin": null, | |
| 5295 | + | "files": [ | |
| 5296 | + | { | |
| 5297 | + | "path": "SKILL.md", | |
| 5298 | + | "content": "---\nname: house-style\ndescription: How Flagon writes code and prose.\n---\n\n# House style\n…", | |
| 5299 | + | "encoding": "utf8" | |
| 5300 | + | }, | |
| 5301 | + | { | |
| 5302 | + | "path": "examples/commit.md", | |
| 5303 | + | "content": "# A good commit message\n…", | |
| 5304 | + | "encoding": "utf8" | |
| 5305 | + | } | |
| 5306 | + | ] | |
| 5307 | + | } | |
| 5308 | + | ], | |
| 5309 | + | "connectors": [ | |
| 5310 | + | { | |
| 5311 | + | "name": "linear", | |
| 5312 | + | "url": "https://mcp.linear.app/mcp", | |
| 5313 | + | "headers": { | |
| 5314 | + | "Authorization": "Bearer lin_oauth_…" | |
| 5315 | + | } | |
| 5316 | + | } | |
| 5317 | + | ] | |
| 5318 | + | }, | |
| 5319 | + | "notes": "Read with the job's agent token: what comes back is what reaches that agent (its own grants, its teams' and the workspace's). A workspace's token gets everything the workspace has; a person's token is refused with `403`. `plugins` are switched-on installs, to fetch at `fetch.sha` (or `fetch.ref`); a plugin's skills are not repeated in `skills`, since Claude Code reads them from the plugin. `skills` are the library's own, as files for a synthetic plugin called `g1t-library`, so each is `/g1t-library:<name>` in Claude Code. `connectors` are the connected ones that reach the agent, with the headers that sign them in, refreshed when a sign-in had expired: it is for the runner's `--mcp-config`, and g1t never keeps or logs it. See [what a run gets from Customize](/guides/self-hosted-runners/#what-a-run-gets-from-customize)." | |
| 5320 | + | }, | |
| 4353 | 5321 | "get_model_routes": { | |
| 4354 | 5322 | "params": { | |
| 4355 | 5323 | "workspace": "flagon-io" |
| 4 | 4 | ||
| 5 | 5 | use crate::about::AboutOp; | |
| 6 | 6 | use crate::run_artifacts::ArtifactsOp; | |
| 7 | + | use crate::customize::CustomizeOp; | |
| 7 | 8 | use crate::deploy_keys::DeployKeysOp; | |
| 8 | 9 | use crate::mirrors::MirrorsOp; | |
| 9 | 10 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 649 | 650 | Op::TestIntegration, | |
| 650 | 651 | &[], | |
| 651 | 652 | ), | |
| 653 | + | // Customize: marketplaces, Discover, plugins and connectors. A plugin's | |
| 654 | + | // ref is `name@marketplace`, URL-encoded (`@` as `%40`). | |
| 655 | + | route("GET", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), &[]), | |
| 656 | + | route("POST", "/workspaces/:workspace/customize/marketplaces", Op::Customize(CustomizeOp::AddMarketplace), &[]), | |
| 657 | + | route("DELETE", "/workspaces/:workspace/customize/marketplaces/:marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), &[]), | |
| 658 | + | route("POST", "/workspaces/:workspace/customize/marketplaces/:marketplace/refresh", Op::Customize(CustomizeOp::RefreshMarketplace), &[]), | |
| 659 | + | route("GET", "/workspaces/:workspace/customize/discover", Op::Customize(CustomizeOp::Discover), &[]), | |
| 660 | + | route("GET", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::ListPlugins), &[]), | |
| 661 | + | route("POST", "/workspaces/:workspace/customize/plugins", Op::Customize(CustomizeOp::InstallPlugin), &[]), | |
| 662 | + | route("GET", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::GetPlugin), &[]), | |
| 663 | + | route("PATCH", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::SetPluginEnabled), &[]), | |
| 664 | + | route("DELETE", "/workspaces/:workspace/customize/plugins/:ref", Op::Customize(CustomizeOp::UninstallPlugin), &[]), | |
| 665 | + | route("POST", "/workspaces/:workspace/customize/plugins/:ref/update", Op::Customize(CustomizeOp::UpdatePlugin), &[]), | |
| 666 | + | route("GET", "/workspaces/:workspace/customize/agents", Op::Customize(CustomizeOp::ListPluginAgents), &[]), | |
| 667 | + | route("GET", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::ListConnectors), &[]), | |
| 668 | + | route("POST", "/workspaces/:workspace/customize/connectors", Op::Customize(CustomizeOp::AddConnector), &[]), | |
| 669 | + | route("GET", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::GetConnector), &[]), | |
| 670 | + | route("PATCH", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::SetConnectorKey), &[]), | |
| 671 | + | route("DELETE", "/workspaces/:workspace/customize/connectors/:id", Op::Customize(CustomizeOp::RemoveConnector), &[]), | |
| 672 | + | route("POST", "/workspaces/:workspace/customize/connectors/:id/refresh", Op::Customize(CustomizeOp::RefreshConnector), &[]), | |
| 673 | + | route("POST", "/workspaces/:workspace/customize/connectors/:id/grants", Op::Customize(CustomizeOp::GrantConnector), &[]), | |
| 674 | + | route("DELETE", "/workspaces/:workspace/customize/connectors/:id/grants/:grant", Op::Customize(CustomizeOp::RevokeConnector), &[]), | |
| 675 | + | route("GET", "/workspaces/:workspace/customize/bundle", Op::Customize(CustomizeOp::Bundle), &[]), | |
| 652 | 676 | route( | |
| 653 | 677 | "GET", | |
| 654 | 678 | "/repos/:owner/:name/actions/workflows", | |
| ⋯ | |||
| 993 | 1017 | /// Whether the route answers success with `204` and no body, as | |
| 994 | 1018 | /// GitHub's address for the same does. MCP still answers `true`. | |
| 995 | 1019 | pub fn no_content(&self) -> bool { | |
| 996 | − | self.op == Op::DeleteComment | |
| 1020 | + | matches!( | |
| 1021 | + | self.op, | |
| 1022 | + | Op::DeleteComment | |
| 1023 | + | | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector) | |
| 1024 | + | ) | |
| 997 | 1025 | } | |
| 998 | 1026 | ||
| 999 | 1027 | /// The names of the route's path parameters, in order. | |
| 20 | 20 | ||
| 21 | 21 | use crate::about::AboutOp; | |
| 22 | 22 | use crate::run_artifacts::ArtifactsOp; | |
| 23 | + | use crate::customize::CustomizeOp; | |
| 23 | 24 | use crate::deploy_keys::DeployKeysOp; | |
| 24 | 25 | use crate::mirrors::MirrorsOp; | |
| 25 | 26 | use crate::deployments::DeploymentsOp; | |
| ⋯ | |||
| 432 | 433 | ], | |
| 433 | 434 | }, | |
| 434 | 435 | Tool { | |
| 436 | + | name: "customize", | |
| 437 | + | title: "Customize", | |
| 438 | + | 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.", | |
| 439 | + | default_action: None, | |
| 440 | + | actions: &[ | |
| 441 | + | a("list_marketplaces", Op::Customize(CustomizeOp::ListMarketplaces), "The marketplaces it reads plugins from"), | |
| 442 | + | a("add_marketplace", Op::Customize(CustomizeOp::AddMarketplace), "Add one by owner/repo, workspace/name, a git address or a marketplace.json link"), | |
| 443 | + | a("remove_marketplace", Op::Customize(CustomizeOp::RemoveMarketplace), "Remove one; its installed plugins stay"), | |
| 444 | + | a("refresh_marketplace", Op::Customize(CustomizeOp::RefreshMarketplace), "Read its manifest again now"), | |
| 445 | + | a("discover", Op::Customize(CustomizeOp::Discover), "Every plugin and skill it can add, with availability"), | |
| 446 | + | a("list_plugins", Op::Customize(CustomizeOp::ListPlugins), "Installed plugins: pinned commit, components, updates"), | |
| 447 | + | a("get_plugin", Op::Customize(CustomizeOp::GetPlugin), "One plugin's listing, install, components, README and agents"), | |
| 448 | + | a("install_plugin", Op::Customize(CustomizeOp::InstallPlugin), "Install one at its listed commit"), | |
| 449 | + | a("set_plugin_enabled", Op::Customize(CustomizeOp::SetPluginEnabled), "Switch an installed one on or off"), | |
| 450 | + | a("uninstall_plugin", Op::Customize(CustomizeOp::UninstallPlugin), "Uninstall one, with its skills, templates and connectors"), | |
| 451 | + | a("update_plugin", Op::Customize(CustomizeOp::UpdatePlugin), "Read one again at its marketplace's current commit"), | |
| 452 | + | a("list_plugin_agents", Op::Customize(CustomizeOp::ListPluginAgents), "Subagents plugins brought, as templates to hire from"), | |
| 453 | + | a("list_connectors", Op::Customize(CustomizeOp::ListConnectors), "MCP servers connected to the workspace, with state, tools and grants"), | |
| 454 | + | a("get_connector", Op::Customize(CustomizeOp::GetConnector), "One connector"), | |
| 455 | + | a("add_connector", Op::Customize(CustomizeOp::AddConnector), "Add one from the directory, a plugin's server or an address"), | |
| 456 | + | a("set_connector_key", Op::Customize(CustomizeOp::SetConnectorKey), "Put a key on one; never returned"), | |
| 457 | + | a("remove_connector", Op::Customize(CustomizeOp::RemoveConnector), "Remove one with its secrets and grants"), | |
| 458 | + | a("refresh_connector", Op::Customize(CustomizeOp::RefreshConnector), "List its tools again now"), | |
| 459 | + | a("grant_connector", Op::Customize(CustomizeOp::GrantConnector), "Give it to every agent, a team or one agent"), | |
| 460 | + | a("revoke_connector", Op::Customize(CustomizeOp::RevokeConnector), "Take one grant back"), | |
| 461 | + | a("bundle", Op::Customize(CustomizeOp::Bundle), "What a job's Claude Code loads: plugins, library skills, connectors with headers"), | |
| 462 | + | ], | |
| 463 | + | }, | |
| 464 | + | Tool { | |
| 435 | 465 | name: "billing", | |
| 436 | 466 | title: "Billing", | |
| 437 | 467 | 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.", | |
| ⋯ | |||
| 593 | 623 | | Op::DeleteRunnerGroup | |
| 594 | 624 | | Op::UpdateRunnerSettings | |
| 595 | 625 | | Op::Folios(FoliosOp::SetAccess | FoliosOp::Purge) | |
| 626 | + | | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector) | |
| 596 | 627 | ) | |
| 597 | 628 | } | |
| 598 | 629 | ||
| ⋯ | |||
| 843 | 874 | assert!(tool.action(default).is_some(), "{}", tool.name); | |
| 844 | 875 | } | |
| 845 | 876 | } | |
| 846 | − | assert!(TOOLS.len() <= 19, "{} tools", TOOLS.len()); | |
| 877 | + | assert!(TOOLS.len() <= 20, "{} tools", TOOLS.len()); | |
| 847 | 878 | } | |
| 848 | 879 | ||
| 849 | 880 | #[test] | |
| 487 | 487 | writes the plugins to disk and starts Claude Code with each as a plugin | |
| 488 | 488 | directory, puts the skills where it reads them, and merges the connectors | |
| 489 | 489 | into its MCP configuration. See | |
| 490 | − | [plugins and skills on runners](/guides/self-hosted-runners/#plugins-and-skills-on-runners). | |
| 490 | + | [what a run gets from Customize](/guides/self-hosted-runners/#what-a-run-gets-from-customize). | |
| 491 | 491 | ||
| 492 | 492 | Chat replies and [sessions](/guides/agent-sessions/) on g1t get the same | |
| 493 | 493 | things in their own way: skills through `use_skill`, connectors as | |
| ⋯ | |||
| 497 | 497 | ||
| 498 | 498 | Customize is in the REST API under `/workspaces/{workspace}/customize/…` | |
| 499 | 499 | and in the MCP server as the `customize` tool, one action per thing an | |
| 500 | − | owner does here: marketplaces, Discover, plugins and installs, connectors | |
| 501 | − | with their sign-ins and grants. Reading needs `workspace:read`; adding, | |
| 502 | − | installing, connecting and granting need `workspace:admin`, as a person | |
| 503 | − | who owns the workspace. The bundle a runner reads, | |
| 500 | + | owner does here: marketplaces, Discover, plugins and installs (installing, | |
| 501 | + | switching off, updating and uninstalling), connectors with their keys and | |
| 502 | + | grants. Reading needs `workspace:read`; adding, installing, connecting and | |
| 503 | + | granting need `workspace:admin`, as a person who owns the workspace. | |
| 504 | + | Signing a connector in with OAuth needs a browser, so it stays on the | |
| 505 | + | site. The bundle a runner reads, | |
| 504 | 506 | `GET /workspaces/{workspace}/customize/bundle`, needs `agents:run` and is | |
| 505 | 507 | read with a job's agent token, since it carries the connectors' sign-ins. | |
| 506 | 508 | See the [MCP tools](/reference/mcp/) and the [API reference](/reference/api/). | |
| 203 | 203 | that credential, so budgets, caps and the audit log work as before. With | |
| 204 | 204 | your own [model provider](/guides/models/), g1t charges nothing for the | |
| 205 | 205 | run; otherwise the model is paid as usual and the machine is free. | |
| 206 | − | - Agent work needs an image with git, Node and the agent's CLI: register | |
| 207 | − | the runner with `--agent-image` and an image of yours that has them, or | |
| 208 | − | run it with `--no-docker` on a Linux machine set aside for it, where | |
| 209 | − | `/work` can be written. g1t does not publish an agent image yet. | |
| 206 | + | - Agent work needs an image with git, Node, `tar` and the agent's CLI: | |
| 207 | + | register the runner with `--agent-image` and an image of yours that has | |
| 208 | + | them, or run it with `--no-docker` on a Linux machine set aside for it, | |
| 209 | + | where `/work` can be written. g1t does not publish an agent image yet. | |
| 210 | 210 | - [Guardrails](/guides/guardrails/) still apply to what the agent does, but | |
| 211 | 211 | their network list cannot be enforced on your machine. The run's session | |
| 212 | 212 | says so when it starts. | |
| 213 | 213 | ||
| 214 | 214 | A project can have its own setting, or follow the workspace's. | |
| 215 | 215 | ||
| 216 | − | ## Plugins and skills on runners | |
| 216 | + | ## What a run gets from Customize | |
| 217 | + | ||
| 218 | + | When g1t's work runs Claude Code on a runner (on g1t's own machines too), | |
| 219 | + | the job gets what the workspace set up in [Customize](/guides/customize/), | |
| 220 | + | as the job's agent would have it. Before Claude Code starts, the runner | |
| 221 | + | reads the workspace's bundle with the job's short-lived agent credential, | |
| 222 | + | once per job, and makes it ready: | |
| 223 | + | ||
| 224 | + | | In the bundle | What the runner does | In Claude Code | | |
| 225 | + | | --- | --- | --- | | |
| 226 | + | | Installed plugins that are switched on, each with where to fetch it and the commit it is pinned to | Fetches each at that commit (GitHub's archive of the commit, a clone of the repository here, or the zip the marketplace names) into `/work/.g1t/plugins/<name>/` and passes it with `--plugin-dir`, one flag per plugin | Its skills as `/<plugin>:<skill>`, its subagents as `<plugin>:<agent>`, and its hooks, LSP servers, output styles and `bin/`, which run only here and never on g1t's own agents | | |
| 227 | + | | The library's own skills that reach the agent, as files | Writes them as one more plugin, `g1t-library`, with a `skills/<name>/` folder each, passed with `--plugin-dir` too. A plugin's skills come with the plugin itself | `/g1t-library:<skill>` | | |
| 228 | + | | The connectors that reach the agent and are connected, with the headers that sign them in | Merges each into the `--mcp-config` file's `mcpServers`, by its name, beside g1t's own server | `mcp__<name>__<tool>` | | |
| 217 | 229 | ||
| 218 | − | When g1t's work runs Claude Code on a runner, the job gets what the | |
| 219 | − | workspace set up in [Customize](/guides/customize/), as the job's agent | |
| 220 | − | would have it. Before the first step, the runner reads the workspace's | |
| 221 | − | bundle with the job's short-lived credential, once per job: | |
| 230 | + | The run's session gets one line saying what was loaded: *Loaded 2 plugins, | |
| 231 | + | 1 library skill and 3 connectors from the workspace's Customize.* A plugin | |
| 232 | + | that could not be fetched, or a bundle that could not be read, is named | |
| 233 | + | there and the run goes on without it; nothing from Customize fails a run. | |
| 234 | + | Plugins from GitHub need `tar` on the machine. | |
| 222 | 235 | ||
| 223 | − | | In the bundle | What the runner does | | |
| 224 | − | | --- | --- | | |
| 225 | − | | Installed plugins that are switched on, each with where to fetch it and the commit it was installed at | Fetches each at that commit and starts Claude Code with it as a plugin directory, so its skills, agents, commands, hooks, LSP servers and `bin/` are all there | | |
| 226 | − | | The library's own skills that reach the agent, as files | Writes each skill's folder where Claude Code reads skills. A plugin's skills come with the plugin itself | | |
| 227 | − | | The connectors that reach the agent and are connected, with the headers that sign them in | Merges them into Claude Code's MCP configuration | | |
| 236 | + | A run on a fork's head loads none of the fork's own settings, hooks, MCP | |
| 237 | + | servers or commands ([guardrails](/guides/guardrails/)). The workspace's | |
| 238 | + | plugins and connectors are the workspace's, not the checkout's, so they are | |
| 239 | + | still passed and merged; but every skill and command is switched off on | |
| 240 | + | such a run, the plugins' and the library's included, so an agent cannot | |
| 241 | + | invoke them there. | |
| 228 | 242 | ||
| 229 | − | The bundle carries the connectors' sign-ins, so only a job's credential | |
| 230 | − | reads it: `GET /workspaces/{workspace}/customize/bundle`, scope | |
| 231 | − | `agents:run`. See [what a run gets](/guides/customize/#what-a-run-gets). | |
| 243 | + | The bundle carries the connectors' sign-ins, so only a job's agent | |
| 244 | + | credential or a workspace's token reads it, never a person's: | |
| 245 | + | [`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/), | |
| 246 | + | scope `agents:run`. g1t never keeps or logs it, and the runner keeps the | |
| 247 | + | keys it brought out of the session. See | |
| 248 | + | [what a run gets](/guides/customize/#what-a-run-gets). | |
| 232 | 249 | ||
| 233 | 250 | ## Billing | |
| 234 | 251 | ||
| ⋯ | |||
| 353 | 370 | | Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` | | |
| 354 | 371 | | Groups | [`GET`](/reference/api/runners/list-runner-groups/), [`POST`](/reference/api/runners/create-runner-group/), [`PATCH`](/reference/api/runners/update-runner-group/), [`DELETE`](/reference/api/runners/delete-runner-group/) `/workspaces/{workspace}/actions/runner-groups` | `list_runner_groups`, `create_runner_group`, `update_runner_group`, `delete_runner_group` | `runners:read`, `runners:admin` | | |
| 355 | 372 | | Where work runs | [`GET`](/reference/api/runners/get-runner-settings-for-workspace/), [`PATCH`](/reference/api/runners/update-runner-settings-for-workspace/) `…/actions/runner-settings` | `get_runner_settings`, `update_runner_settings` | `runners:read`, `runners:admin` | | |
| 373 | + | | What a run gets from Customize | [`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/) | `customize` tool, `bundle` | `agents:run`, with a job's agent token | | |
| 356 | 374 | ||
| 357 | 375 | The [Agent preset](/guides/authentication/#scopes) does not include | |
| 358 | 376 | `runners:read`. | |
| 616 | 616 | model calls still go through g1t; the machine time is free. g1t's network | |
| 617 | 617 | guardrails cannot be enforced on your machines, and the run says so. | |
| 618 | 618 | ||
| 619 | + | ## What a run gets from Customize | |
| 620 | + | ||
| 621 | + | Wherever it runs, a run of @g1t is Claude Code, and it starts with what the | |
| 622 | + | workspace set up in [Customize](/guides/customize/), as the run's agent | |
| 623 | + | would have it: every installed plugin that is switched on, fetched at the | |
| 624 | + | commit it is pinned to and loaded as a plugin, so its skills are | |
| 625 | + | `/<plugin>:<skill>`; the library's own skills that reach the agent, as a | |
| 626 | + | plugin called `g1t-library`, so each is `/g1t-library:<skill>`; and the | |
| 627 | + | connectors that reach the agent and are signed in, as MCP servers beside | |
| 628 | + | g1t's own, so their tools are `mcp__<name>__<tool>`. A plugin's hooks, LSP | |
| 629 | + | servers, output styles and `bin/` run in that sandbox and never on g1t's | |
| 630 | + | own agents. The session's first lines say what was loaded; a plugin that | |
| 631 | + | could not be fetched is named there and the run goes on without it. See | |
| 632 | + | [what a run gets from Customize](/guides/self-hosted-runners/#what-a-run-gets-from-customize) | |
| 633 | + | for how the runner does it. | |
| 634 | + | ||
| 619 | 635 | ## Who can run agents | |
| 620 | 636 | ||
| 621 | 637 | Putting an agent to work (assigning it, mentioning it, asking it for a |
| 242 | 242 | | [Secrets and variables](/reference/api/secrets-and-variables/list-actions-secrets/) | Values workflows and deployments read. | | |
| 243 | 243 | | [Webhooks](/reference/api/webhooks/list-webhooks/) | Events sent to your own address. | | |
| 244 | 244 | | [Integrations](/reference/api/integrations/list-integrations/) | Model providers, alert sources and issue trackers. | | |
| 245 | + | | [Customize](/reference/api/customize/list-marketplaces/) | What shapes a workspace's agents, in the formats Claude Code reads: the marketplaces it reads plugins from, Discover, installed plugins, connectors with their keys and grants, and the bundle a run's Claude Code loads. See [Customize](/guides/customize/). | |
| 3 | 3 | description: The g1t MCP server's resource tools, each action they take with its required inputs and scope, and how to call them. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | − | The MCP server at `https://mcp.g1t.sh` exposes 19 tools, one per kind of | |
| 6 | + | The MCP server at `https://mcp.g1t.sh` exposes 20 tools, one per kind of | |
| 7 | 7 | thing on g1t: `search`, `repository`, `issue`, `pull_request`, `agent`, | |
| 8 | 8 | `plan`, `memory`, `workflow`, `package`, `secret`, `security`, `webhook`, `access`, | |
| 9 | − | `team`, `workspace`, `billing`, `notifications`, `account` and `artifact`. Each tool takes an `action` that says what to do. Every | |
| 9 | + | `team`, `workspace`, `customize`, `billing`, `notifications`, `account` and `artifact`. Each tool takes an `action` that says what to do. Every | |
| 10 | 10 | action is the same operation as a route of the [REST API](/reference/api/), | |
| 11 | 11 | with the same inputs, permissions and results, so the two always agree. | |
| 12 | 12 | ||
| 13 | − | <!-- TODO(customize): the `customize` tool (marketplaces, Discover, plugins and installs, connectors with sign-ins and grants, and the runner bundle; scopes workspace:read, workspace:admin and agents:run) is documented with the API change that adds it. The guide is /guides/customize/#api. --> | |
| 14 | − | ||
| 15 | 13 | ## Connect | |
| 16 | 14 | ||
| 17 | 15 | To connect Claude Code, Codex, OpenCode, Cursor or another client, see | |
| ⋯ | |||
| 171 | 169 | | --- | --- | | |
| 172 | 170 | | `title` | The tool's name for people, such as `Pull requests`. | | |
| 173 | 171 | | `readOnlyHint` | `true` when every action shown only reads. | | |
| 174 | − | | `destructiveHint` | `true` when the tool is not read-only and an action shown cannot be undone or reaches beyond g1t's own records: deleting a workspace, deleting, purging or transferring a repository, changing its visibility, removing an email address or a collaborator, deleting a team or taking its role on a repository away, disconnecting an integration, deleting a webhook, setting or deleting secrets and variables, replacing model routes, setting a workspace's base permission, removing a member, transferring a workspace's ownership, leaving a workspace, merging a pull request, removing a self-hosted runner, deleting a runner group, changing runner settings, sharing an artifact, and deleting an artifact for good. | | |
| 172 | + | | `destructiveHint` | `true` when the tool is not read-only and an action shown cannot be undone or reaches beyond g1t's own records: deleting a workspace, deleting, purging or transferring a repository, changing its visibility, removing an email address or a collaborator, deleting a team or taking its role on a repository away, disconnecting an integration, removing a marketplace, uninstalling a plugin, removing a connector, deleting a webhook, setting or deleting secrets and variables, replacing model routes, setting a workspace's base permission, removing a member, transferring a workspace's ownership, leaving a workspace, merging a pull request, removing a self-hosted runner, deleting a runner group, changing runner settings, sharing an artifact, and deleting an artifact for good. | | |
| 175 | 173 | | `idempotentHint` | The same as `readOnlyHint`. | | |
| 176 | 174 | | `openWorldHint` | Always `false`. | | |
| 177 | 175 | ||
| ⋯ | |||
| 699 | 697 | | [`review_token_request`](/reference/api/personal-access-tokens/review-token-request/) | Approve or deny one: `decision` is `approve` or `deny`, with an optional `reason` its owner is shown. Owners only, as people. | `workspace`, `id`, `decision` | `access:admin` | | |
| 700 | 698 | | [`revoke_member_token`](/reference/api/personal-access-tokens/revoke-member-token/) | Take a member's token out of the workspace, with an optional `reason`. A token made for it stops reaching it; one made for all of its owner's workspaces keeps working elsewhere. Owners only, as people. | `workspace`, `id` | `access:admin` | | |
| 701 | 699 | ||
| 700 | + | ## `customize` | |
| 701 | + | ||
| 702 | + | What shapes how a workspace's agents work, in the formats Claude Code | |
| 703 | + | reads: the marketplaces it reads plugins from (`.claude-plugin/marketplace.json`), | |
| 704 | + | what Discover lists, the plugins it installed as `name@marketplace` (their | |
| 705 | + | skills join the library as `plugin:skill`, their subagents become | |
| 706 | + | templates, their remote MCP servers become connectors), and its | |
| 707 | + | connectors: MCP servers connected once with a key or a sign-in, then given | |
| 708 | + | to every agent, a team or one agent. Members read; owners change. Signing a | |
| 709 | + | connector in with OAuth needs a browser, so it is done on the site. `bundle` | |
| 710 | + | is what a job's Claude Code loads on a runner, for the job's own token. See | |
| 711 | + | [Customize](/guides/customize/). | |
| 712 | + | ||
| 713 | + | | Action | What it does | Required | Scope | | |
| 714 | + | | --- | --- | --- | --- | | |
| 715 | + | | [`list_marketplaces`](/reference/api/customize/list-marketplaces/) | The marketplaces the workspace reads plugins from: `name`, `source`, whether `builtin`, how many `plugins`, `fetched_at` and `problem`. Members only. | `workspace` | `workspace:read` | | |
| 716 | + | | [`add_marketplace`](/reference/api/customize/add-marketplace/) | Add one by `address`: `owner/repo` on GitHub, `workspace/name` here, a git address or a `marketplace.json` link; `name` overrides its own. Its manifest is read first. Owners only. | `workspace`, `address` | `workspace:admin` | | |
| 717 | + | | [`remove_marketplace`](/reference/api/customize/remove-marketplace/) | Remove one by its name (`marketplace`); plugins installed from it stay. Never a built-in one. Owners only. | `workspace`, `marketplace` | `workspace:admin` | | |
| 718 | + | | [`refresh_marketplace`](/reference/api/customize/refresh-marketplace/) | Read its manifest again now. Owners only. | `workspace`, `marketplace` | `workspace:admin` | | |
| 719 | + | | [`discover`](/reference/api/customize/discover/) | Every plugin and skill the workspace can add, with each one's `availability` (`available`, `installed`, `unavailable` with why, or `soon`), and the marketplaces. Members only. | `workspace` | `workspace:read` | | |
| 720 | + | | [`list_plugins`](/reference/api/customize/list-plugins/) | Installed plugins, switched-off ones included: pinned `sha`, `fetch`, `components`, `problems`, `enabled` and `update`. Members only. | `workspace` | `workspace:read` | | |
| 721 | + | | [`get_plugin`](/reference/api/customize/get-plugin/) | One plugin by `ref` (`name@marketplace`), installed or not: its listing, install, components, README, manifest and subagents, read from its source. Members only. | `workspace`, `ref` | `workspace:read` | | |
| 722 | + | | [`install_plugin`](/reference/api/customize/install-plugin/) | Install one at the commit its listing names: skills to the library as `plugin:skill`, subagents to Templates, remote servers to connectors. Owners only. | `workspace`, `ref` | `workspace:admin` | | |
| 723 | + | | [`set_plugin_enabled`](/reference/api/customize/set-plugin-enabled/) | Switch an installed one off (`enabled` false: its skills, templates and connectors leave agents, and runners leave it out) or on. Owners only. | `workspace`, `ref`, `enabled` | `workspace:admin` | | |
| 724 | + | | [`uninstall_plugin`](/reference/api/customize/uninstall-plugin/) | Uninstall one: its skills, templates and the connectors it brought go. Owners only. | `workspace`, `ref` | `workspace:admin` | | |
| 725 | + | | [`update_plugin`](/reference/api/customize/update-plugin/) | Read one again at its marketplace's current commit and pin it there: skills get new versions, ones it lost are archived, a server it already brought keeps its connector. Owners only. | `workspace`, `ref` | `workspace:admin` | | |
| 726 | + | | [`list_plugin_agents`](/reference/api/customize/list-plugin-agents/) | The subagents installed plugins brought, as templates to hire from (`plugin:agent`, with `prompt`, `model`, `skills`, `tools`). Members only. | `workspace` | `workspace:read` | | |
| 727 | + | | [`list_connectors`](/reference/api/customize/list-connectors/) | The workspace's connectors: `name`, `url`, `auth`, `state` (`connected`, `needs_sign_in`, `needs_key`, `problem`), `from`, `tools`, `grants`. Keys are never returned. Members only. | `workspace` | `workspace:read` | | |
| 728 | + | | [`get_connector`](/reference/api/customize/get-connector/) | One connector by `id` (`con_…`). Members only. | `workspace`, `id` | `workspace:read` | | |
| 729 | + | | [`add_connector`](/reference/api/customize/add-connector/) | Add one `from` the directory (`id`), an installed plugin's server (`ref`, `server`) or an address (`name`, `url`), with `auth` `none`, `header` (and `header` `name` and `value`) or `oauth`. Owners only. | `workspace`, `from` | `workspace:admin` | | |
| 730 | + | | [`set_connector_key`](/reference/api/customize/set-connector-key/) | Put a key on one: `value`, sent as `header_name` (its own, or `Authorization`). Kept sealed, never returned. Owners only. | `workspace`, `id`, `value` | `workspace:admin` | | |
| 731 | + | | [`remove_connector`](/reference/api/customize/remove-connector/) | Remove one with its secrets and grants. Owners only. | `workspace`, `id` | `workspace:admin` | | |
| 732 | + | | [`refresh_connector`](/reference/api/customize/refresh-connector/) | List its tools again now. Owners only. | `workspace`, `id` | `workspace:admin` | | |
| 733 | + | | [`grant_connector`](/reference/api/customize/grant-connector/) | Give it to every agent (`scope` `workspace`), a team (`team`, `target` its slug) or one agent (`agent`, `target` its handle). Owners only. | `workspace`, `id`, `scope` | `workspace:admin` | | |
| 734 | + | | [`revoke_connector`](/reference/api/customize/revoke-connector/) | Take one grant back, by its id (`grant`). Owners only. | `workspace`, `id`, `grant` | `workspace:admin` | | |
| 735 | + | | [`bundle`](/reference/api/customize/customize-bundle/) | What a job's Claude Code loads: switched-on `plugins` with where to fetch each at its pinned commit, the library `skills` that reach the job's agent as files, and the `connectors` that reach it with the headers that sign them in. A job's agent token or a workspace's token; a person's is refused. Secrets are in it: g1t never keeps or logs it. | `workspace` | `agents:run` | | |
| 736 | + | ||
| 702 | 737 | ## `billing` | |
| 703 | 738 | ||
| 704 | 739 | A workspace's billing: its usage, its budget, its AI credit, its | |
| ⋯ | |||
| 817 | 852 | ||
| 818 | 853 | | Run | Actions | | |
| 819 | 854 | | --- | --- | | |
| 820 | − | | Implement, revise, answer | Reading: `repository` `get`, `list_labels`, `list_milestones`, `get_milestone` and `list_events`; `issue` `list`, `get` and `labels`; `pull_request` `list`, `get`, `changes`, `read_session` and `merge_queue`; `memory` `recall`; `search` `code`, `context` and `entity`; `workflow` `list`, `list_runs`, `get_run` and `job_logs`. Then `issue` `create` and `comment`, `memory` `remember`, `agent` `message`, `answer` and `take_messages`, and `search` `ticket`. | | |
| 855 | + | | Implement, revise, answer | Reading: `repository` `get`, `list_labels`, `list_milestones`, `get_milestone` and `list_events`; `issue` `list`, `get` and `labels`; `pull_request` `list`, `get`, `changes`, `read_session` and `merge_queue`; `memory` `recall`; `search` `code`, `context` and `entity`; `workflow` `list`, `list_runs`, `get_run` and `job_logs`; `customize` `bundle`, which the runner reads before Claude Code starts. Then `issue` `create` and `comment`, `memory` `remember`, `agent` `message`, `answer` and `take_messages`, and `search` `ticket`. | | |
| 821 | 856 | | Review | The same reading actions, and `issue` `comment`, `pull_request` `review` and `search` `ticket`. | | |
| 822 | 857 | | Plan | The same reading actions, and `issue` `create` and `search` `ticket`. | | |
| 823 | 858 | | Catch up | The reading actions only. | | |
| 824 | 859 | ||
| 825 | 860 | No agent's token can use the `workspace`, `access`, `team`, `secret`, `webhook` or | |
| 826 | − | `notifications` tools (g1t acts as `g1t`, which has no notifications), the controls of `workflow`, or `pull_request` `merge`, `agent` | |
| 861 | + | `notifications` tools, or any `customize` action but `bundle` (g1t acts as `g1t`, which has no notifications), the controls of `workflow`, or `pull_request` `merge`, `agent` | |
| 827 | 862 | `assign` and `delegate`, `plan` `create` and `apply`, `issue` `import`, or | |
| 828 | 863 | any `repository` action that creates, changes, renames, archives, | |
| 829 | 864 | transfers, deletes, restores or purges a repository, or dismisses or | |
Binary or large file; its contents are not shown.
| 549 | 549 | "message_agent", | |
| 550 | 550 | "answer_message", | |
| 551 | 551 | "get_context", | |
| 552 | + | "customize_bundle", | |
| 552 | 553 | ]); | |
| 553 | 554 | } | |
| 554 | 555 | K::Review => { | |
| 555 | 556 | operations.extend(TOOLS_READ.iter().copied()); | |
| 556 | − | operations.extend(["add_comment", "review_pull_request", "get_context"]); | |
| 557 | + | operations.extend(["add_comment", "review_pull_request", "get_context", "customize_bundle"]); | |
| 557 | 558 | } | |
| 558 | 559 | K::Plan => { | |
| 559 | 560 | operations.extend(TOOLS_READ.iter().copied()); | |
| 560 | − | operations.extend(["create_issue", "get_context"]); | |
| 561 | + | operations.extend(["create_issue", "get_context", "customize_bundle"]); | |
| 562 | + | } | |
| 563 | + | K::Update => { | |
| 564 | + | operations.extend(TOOLS_READ.iter().copied()); | |
| 565 | + | operations.push("customize_bundle"); | |
| 561 | 566 | } | |
| 562 | − | K::Update => operations.extend(TOOLS_READ.iter().copied()), | |
| 563 | 567 | K::Checks | K::Queue | K::Mergecheck | K::Deploy | K::Bump => {} | |
| 564 | 568 | }, | |
| 565 | 569 | } |
| 935 | 935 | ("test_integration", Scope::WorkspaceAdmin), | |
| 936 | 936 | ("get_model_routes", Scope::WorkspaceRead), | |
| 937 | 937 | ("set_model_routes", Scope::WorkspaceAdmin), | |
| 938 | + | // Customize: the marketplaces a workspace reads, the plugins it installs | |
| 939 | + | // and the connectors it signs in. Members see them, owners change them, | |
| 940 | + | // and a job's agent token reads the bundle its Claude Code loads. | |
| 941 | + | ("list_marketplaces", Scope::WorkspaceRead), | |
| 942 | + | ("add_marketplace", Scope::WorkspaceAdmin), | |
| 943 | + | ("remove_marketplace", Scope::WorkspaceAdmin), | |
| 944 | + | ("refresh_marketplace", Scope::WorkspaceAdmin), | |
| 945 | + | ("discover", Scope::WorkspaceRead), | |
| 946 | + | ("list_plugins", Scope::WorkspaceRead), | |
| 947 | + | ("get_plugin", Scope::WorkspaceRead), | |
| 948 | + | ("install_plugin", Scope::WorkspaceAdmin), | |
| 949 | + | ("set_plugin_enabled", Scope::WorkspaceAdmin), | |
| 950 | + | ("uninstall_plugin", Scope::WorkspaceAdmin), | |
| 951 | + | ("update_plugin", Scope::WorkspaceAdmin), | |
| 952 | + | ("list_plugin_agents", Scope::WorkspaceRead), | |
| 953 | + | ("list_connectors", Scope::WorkspaceRead), | |
| 954 | + | ("get_connector", Scope::WorkspaceRead), | |
| 955 | + | ("add_connector", Scope::WorkspaceAdmin), | |
| 956 | + | ("set_connector_key", Scope::WorkspaceAdmin), | |
| 957 | + | ("remove_connector", Scope::WorkspaceAdmin), | |
| 958 | + | ("refresh_connector", Scope::WorkspaceAdmin), | |
| 959 | + | ("grant_connector", Scope::WorkspaceAdmin), | |
| 960 | + | ("revoke_connector", Scope::WorkspaceAdmin), | |
| 961 | + | ("customize_bundle", Scope::AgentsRun), | |
| 938 | 962 | // Teams: reading them, and managing them. A team's role on a | |
| 939 | 963 | // repository is who has access. | |
| 940 | 964 | ("list_teams", Scope::WorkspaceRead), |
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Comments
Sign in to comment.