Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

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.

syntaqxcommitted Parenta1cd67eBrowse files
22 files+1690−376 files not counted0/22 viewed
+463−0
1+//! Customize over REST and MCP: what shapes how a workspace's agents work,
2+//! at `/workspaces/{workspace}/customize/…` and as the MCP `customize` tool.
3+//! Marketplaces the workspace reads plugins from, what Discover lists, the
4+//! plugins it installed and the agents they brought, and its connectors
5+//! (MCP servers connected once, then given to agents), with their keys and
6+//! grants. Also the bundle a job's Claude Code loads on a runner.
7+//!
8+//! The agents service keeps all of it and decides who may do what
9+//! (`services/agents/src/customize-rpc.ts`): members see, owners change,
10+//! and the bundle is read with a job's agent token or a workspace's. The
11+//! bundle holds connectors' secrets: the API hands it on and never keeps
12+//! or logs it. OAuth sign-in to a connector needs a browser, so it stays
13+//! on the site.
14+
15+use g1t_contracts::rules::actor_kind;
16+use g1t_contracts::{FailureCode, Outcome, Viewer};
17+use serde_json::{Value, json};
18+use worker::Result;
19+
20+use crate::operations::Services;
21+
22+/// One operation on a workspace's Customize.
23+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24+pub enum CustomizeOp {
25+ ListMarketplaces,
26+ AddMarketplace,
27+ RemoveMarketplace,
28+ RefreshMarketplace,
29+ Discover,
30+ ListPlugins,
31+ GetPlugin,
32+ InstallPlugin,
33+ SetPluginEnabled,
34+ UninstallPlugin,
35+ UpdatePlugin,
36+ ListPluginAgents,
37+ ListConnectors,
38+ GetConnector,
39+ AddConnector,
40+ SetConnectorKey,
41+ RemoveConnector,
42+ RefreshConnector,
43+ GrantConnector,
44+ RevokeConnector,
45+ Bundle,
46+}
47+
48+impl CustomizeOp {
49+ /// Every one: `Op::ALL` lists each as `Op::Customize(…)`, which a test
50+ /// checks against this.
51+ #[cfg(test)]
52+ pub const ALL: [CustomizeOp; 21] = [
53+ CustomizeOp::ListMarketplaces,
54+ CustomizeOp::AddMarketplace,
55+ CustomizeOp::RemoveMarketplace,
56+ CustomizeOp::RefreshMarketplace,
57+ CustomizeOp::Discover,
58+ CustomizeOp::ListPlugins,
59+ CustomizeOp::GetPlugin,
60+ CustomizeOp::InstallPlugin,
61+ CustomizeOp::SetPluginEnabled,
62+ CustomizeOp::UninstallPlugin,
63+ CustomizeOp::UpdatePlugin,
64+ CustomizeOp::ListPluginAgents,
65+ CustomizeOp::ListConnectors,
66+ CustomizeOp::GetConnector,
67+ CustomizeOp::AddConnector,
68+ CustomizeOp::SetConnectorKey,
69+ CustomizeOp::RemoveConnector,
70+ CustomizeOp::RefreshConnector,
71+ CustomizeOp::GrantConnector,
72+ CustomizeOp::RevokeConnector,
73+ CustomizeOp::Bundle,
74+ ];
75+
76+ pub fn name(self) -> &'static str {
77+ match self {
78+ CustomizeOp::ListMarketplaces => "list_marketplaces",
79+ CustomizeOp::AddMarketplace => "add_marketplace",
80+ CustomizeOp::RemoveMarketplace => "remove_marketplace",
81+ CustomizeOp::RefreshMarketplace => "refresh_marketplace",
82+ CustomizeOp::Discover => "discover",
83+ CustomizeOp::ListPlugins => "list_plugins",
84+ CustomizeOp::GetPlugin => "get_plugin",
85+ CustomizeOp::InstallPlugin => "install_plugin",
86+ CustomizeOp::SetPluginEnabled => "set_plugin_enabled",
87+ CustomizeOp::UninstallPlugin => "uninstall_plugin",
88+ CustomizeOp::UpdatePlugin => "update_plugin",
89+ CustomizeOp::ListPluginAgents => "list_plugin_agents",
90+ CustomizeOp::ListConnectors => "list_connectors",
91+ CustomizeOp::GetConnector => "get_connector",
92+ CustomizeOp::AddConnector => "add_connector",
93+ CustomizeOp::SetConnectorKey => "set_connector_key",
94+ CustomizeOp::RemoveConnector => "remove_connector",
95+ CustomizeOp::RefreshConnector => "refresh_connector",
96+ CustomizeOp::GrantConnector => "grant_connector",
97+ CustomizeOp::RevokeConnector => "revoke_connector",
98+ CustomizeOp::Bundle => "customize_bundle",
99+ }
100+ }
101+
102+ /// The agents service method it passes through to.
103+ fn method(self) -> &'static str {
104+ match self {
105+ CustomizeOp::ListMarketplaces => "marketplaces",
106+ CustomizeOp::AddMarketplace => "add_marketplace",
107+ CustomizeOp::RemoveMarketplace => "remove_marketplace",
108+ CustomizeOp::RefreshMarketplace => "refresh_marketplace",
109+ CustomizeOp::Discover => "discover",
110+ CustomizeOp::ListPlugins => "plugin_installs",
111+ CustomizeOp::GetPlugin => "plugin",
112+ CustomizeOp::InstallPlugin => "install_plugin",
113+ CustomizeOp::SetPluginEnabled => "set_plugin_enabled",
114+ CustomizeOp::UninstallPlugin => "uninstall_plugin",
115+ CustomizeOp::UpdatePlugin => "update_plugin",
116+ CustomizeOp::ListPluginAgents => "plugin_agents",
117+ CustomizeOp::ListConnectors => "connectors",
118+ CustomizeOp::GetConnector => "connector",
119+ CustomizeOp::AddConnector => "add_connector",
120+ CustomizeOp::SetConnectorKey => "set_connector_key",
121+ CustomizeOp::RemoveConnector => "remove_connector",
122+ CustomizeOp::RefreshConnector => "refresh_connector",
123+ CustomizeOp::GrantConnector => "grant_connector",
124+ CustomizeOp::RevokeConnector => "revoke_connector",
125+ CustomizeOp::Bundle => "customize_bundle",
126+ }
127+ }
128+
129+ /// For the API reference.
130+ pub fn title(self) -> &'static str {
131+ match self {
132+ CustomizeOp::ListMarketplaces => "List marketplaces",
133+ CustomizeOp::AddMarketplace => "Add a marketplace",
134+ CustomizeOp::RemoveMarketplace => "Remove a marketplace",
135+ CustomizeOp::RefreshMarketplace => "Refresh a marketplace",
136+ CustomizeOp::Discover => "Discover plugins and skills",
137+ CustomizeOp::ListPlugins => "List installed plugins",
138+ CustomizeOp::GetPlugin => "Get a plugin",
139+ CustomizeOp::InstallPlugin => "Install a plugin",
140+ CustomizeOp::SetPluginEnabled => "Switch a plugin on or off",
141+ CustomizeOp::UninstallPlugin => "Uninstall a plugin",
142+ CustomizeOp::UpdatePlugin => "Update a plugin",
143+ CustomizeOp::ListPluginAgents => "List plugin agents",
144+ CustomizeOp::ListConnectors => "List connectors",
145+ CustomizeOp::GetConnector => "Get a connector",
146+ CustomizeOp::AddConnector => "Add a connector",
147+ CustomizeOp::SetConnectorKey => "Set a connector's key",
148+ CustomizeOp::RemoveConnector => "Remove a connector",
149+ CustomizeOp::RefreshConnector => "Refresh a connector's tools",
150+ CustomizeOp::GrantConnector => "Give a connector to agents",
151+ CustomizeOp::RevokeConnector => "Take a connector back",
152+ CustomizeOp::Bundle => "Get the runner bundle",
153+ }
154+ }
155+
156+ pub fn description(self) -> &'static str {
157+ match self {
158+ CustomizeOp::ListMarketplaces => "The marketplaces a workspace reads plugins from, in the open plugin format (`.claude-plugin/marketplace.json`): each with its `name` (the `@marketplace` part of a plugin's ref), `source` (`github` with `repo`, `g1t` with `repo`, `git` or `url` with `url`, or `builtin`), `builtin` (every workspace has it and it cannot be removed: claude-plugins-official, anthropic-agent-skills and g1t), `about`, `owner`, how many `plugins` it lists, `fetched_at` and `problem` (why its manifest could not be read, when it could not), who added it and when, and `can_remove`. Members only.",
159+ CustomizeOp::AddMarketplace => "Add a marketplace by `address`: `owner/repo` on GitHub, `workspace/name` for a repository on this g1t, a git address, or a link to a `marketplace.json`. `name` overrides the one its manifest gives. Its manifest is read at once; a marketplace that cannot be read is refused with `422`, saying why. At most 20 a workspace. Owners only. Recorded in the audit log.",
160+ CustomizeOp::RemoveMarketplace => "Remove a marketplace by its name (`marketplace`). Plugins installed from it stay installed, but get no updates and cannot be installed again until it is added back. Built-in marketplaces cannot be removed (`409`). Owners only. Recorded in the audit log.",
161+ CustomizeOp::RefreshMarketplace => "Read a marketplace's manifest again now, by its name (`marketplace`), and return it as list_marketplaces gives it. Marketplaces are refreshed on their own in the background; this is for seeing a change at once. Owners only.",
162+ CustomizeOp::Discover => "What a workspace can add, from every marketplace it reads: `plugins`, one per listing (`ref` as `name@marketplace`, `display_name`, `description`, `version`, `author`, `category`, `tags`, how g1t would `fetch` it, and `availability`: `available`, `installed` with `enabled`, `unavailable` with `why` for sources g1t cannot read such as npm or a command, or `soon` for g1t's own announced ones), `skills`, one per skill inside those plugins (`ref` as `plugin:skill`, with `added` when the library already has it), and the `marketplaces` themselves. Members only.",
163+ CustomizeOp::ListPlugins => "The plugins a workspace installed, switched off ones included: each with its `ref` (`name@marketplace`), `name`, `marketplace`, `display_name`, `description`, `version`, the commit it was read at (`sha`), how it is fetched (`fetch`), what it brought (`components`: skills, agents, commands, mcp_servers, and whether it carries hooks, lsp, bin or output_styles, which only Claude Code on a runner uses), `problems` met when installing, `enabled`, who installed it and when, and `update` when the marketplace lists a newer version. Members only.",
164+ CustomizeOp::GetPlugin => "One plugin's page by `ref` (`name@marketplace`, URL-encoded in a path: `@` as `%40`), installed or not: its marketplace `listing`, its `install` if any, its `components` and `readme` read from its source, its `manifest`, its subagents in full (`agents`) for hiring from, `problems`, and the marketplace it is listed in (`source`). Reads the plugin's files, so it takes a moment. Members only.",
165+ CustomizeOp::InstallPlugin => "Install a plugin by `ref` (`name@marketplace`) from a marketplace the workspace reads, at the commit its listing names, which it stays pinned to. Its skills and commands join the workspace's library as `plugin:skill`, its subagents become templates to hire from, and the remote MCP servers of its `.mcp.json` become connectors, unconnected until an owner signs in or gives a key; its hooks, LSP servers, output styles and binaries reach Claude Code on runners and never g1t's own agents. A plugin g1t cannot fetch (npm, a command, a git host g1t does not read) is refused with `422`. At most 100 a workspace. Owners only. Recorded in the audit log.",
166+ CustomizeOp::SetPluginEnabled => "Switch an installed plugin on or off by `ref`: `enabled` false keeps it installed but takes its skills, agents and connectors away from agents and leaves it out of runners' bundles; true brings them back. Owners only. Recorded in the audit log.",
167+ CustomizeOp::UninstallPlugin => "Uninstall a plugin by `ref`: its skills leave the library, its agent templates go, and the connectors it brought are removed with their secrets. Agents hired from its templates stay. Owners only. Recorded in the audit log.",
168+ CustomizeOp::UpdatePlugin => "Update an installed plugin by `ref`: read it again at the commit its marketplace now lists and pin it there. Its skills get new versions and keep their attachments, skills it no longer has are archived, its agent templates and components are rewritten, and a server it already brought keeps its connector; new remote servers become connectors. A plugin already at its marketplace's commit comes back unchanged. list_plugins shows `update` when there is one to take. Owners only. Recorded in the audit log.",
169+ CustomizeOp::ListPluginAgents => "The subagents installed plugins brought, as templates to hire an agent from: each with its `ref` (`plugin:agent`), `plugin`, `marketplace`, `name`, `description`, the system prompt as `prompt`, and its `model`, `skills` and `tools`. Members only.",
170+ CustomizeOp::ListConnectors => "A workspace's connectors: MCP servers connected once and given to agents. Each has its `id` (`con_…`), `name` (tools are offered to agents as `<name>__<tool>`), `title`, `url`, `category`, `auth` (`none`, `header` or `oauth`), `state` (`connected`, `needs_sign_in`, `needs_key` or `problem`, with `problem` saying why), `from` (the directory, a plugin's server, an address, or one agent's own server), its `tools` as last listed and `checked_at`, `header_name` for a key (the key itself is never returned), who it is `signed_in_as` for OAuth, its `grants` (where it reaches: every agent, a team or one agent), and who added it. g1t's own integrations (GitHub, trackers, alert sources, model providers) are list_integrations. Members only.",
171+ CustomizeOp::GetConnector => "One connector by `id`, in the shape list_connectors gives. Members only.",
172+ CustomizeOp::AddConnector => "Add a connector. `from` says where it comes from: `{\"kind\": \"directory\", \"id\"}` for one of the directory's remote MCP servers, `{\"kind\": \"plugin\", \"ref\", \"server\"}` for a server an installed plugin names, or `{\"kind\": \"address\", \"name\", \"url\"}` for any MCP server over HTTP (`name` is 2 to 32 lowercase letters, digits and hyphens). `auth` is `none`, `header` (give `header` with the header's `name` and `value`, kept sealed and never returned) or `oauth` (an owner then signs in on the site: sign-in needs a browser, so it has no route). Its tools are listed as soon as it can be reached. At most 50 a workspace. Owners only. Recorded in the audit log.",
173+ CustomizeOp::SetConnectorKey => "Put a key on a connector by `id`: `value` is the header's value, kept sealed and never returned, and `header_name` the header it is sent as (the one it had, or Authorization, when left out). Makes the connector use header auth, lists its tools again, and returns it. Owners only. Recorded in the audit log.",
174+ CustomizeOp::RemoveConnector => "Remove a connector by `id`, with its secrets and grants. Agents that had it lose its tools at their next turn. Owners only. Recorded in the audit log.",
175+ CustomizeOp::RefreshConnector => "Ask a connector's server for its tools again now, by `id`, and return the connector with its `tools`, `checked_at` and `state` as they are now. Owners only.",
176+ CustomizeOp::GrantConnector => "Give a connector by `id` to agents: `scope` is `workspace` (every agent), `team` (with `target`, the team's slug) or `agent` (with `target`, the agent's handle). Returns the connector with its `grants`. Owners only. Recorded in the audit log.",
177+ CustomizeOp::RevokeConnector => "Take a connector back from where one grant reaches, by the connector's `id` and the grant's id (`grant`). Returns the connector with its `grants`. Owners only. Recorded in the audit log.",
178+ CustomizeOp::Bundle => "What a job's Claude Code loads from the workspace's Customize, read once per job with the job's agent token (or a workspace's token; a person's token is refused): `plugins` to fetch at their pinned commit (`ref`, `name`, `fetch`, `version`; switched-off ones left out), the library `skills` that reach the job's agent as files (each `name` with its `files`: `path`, `content`, `encoding` utf8 or base64; a plugin's skills come with the plugin), and the `connectors` that reach it and are connected, each with its `name`, `url` and the `headers` that sign it in. Secrets are in it: it is for the runner, and g1t never keeps or logs it. See self-hosted runners, \"What a run gets from Customize\".",
179+ }
180+ }
181+
182+ pub fn input(self) -> Value {
183+ let workspace = json!({ "type": "string", "description": "The workspace's slug." });
184+ let marketplace = json!({ "type": "string", "description": "The marketplace's name, as list_marketplaces gives it." });
185+ let plugin_ref = json!({ "type": "string", "description": "The plugin as name@marketplace, such as commit-commands@claude-plugins-official." });
186+ let id = json!({ "type": "string", "description": "The connector's id (con_…), from list_connectors." });
187+ let (properties, required): (Value, &[&str]) = match self {
188+ CustomizeOp::ListMarketplaces
189+ | CustomizeOp::Discover
190+ | CustomizeOp::ListPlugins
191+ | CustomizeOp::ListPluginAgents
192+ | CustomizeOp::ListConnectors
193+ | CustomizeOp::Bundle => (json!({ "workspace": workspace }), &["workspace"]),
194+ CustomizeOp::AddMarketplace => (
195+ json!({
196+ "workspace": workspace,
197+ "address": { "type": "string", "description": "owner/repo on GitHub, workspace/name for a repository here, a git address, or a link to a marketplace.json." },
198+ "name": { "type": "string", "description": "What to call it here. The name its manifest gives when left out." },
199+ }),
200+ &["workspace", "address"],
201+ ),
202+ CustomizeOp::RemoveMarketplace | CustomizeOp::RefreshMarketplace => {
203+ (json!({ "workspace": workspace, "marketplace": marketplace }), &["workspace", "marketplace"])
204+ }
205+ CustomizeOp::GetPlugin | CustomizeOp::InstallPlugin | CustomizeOp::UninstallPlugin | CustomizeOp::UpdatePlugin => {
206+ (json!({ "workspace": workspace, "ref": plugin_ref }), &["workspace", "ref"])
207+ }
208+ CustomizeOp::SetPluginEnabled => (
209+ json!({
210+ "workspace": workspace,
211+ "ref": plugin_ref,
212+ "enabled": { "type": "boolean", "description": "True to switch it on, false to switch it off." },
213+ }),
214+ &["workspace", "ref", "enabled"],
215+ ),
216+ CustomizeOp::GetConnector | CustomizeOp::RemoveConnector | CustomizeOp::RefreshConnector => (json!({ "workspace": workspace, "id": id }), &["workspace", "id"]),
217+ CustomizeOp::AddConnector => (
218+ json!({
219+ "workspace": workspace,
220+ "from": {
221+ "type": "object",
222+ "description": "Where it comes from: {\"kind\": \"directory\", \"id\"}, {\"kind\": \"plugin\", \"ref\", \"server\"} or {\"kind\": \"address\", \"name\", \"url\"}.",
223+ "properties": {
224+ "kind": { "type": "string", "enum": ["directory", "plugin", "address"] },
225+ "id": { "type": "string", "description": "For directory: the directory entry's id." },
226+ "ref": { "type": "string", "description": "For plugin: the installed plugin, as name@marketplace." },
227+ "server": { "type": "string", "description": "For plugin: the server's name in its .mcp.json." },
228+ "name": { "type": "string", "description": "For address: 2 to 32 lowercase letters, digits and hyphens." },
229+ "url": { "type": "string", "description": "For address: the MCP server's https address." },
230+ },
231+ "required": ["kind"],
232+ },
233+ "auth": { "type": "string", "enum": ["none", "header", "oauth"], "description": "How it signs in. none when left out." },
234+ "header": {
235+ "type": ["object", "null"],
236+ "description": "For header auth: the header's name and value. The value is kept sealed and never returned.",
237+ "properties": {
238+ "name": { "type": "string" },
239+ "value": { "type": "string" },
240+ },
241+ "required": ["name", "value"],
242+ },
243+ }),
244+ &["workspace", "from"],
245+ ),
246+ CustomizeOp::SetConnectorKey => (
247+ json!({
248+ "workspace": workspace,
249+ "id": id,
250+ "header_name": { "type": "string", "description": "The header the key is sent as. The one it had, or Authorization, when left out." },
251+ "value": { "type": "string", "description": "The key. Kept sealed; never returned." },
252+ }),
253+ &["workspace", "id", "value"],
254+ ),
255+ CustomizeOp::GrantConnector => (
256+ json!({
257+ "workspace": workspace,
258+ "id": id,
259+ "scope": { "type": "string", "enum": ["workspace", "team", "agent"], "description": "Every agent, a team's agents, or one agent." },
260+ "target": { "type": "string", "description": "The team's slug or the agent's handle; nothing for workspace." },
261+ }),
262+ &["workspace", "id", "scope"],
263+ ),
264+ CustomizeOp::RevokeConnector => (
265+ json!({
266+ "workspace": workspace,
267+ "id": id,
268+ "grant": { "type": "string", "description": "The grant's id, from the connector's grants." },
269+ }),
270+ &["workspace", "id", "grant"],
271+ ),
272+ };
273+ json!({ "type": "object", "properties": properties, "required": required })
274+ }
275+}
276+
277+fn text(input: &Value, key: &str) -> String {
278+ input[key].as_str().map(str::trim).unwrap_or_default().to_owned()
279+}
280+
281+/// A boolean as sent: JSON, or a word from a form.
282+fn flag(value: &Value) -> Option<bool> {
283+ match value {
284+ Value::Bool(flag) => Some(*flag),
285+ Value::String(word) => match word.trim().to_ascii_lowercase().as_str() {
286+ "true" | "1" => Some(true),
287+ "false" | "0" => Some(false),
288+ _ => None,
289+ },
290+ _ => None,
291+ }
292+}
293+
294+/// What an operation asks the agents service beyond the workspace and the
295+/// viewer, read from the input, or why the input will not do.
296+fn arguments(op: CustomizeOp, input: &Value) -> std::result::Result<Value, String> {
297+ let mut args = json!({});
298+ let need = |key: &str, what: &str| {
299+ let value = text(input, key);
300+ if value.is_empty() {
301+ Err(what.to_owned())
302+ } else {
303+ Ok(value)
304+ }
305+ };
306+ match op {
307+ CustomizeOp::ListMarketplaces
308+ | CustomizeOp::Discover
309+ | CustomizeOp::ListPlugins
310+ | CustomizeOp::ListPluginAgents
311+ | CustomizeOp::ListConnectors
312+ | CustomizeOp::Bundle => {}
313+ CustomizeOp::AddMarketplace => {
314+ args["address"] = json!(need("address", "Give address: owner/repo, workspace/name, a git address or a link to marketplace.json.")?);
315+ if let Some(name) = input["name"].as_str().map(str::trim).filter(|name| !name.is_empty()) {
316+ args["name"] = json!(name);
317+ }
318+ }
319+ CustomizeOp::RemoveMarketplace | CustomizeOp::RefreshMarketplace => {
320+ args["name"] = json!(need("marketplace", "Name the marketplace.")?);
321+ }
322+ CustomizeOp::GetPlugin | CustomizeOp::InstallPlugin | CustomizeOp::UninstallPlugin | CustomizeOp::UpdatePlugin => {
323+ args["ref"] = json!(need("ref", "Name the plugin as name@marketplace.")?);
324+ }
325+ CustomizeOp::SetPluginEnabled => {
326+ args["ref"] = json!(need("ref", "Name the plugin as name@marketplace.")?);
327+ args["enabled"] = json!(flag(&input["enabled"]).ok_or("enabled is true or false.")?);
328+ }
329+ CustomizeOp::GetConnector | CustomizeOp::RemoveConnector | CustomizeOp::RefreshConnector => {
330+ args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
331+ }
332+ CustomizeOp::AddConnector => {
333+ let from = &input["from"];
334+ if !matches!(from["kind"].as_str(), Some("directory" | "plugin" | "address")) {
335+ return Err("from.kind is directory, plugin or address.".to_owned());
336+ }
337+ let auth = match input["auth"].as_str().map(str::trim) {
338+ None | Some("") | Some("none") => "none",
339+ Some(auth @ ("header" | "oauth")) => auth,
340+ Some(_) => return Err("auth is none, header or oauth.".to_owned()),
341+ };
342+ let header = match &input["header"] {
343+ Value::Null => Value::Null,
344+ header if header["name"].is_string() && header["value"].is_string() => {
345+ json!({ "name": header["name"], "value": header["value"] })
346+ }
347+ _ => return Err("header has a name and a value.".to_owned()),
348+ };
349+ if auth == "header" && header.is_null() {
350+ return Err("Header auth needs header: its name and value.".to_owned());
351+ }
352+ args["input"] = json!({ "from": from, "auth": auth, "header": header });
353+ }
354+ CustomizeOp::SetConnectorKey => {
355+ args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
356+ args["value"] = json!(need("value", "Give value: the key.")?);
357+ if let Some(name) = input["header_name"].as_str().map(str::trim).filter(|name| !name.is_empty()) {
358+ args["header_name"] = json!(name);
359+ }
360+ }
361+ CustomizeOp::GrantConnector => {
362+ args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
363+ let scope = text(input, "scope");
364+ if !matches!(scope.as_str(), "workspace" | "team" | "agent") {
365+ return Err("scope is workspace, team or agent.".to_owned());
366+ }
367+ if scope != "workspace" {
368+ args["target"] = json!(need("target", "Give target: the team's slug or the agent's handle.")?);
369+ }
370+ args["scope"] = json!(scope);
371+ }
372+ CustomizeOp::RevokeConnector => {
373+ args["id"] = json!(need("id", "Name the connector by its id (con_…).")?);
374+ args["grant"] = json!(need("grant", "Name the grant by its id.")?);
375+ }
376+ }
377+ Ok(args)
378+}
379+
380+pub async fn run(op: CustomizeOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
381+ let workspace = text(input, "workspace").to_lowercase();
382+ if workspace.is_empty() {
383+ return Ok(Outcome::fail(FailureCode::Invalid, "Name the workspace."));
384+ }
385+ let actor = viewer.clone().unwrap_or_default();
386+ // The bundle carries connectors' secrets: only a run's own token, or
387+ // the workspace's, reads it, never a person's. The agents service
388+ // checks this too.
389+ if op == CustomizeOp::Bundle && actor_kind(&actor) == "person" {
390+ return Ok(Outcome::fail(
391+ FailureCode::Forbidden,
392+ "The bundle is read with a job's agent token or a workspace's token, not a person's.",
393+ ));
394+ }
395+ let mut args = match arguments(op, input) {
396+ Ok(args) => args,
397+ Err(why) => return Ok(Outcome::fail(FailureCode::Invalid, &why)),
398+ };
399+ args["workspace"] = json!(workspace);
400+ args["viewer"] = json!(actor);
401+ g1t_kit::call(&services.agents, op.method(), &args).await
402+}
403+
404+#[cfg(test)]
405+mod tests {
406+ use super::*;
407+ use g1t_contracts::scopes::{Level, scope_for};
408+
409+ #[test]
410+ fn each_operation_is_described_and_scoped() {
411+ for op in CustomizeOp::ALL {
412+ assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Customize(op)), "{}", op.name());
413+ assert!(!op.title().is_empty() && op.description().len() > 40, "{}", op.name());
414+ assert!(op.input()["required"].as_array().unwrap().contains(&json!("workspace")), "{}", op.name());
415+ let scope = scope_for(op.name()).unwrap();
416+ let changes = !matches!(
417+ op,
418+ CustomizeOp::ListMarketplaces
419+ | CustomizeOp::Discover
420+ | CustomizeOp::ListPlugins
421+ | CustomizeOp::GetPlugin
422+ | CustomizeOp::ListPluginAgents
423+ | CustomizeOp::ListConnectors
424+ | CustomizeOp::GetConnector
425+ | CustomizeOp::Bundle
426+ );
427+ let expected = match op {
428+ CustomizeOp::Bundle => Level::Run,
429+ _ if changes => Level::Admin,
430+ _ => Level::Read,
431+ };
432+ assert_eq!(scope.level(), expected, "{}", op.name());
433+ }
434+ assert_eq!(scope_for("customize_bundle").unwrap().as_str(), "agents:run");
435+ }
436+
437+ #[test]
438+ fn inputs_are_checked_before_the_call() {
439+ let bad = |op, input: Value| arguments(op, &input).unwrap_err();
440+ assert!(bad(CustomizeOp::AddMarketplace, json!({})).starts_with("Give address"));
441+ assert!(bad(CustomizeOp::SetPluginEnabled, json!({ "ref": "a@b" })).contains("enabled"));
442+ assert!(bad(CustomizeOp::AddConnector, json!({ "from": { "kind": "elsewhere" } })).contains("from.kind"));
443+ assert!(bad(CustomizeOp::AddConnector, json!({ "from": { "kind": "address", "name": "x", "url": "https://x" }, "auth": "header" })).contains("header"));
444+ assert!(bad(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "team" })).starts_with("Give target"));
445+ assert!(bad(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "everyone" })).contains("scope"));
446+
447+ let ok = |op, input: Value| arguments(op, &input).unwrap();
448+ assert_eq!(ok(CustomizeOp::SetPluginEnabled, json!({ "ref": "a@b", "enabled": "false" })), json!({ "ref": "a@b", "enabled": false }));
449+ assert_eq!(ok(CustomizeOp::GrantConnector, json!({ "id": "con_1", "scope": "workspace", "target": "ignored" })), json!({ "id": "con_1", "scope": "workspace" }));
450+ assert_eq!(
451+ ok(CustomizeOp::AddConnector, json!({ "from": { "kind": "directory", "id": "linear" } })),
452+ json!({ "input": { "from": { "kind": "directory", "id": "linear" }, "auth": "none", "header": null } })
453+ );
454+ assert_eq!(
455+ ok(CustomizeOp::SetConnectorKey, json!({ "id": "con_1", "value": "k", "header_name": " X-Api-Key " })),
456+ json!({ "id": "con_1", "value": "k", "header_name": "X-Api-Key" })
457+ );
458+ assert_eq!(ok(CustomizeOp::GetPlugin, json!({ "ref": "commit-commands@claude-plugins-official" }))["ref"], "commit-commands@claude-plugins-official");
459+ // The path names a marketplace as `marketplace`; the agents service as `name`.
460+ assert_eq!(ok(CustomizeOp::RemoveMarketplace, json!({ "marketplace": "flagon-tools" })), json!({ "name": "flagon-tools" }));
461+ assert!(bad(CustomizeOp::RefreshMarketplace, json!({ "name": "flagon-tools" })).starts_with("Name the marketplace"));
462+ }
463+}
+1−0
1212 mod billing;
1313 mod blobs;
1414 mod checks;
15+mod customize;
1516 mod deployments;
1617 mod deploy_keys;
1718 mod limits;
+1−1
1111 const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
1212
1313 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.
1515 Find a repository: account whoami lists your workspaces; repository list or search finds one.
1616 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.
1717 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.
+29−0
99
1010 use crate::about::AboutOp;
1111 use crate::run_artifacts::ArtifactsOp;
12+use crate::customize::CustomizeOp;
1213 use crate::deploy_keys::DeployKeysOp;
1314 use crate::mirrors::MirrorsOp;
1415 use crate::deployments::DeploymentsOp;
549550 ],
550551 ),
551552 (
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+ (
552580 "Artifacts",
553581 "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.",
554582 &[
793821 Op::Mirrors(op) => op.title(),
794822 Op::Packages(op) => op.title(),
795823 Op::Folios(op) => op.title(),
824+ Op::Customize(op) => op.title(),
796825 }
797826 }
798827
+35−2
2929 use crate::checks::ChecksOp;
3030 use crate::about::AboutOp;
3131 use crate::run_artifacts::ArtifactsOp;
32+use crate::customize::CustomizeOp;
3233 use crate::deploy_keys::DeployKeysOp;
3334 use crate::mirrors::MirrorsOp;
3435 use crate::deployments::DeploymentsOp;
338339 /// Artifacts mode's docs, slides, designs and dashboards, kept by the
339340 /// artifacts service: folios.rs.
340341 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),
341345 }
342346
343347 fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
707711 }
708712
709713 impl Op {
710− pub const ALL: [Op; 350] = [
714+ pub const ALL: [Op; 371] = [
711715 Op::Whoami,
712716 Op::GetWorkspace,
713717 Op::CreateWorkspace,
994998 Op::DeployKeys(DeployKeysOp::GetDeployKey),
995999 Op::DeployKeys(DeployKeysOp::CreateDeployKey),
9961000 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),
9971022 Op::Mirrors(MirrorsOp::GetMirror),
9981023 Op::Mirrors(MirrorsOp::GetHandBackPlan),
9991024 Op::Mirrors(MirrorsOp::TakeOver),
12761301 Op::Mirrors(op) => op.name(),
12771302 Op::Packages(op) => op.name(),
12781303 Op::Folios(op) => op.name(),
1304+ Op::Customize(op) => op.name(),
12791305 }
12801306 }
12811307
18481874 Op::Mirrors(op) => op.description(),
18491875 Op::Packages(op) => op.description(),
18501876 Op::Folios(op) => op.description(),
1877+ Op::Customize(op) => op.description(),
18511878 }
18521879 }
18531880
33413368 Op::Mirrors(op) => op.input(),
33423369 Op::Packages(op) => op.input(),
33433370 Op::Folios(op) => op.input(),
3371+ Op::Customize(op) => op.input(),
33443372 }
33453373 }
33463374
34193447 if let Op::Folios(_) = self {
34203448 return false;
34213449 }
3450+ // So does its Customize.
3451+ if let Op::Customize(_) = self {
3452+ return false;
3453+ }
34223454 if let Op::About(op) = self {
34233455 return op.needs_repo();
34243456 }
46934725 }
46944726 }
46954727 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()?;
46974729 if wanted.as_deref() == Some("open") && status == Some(PullStatus::Closed) {
46984730 match pass(work, "reopen_pull", &pull_action()).await? {
46994731 Outcome::Ok(pull) => answer = Some(pull),
56965728 Op::Mirrors(op) => crate::mirrors::run(op, services, viewer, input).await,
56975729 Op::Packages(op) => crate::packages::run(op, services, viewer, input).await,
56985730 Op::Folios(op) => crate::folios::run(op, services, viewer, input).await,
5731+ Op::Customize(op) => crate::customize::run(op, services, viewer, input).await,
56995732 Op::ReopenSecurityAlert => {
57005733 let changed: Outcome<AlertChange> = call(
57015734 &services.security,
+968−0
43504350 },
43514351 "notes": "Credentials that do not work still answer `200`, with `ok` false and `message` saying what went wrong."
43524352 },
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+ },
43535321 "get_model_routes": {
43545322 "params": {
43555323 "workspace": "flagon-io"
+29−1
44
55 use crate::about::AboutOp;
66 use crate::run_artifacts::ArtifactsOp;
7+use crate::customize::CustomizeOp;
78 use crate::deploy_keys::DeployKeysOp;
89 use crate::mirrors::MirrorsOp;
910 use crate::deployments::DeploymentsOp;
649650 Op::TestIntegration,
650651 &[],
651652 ),
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), &[]),
652676 route(
653677 "GET",
654678 "/repos/:owner/:name/actions/workflows",
9931017 /// Whether the route answers success with `204` and no body, as
9941018 /// GitHub's address for the same does. MCP still answers `true`.
9951019 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+ )
9971025 }
9981026
9991027 /// The names of the route's path parameters, in order.
+32−1
2020
2121 use crate::about::AboutOp;
2222 use crate::run_artifacts::ArtifactsOp;
23+use crate::customize::CustomizeOp;
2324 use crate::deploy_keys::DeployKeysOp;
2425 use crate::mirrors::MirrorsOp;
2526 use crate::deployments::DeploymentsOp;
432433 ],
433434 },
434435 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 {
435465 name: "billing",
436466 title: "Billing",
437467 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.",
593623 | Op::DeleteRunnerGroup
594624 | Op::UpdateRunnerSettings
595625 | Op::Folios(FoliosOp::SetAccess | FoliosOp::Purge)
626+ | Op::Customize(CustomizeOp::RemoveMarketplace | CustomizeOp::UninstallPlugin | CustomizeOp::RemoveConnector)
596627 )
597628 }
598629
843874 assert!(tool.action(default).is_some(), "{}", tool.name);
844875 }
845876 }
846− assert!(TOOLS.len() <= 19, "{} tools", TOOLS.len());
877+ assert!(TOOLS.len() <= 20, "{} tools", TOOLS.len());
847878 }
848879
849880 #[test]
+7−5
487487 writes the plugins to disk and starts Claude Code with each as a plugin
488488 directory, puts the skills where it reads them, and merges the connectors
489489 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).
491491
492492 Chat replies and [sessions](/guides/agent-sessions/) on g1t get the same
493493 things in their own way: skills through `use_skill`, connectors as
497497
498498 Customize is in the REST API under `/workspaces/{workspace}/customize/…`
499499 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,
504506 `GET /workspaces/{workspace}/customize/bundle`, needs `agents:run` and is
505507 read with a job's agent token, since it carries the connectors' sign-ins.
506508 See the [MCP tools](/reference/mcp/) and the [API reference](/reference/api/).
+35−17
203203 that credential, so budgets, caps and the audit log work as before. With
204204 your own [model provider](/guides/models/), g1t charges nothing for the
205205 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.
210210 - [Guardrails](/guides/guardrails/) still apply to what the agent does, but
211211 their network list cannot be enforced on your machine. The run's session
212212 says so when it starts.
213213
214214 A project can have its own setting, or follow the workspace's.
215215
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>` |
217229
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.
222235
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.
228242
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).
232249
233250 ## Billing
234251
353370 | Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
354371 | 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` |
355372 | 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 |
356374
357375 The [Agent preset](/guides/authentication/#scopes) does not include
358376 `runners:read`.
+16−0
616616 model calls still go through g1t; the machine time is free. g1t's network
617617 guardrails cannot be enforced on your machines, and the run says so.
618618
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+
619635 ## Who can run agents
620636
621637 Putting an agent to work (assigning it, mentioning it, asking it for a
+1−0
242242 | [Secrets and variables](/reference/api/secrets-and-variables/list-actions-secrets/) | Values workflows and deployments read. |
243243 | [Webhooks](/reference/api/webhooks/list-webhooks/) | Events sent to your own address. |
244244 | [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/). |
+42−7
33 description: The g1t MCP server's resource tools, each action they take with its required inputs and scope, and how to call them.
44 ---
55
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
77 thing on g1t: `search`, `repository`, `issue`, `pull_request`, `agent`,
88 `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
1010 action is the same operation as a route of the [REST API](/reference/api/),
1111 with the same inputs, permissions and results, so the two always agree.
1212
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−
1513 ## Connect
1614
1715 To connect Claude Code, Codex, OpenCode, Cursor or another client, see
171169 | --- | --- |
172170 | `title` | The tool's name for people, such as `Pull requests`. |
173171 | `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. |
175173 | `idempotentHint` | The same as `readOnlyHint`. |
176174 | `openWorldHint` | Always `false`. |
177175
699697 | [`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` |
700698 | [`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` |
701699
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+
702737 ## `billing`
703738
704739 A workspace's billing: its usage, its budget, its AI credit, its
817852
818853 | Run | Actions |
819854 | --- | --- |
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`. |
821856 | Review | The same reading actions, and `issue` `comment`, `pull_request` `review` and `search` `ticket`. |
822857 | Plan | The same reading actions, and `issue` `create` and `search` `ticket`. |
823858 | Catch up | The reading actions only. |
824859
825860 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`
827862 `assign` and `delegate`, `plan` `create` and `apply`, `issue` `import`, or
828863 any `repository` action that creates, changes, renames, archives,
829864 transfers, deletes, restores or purges a repository, or dismisses or
+0−0

Binary or large file; its contents are not shown.

+7−3
549549 "message_agent",
550550 "answer_message",
551551 "get_context",
552+ "customize_bundle",
552553 ]);
553554 }
554555 K::Review => {
555556 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"]);
557558 }
558559 K::Plan => {
559560 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");
561566 }
562− K::Update => operations.extend(TOOLS_READ.iter().copied()),
563567 K::Checks | K::Queue | K::Mergecheck | K::Deploy | K::Bump => {}
564568 },
565569 }
+24−0
935935 ("test_integration", Scope::WorkspaceAdmin),
936936 ("get_model_routes", Scope::WorkspaceRead),
937937 ("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),
938962 // Teams: reading them, and managing them. A team's role on a
939963 // repository is who has access.
940964 ("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.