Skip to content

Commit

Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.

syntaqxcommitted Parents30d794f68fa4b9Browse files
171 files+2205−500/171 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]
+2−1
1313 redirects: {
1414 '/guides/g1t-agents/': '/guides/working-with-g1t/',
1515 '/guides/inbox/': '/guides/notifications/',
16+ '/guides/marketplace/': '/guides/customize/',
1617 '/guides/today/': '/guides/home/',
1718 },
1819 integrations: [
149150 {
150151 label: 'Connect your tools',
151152 items: [
152− { label: 'Marketplace', slug: 'guides/marketplace' },
153+ { label: 'Customize', slug: 'guides/customize' },
153154 { label: 'Integrations', slug: 'guides/integrations' },
154155 { label: 'Model providers', slug: 'guides/models' },
155156 { label: 'AI Gateway', slug: 'guides/ai-gateway' },
+33−17
11 ---
22 title: Agent abilities
3−description: What an agent may do, in groups - g1t's own, its computer, each connected integration and MCP servers you add - and for each whether it acts alone, only when asked for it, after asking first, or never; whose connection it uses; and the cards it posts when it can't.
3+description: What an agent may do, in groups - g1t's own, its computer, each connected integration, and the connectors and MCP servers it has - and for each whether it acts alone, only when asked for it, after asking first, or never; whose connection it uses; and the cards it posts when it can't.
44 ---
55
66 Every agent has an **Abilities** tab on its page, after **Skills**. It
102102
103103 An integration that gives agents abilities but isn't connected is listed
104104 too, with **Connect** for owners and **Ask an owner** for anyone else,
105−which opens its [Marketplace](/guides/marketplace/) page. Nothing on it
106−can be set until it is connected.
105+which opens it in [Customize](/guides/customize/#connectors). Nothing on
106+it can be set until it is connected.
107107
108108 #### Whose connection
109109
119119 Sentry can't be connected per person yet, so for them the choice is the
120120 workspace's connection today.
121121
122−### MCP servers
122+### Connectors
123+
124+MCP servers reach an agent in two ways, and the tab lists both under
125+**Connectors**:
123126
124−Owners add MCP servers to an agent, one at a time, by a name and an HTTPS
125−address on a public host. g1t lists the server's tools when it is added,
126−and each tool becomes a row:
127+- **The workspace's connectors**, connected once in
128+ [Customize](/guides/customize/#connectors) and given to every agent, a
129+ team or this agent. Each row says how the agent has it (**Every agent**,
130+ **Team QA**, **This agent**) and its state. A connector that needs a
131+ sign-in or a key, or has a problem, gives the agent nothing until an
132+ owner fixes it there. Servers that need a key or a browser sign-in are
133+ connected this way.
134+- **Only this agent**: servers owners add to this agent alone, one at a
135+ time, by a name and an HTTPS address on a public host, with no sign-in.
136+ An agent keeps at most 10.
137+
138+g1t lists a server's tools when it is connected or added, keeps the first
139+40, and each tool becomes a row:
127140
128141 - a tool the server marks as read-only is a **read**, Alone by default;
129142 - any other tool, including one the server says nothing about, counts as
130143 a **write outside g1t**, Ask first by default.
131144
132145 Tools are offered to the agent as `<server>__<tool>`, with the arguments
133−the server describes. **List its tools again** picks up a server that
134−changed; **Remove** takes the server and its rows off the agent at once.
135−Both are new versions of the agent. An agent keeps at most 10 servers,
136−and each server's first 40 tools. Servers that need a key or a sign-in
137−aren't supported yet.
146+the server describes. For a server added to this agent alone, **List its
147+tools again** picks up a server that changed and **Remove** takes the
148+server and its rows off the agent at once; both are new versions of the
149+agent. A workspace connector is listed again, signed in, given and taken
150+back on its own page in Customize, and the tab links there.
138151
139152 ## Ask first, in the conversation
140153
158171 the agent doesn't have, the agent says so plainly and posts a **Request**
159172 card:
160173
161−- for an integration, **Ask the owners** opens the same request the
162− Marketplace does (see [ask an owner to add something](/guides/marketplace/#ask-an-owner-to-add-something)),
163− and **Open in Marketplace** goes to its page;
174+- for an integration or a connector, **Ask the owners** opens the same
175+ request Customize does (see [requests](/guides/customize/#requests)),
176+ and **Open in Customize** goes to its page;
164177 - for an ability, **Ask the owners** notifies every owner with a link to
165178 the agent's Abilities tab, and **Open Abilities** goes there.
166179
195208 with `abilities: { settings }` replaces its settings, and is a new
196209 version like any change. MCP servers are added and removed with the
197210 agents service's `add_mcp_server`, `refresh_mcp_server` and
198−`remove_mcp_server`; they never travel with a change.
211+`remove_mcp_server`; they never travel with a change. The workspace's
212+connectors and who gets them are the [`customize`](/guides/customize/#api)
213+resource's; which ones reach an agent is read with the agents service's
214+`agent_connectors`.
199215
200216 The computer itself has routes of its own under the agent:
201217 `GET /workspaces/{workspace}/agents/{agent}/computer` for its state and
208224
209225 - [Agents](/guides/agents/): hiring, changing and running agents.
210226 - [Agent skills](/guides/agent-skills/): how an agent does a kind of work.
211−- [Marketplace](/guides/marketplace/): connecting integrations, and asking an owner to.
227+- [Customize](/guides/customize/): connecting connectors and integrations, and asking an owner to.
212228 - [What agents can do for whom](/guides/agent-access/): the access every ability runs within.
+26−8
151151
152152 ## The skill library
153153
154−Your workspace's own skills are under **Agents → Skills**. The library
155−lists drafts waiting for review first, then every skill with its version
156−and where it is attached, then g1t's foundational skills, which you can
157−open to read as SKILL.md.
154+Your workspace's own skills are under **Agents → Skills**, and the same
155+library is **Customize → Skills → Yours** (see
156+[Customize](/guides/customize/#skills), which also lists every skill the
157+workspace's marketplaces publish). The library lists drafts waiting for
158+review first, then every skill with its version and where it is attached,
159+then the skills installed plugins brought, then g1t's foundational skills,
160+which you can open to read as SKILL.md.
158161
159162 A skill does nothing until it is attached. Open it and choose **Attach**:
160163
182185
183186 Every change is in the workspace's audit log under `agents/skills/<name>`.
184187
188+### Skills from plugins
189+
190+A [plugin](/guides/customize/#plugins) installed from Customize brings its
191+skills into the library, each named `plugin:skill` after the plugin
192+(`pdf@anthropic-agent-skills` brings `pdf:pdf`). They are attached like
193+any skill, and read-only here: a plugin's skill changes when the plugin is
194+reinstalled at a newer commit, and leaves with the plugin. Agents call one
195+with `use_skill` by its full name, or by its bare name when only one plugin
196+brings a skill called that; a plain name means the workspace's own skill
197+first. A plugin's skill may be larger than one written here (1.5 MB and
198+1,000 files), keeps its name's case, and may reuse a foundational skill's
199+name, since it is always called with its prefix. See
200+[skills from plugins](/guides/customize/#skills-from-plugins).
201+
185202 ## The skill format
186203
187204 A skill is a folder in the open SKILL.md format, the same one other agent
324341 1. Open **Agents → Skills**. Under **Keep skills in a repository**, enter
325342 the repository as `workspace/name` and choose **Link repository**.
326343 Owners only.
327−2. Each folder in `.g1t/skills/<name>/` on its default branch becomes the
328− skill `<name>`. The folder's name and the `name` in its `SKILL.md` must
329− match.
344+2. Each folder in `.claude/skills/<name>/` on its default branch becomes
345+ the skill `<name>`: the folder Claude Code reads project skills from, so
346+ one repository serves both. A repository that keeps them in
347+ `.g1t/skills/` is still read, when it has no `.claude/skills/`. The
348+ folder's name and the `name` in its `SKILL.md` must match.
330349 3. Every push to the default branch that changes a folder publishes a new
331350 version of that skill and moves all its attachments: the repository's
332351 review is the review. **Read it again** reads it on demand.
367386
368387 | What | Status |
369388 | --- | --- |
370−| Skills from the [Marketplace](/guides/marketplace/): an extension's skills, added in one step | Coming |
371389 | Running a skill's scripts on the agent's own computer | Coming |
372390 | Writing library edits back to the linked repository | Coming |
+23−11
100100 | In the draft | What it means |
101101 | --- | --- |
102102 | **Skills** | The [foundational skills](/guides/agent-skills/) its job needs are marked **Suggested** and on; the rest are off. Turn any on or off. |
103−| **What it needs connected** | Integrations its job would use, with why. Owners choose **Connect**; anyone else **Ask an owner**, which opens the integration's [Marketplace](/guides/marketplace/) page. **Soon** means nobody can connect it yet. The agent works without them and says so when asked for something that needs one. |
103+| **What it needs connected** | Integrations its job would use, with why. Owners choose **Connect**; anyone else **Ask an owner**, which opens it in [Customize](/guides/customize/#connectors). **Soon** means nobody can connect it yet. The agent works without them and says so when asked for something that needs one. |
104104 | **Routines that would suit it** | Set them up on its **Routines** tab once it exists. Personal agents can't run routines yet. |
105105 | **Models and budget** | A personal agent starts with **$20 a month** and **$2 a session**. A workspace agent starts with the workspace's budget for a new agent. |
106106
259259 high-volume intake and summaries never run on the most capable one. Change
260260 either if your team works differently.
261261
262+### From plugins
263+
264+A [plugin](/guides/customize/#plugins) installed from Customize can bring
265+subagents (`agents/<name>.md`), and **Templates** lists each under **From
266+plugins**, as `plugin:name`, with the plugin it came from. Hiring from one
267+works like any template: the file's `name`, `description` and `model`
268+fill the agent's name, description and model, and its body becomes the
269+agent's instructions, all of which you can change before you create it.
270+The agent is yours once it is created, and keeps working if the plugin is
271+switched off or uninstalled; the template leaves with the plugin.
272+
262273 ## Title and responsibilities
263274
264275 A role is broad on purpose. You hire Margo into QA, not into "review pull
794805 | Edit docs | Alone, or as a suggestion | As a suggestion |
795806
796807 These four are also on the agent's **Abilities** tab, beside everything
797−else it may do: each connected integration's actions, MCP servers an owner
798−adds, and whose connection each runs on, with four levels (alone, alone
799−when asked for it, ask first, never). See
808+else it may do: each connected integration's actions, the connectors it
809+has and MCP servers an owner adds, and whose connection each runs on, with
810+four levels (alone, alone when asked for it, ask first, never). See
800811 [agent abilities](/guides/agent-abilities/).
801812
802813 An agent also never does more for someone than that person could do
858869 | --- | --- |
859870 | **Sessions** | Every session it is working on, then every one it worked on before, each child under the session that started it. See [sessions](/guides/agent-sessions/). |
860871 | **Skills** | The [skills](/guides/agent-skills/) it has: g1t's foundational ones and your library's, each on or off. |
861−| **Abilities** | What it may do, in groups, with a level for each: alone, alone when asked for it, ask first, or never; whose connection an integration runs on; MCP servers owners add. See [agent abilities](/guides/agent-abilities/). |
872+| **Abilities** | What it may do, in groups, with a level for each: alone, alone when asked for it, ask first, or never; whose connection an integration runs on; the connectors and MCP servers it has. See [agent abilities](/guides/agent-abilities/). |
862873 | **Computer** | [Its computer](#its-computer): asleep or awake and since when, its disk against the 5 GB it holds, where it runs, and the commands it ran most recently with their output. Owners (or the member, for a personal agent) wake it, put it to sleep, or reset it. |
863874 | **Memory** | What it remembers, by scope, with where each fact came from. Pin, correct or forget facts. See [agent memory](/guides/agent-memory/). |
864875 | **Routines** | Work it does on a schedule or when something happens. See [routines](/guides/agent-routines/). |
943954 and agree in a visible thread when their work would overlap.
944955 <Soon />
945956 </Card>
946− <Card title="Skills and webhooks" icon="calendar-clock">
947− Skills you write or add from the Marketplace, and webhooks that wake an
948− agent. Every agent already has g1t's
949− [foundational skills](/guides/agent-skills/), and schedules and
950− workspace events already run as [routines](/guides/agent-routines/).
957+ <Card title="Webhooks" icon="calendar-clock">
958+ Webhooks that wake an agent. Skills are already here: every agent has
959+ g1t's [foundational skills](/guides/agent-skills/), your library and
960+ [Customize](/guides/customize/) add more, and schedules and workspace
961+ events already run as [routines](/guides/agent-routines/).
951962 <Soon />
952963 </Card>
953964 </CardGrid>
956967
957968 - [Chat](/guides/chat/): channels, DMs, threads and mentions.
958969 - [Agent skills](/guides/agent-skills/): what every agent can make and do, and what is coming.
959−- [Agent abilities](/guides/agent-abilities/): what it may do through integrations and MCP servers, and how freely.
970+- [Agent abilities](/guides/agent-abilities/): what it may do through integrations and connectors, and how freely.
971+- [Customize](/guides/customize/): skills, connectors and plugins for the workspace's agents.
960972 - [What agents can do for whom](/guides/agent-access/): access, audiences
961973 and requests from people who don't work on code.
962974 - [Model providers](/guides/models/): connect your own.
+35−8
6767 | **Shared** | The highest artifact of each tree someone shared with you. |
6868 | **Possibly out of date** | Docs whose cited code changed. Shown when there are any. |
6969 | **Projects' docs** | Repositories' `docs/` folders, read-only. |
70−| **Trash** | What was moved to the trash. |
70+| **Trash** | What was moved to the trash, with how many. |
71+
72+The sidebar follows every change at once: an artifact you move to the trash
73+leaves it, one you restore comes back, and the count beside **Trash** moves
74+with them, without reloading the page.
7175
7276 Drag a row onto a doc to put it inside, or onto the top edge of a row to put
7377 it before it. Only a doc can hold other artifacts: a doc with others inside
137141
138142 ## Spaces
139143
140−Every workspace has a **General** space for what everyone should know. Make
141−more from **+** beside **Spaces** in the sidebar, then **New space**.
144+Every workspace starts with a **General** space for what everyone should
145+know. Make more from **+** beside **Spaces** in the sidebar, then **New
146+space**. No space has to exist: a workspace can have none at all, and
147+artifacts in no space live in their owners' **Private**.
142148
143149 | Who it's for | Who can open it |
144150 | --- | --- |
155161 the projects it's about. **Editors can share**, off by default, lets people
156162 who can edit what's in the space share it too, up to **Can edit**. Only
157163 people with full access give full access, change general access, or change
158−someone else's full access. **Archive this space** takes it out of the
159−sidebar and search; what's in it is kept.
164+someone else's full access.
165+
166+### Archiving and deleting a space
160167
168+Both are at the bottom of a space's **Settings**, for people with full access
169+to the space; workspace owners can also delete any space they can open.
170+
171+| | What happens |
172+| --- | --- |
173+| **Archive space** | It leaves the sidebar and search. Everything in it is kept. **Browse spaces** lists it under **Archived**, where **Restore** brings it back as it was, or **Delete** deletes it. |
174+| **Delete space** | Everything in it goes to the trash together, where anyone who could edit an artifact restores it as usual for 30 days. The space itself, its members and settings are gone for good. |
175+
176+Any space can be archived or deleted, **General** included. A workspace whose
177+General space is deleted has no General space; none takes its place, and
178+new spaces are made as usual. An artifact restored from a deleted space
179+comes back at the top of **General** when there is one you can add to, else
180+in its owner's **Private**.
181+
161182 ## Docs
162183
163184 Make a doc, give it a title, and press **Enter** to start writing. Type `/`
305326 ## Trash
306327
307328 **⋯ → Move to trash** puts an artifact, and everything inside it, in the
308−**Trash**. Anyone who can edit it can restore it from there, with what was
309−inside. After 30 days in the trash it's deleted for good. Deleting it sooner
310−needs full access.
329+**Trash**; it leaves the sidebar at once. Anyone who can edit it can restore
330+it from there, with what was inside. After 30 days in the trash it's deleted
331+for good. Deleting it sooner needs full access: **Delete** beside one
332+artifact, or **Empty trash** at the top of the page.
333+
334+**Empty trash** deletes for good every artifact in the trash you have full
335+access to, with its history, comments and everything inside it. Artifacts
336+you can only edit stay; the page says how many go and how many stay before
337+you confirm. Emptying the trash can't be undone.
311338
312339 ## Projects' docs
313340
+5−0
3131 will use g1t's tools. [MCP tools](/reference/mcp/) lists every one. The
3232 agent sees only the tools and actions its access allows.
3333
34+When Claude Code runs on a [g1t runner](/guides/self-hosted-runners/#plugins-and-skills-on-runners)
35+instead of your machine, it also gets what the workspace set up in
36+[Customize](/guides/customize/): its installed plugins at their pinned
37+commits, the library's skills, and its connectors, signed in.
38+
3439 ### With a token instead
3540
3641 To skip the browser, for a script or a machine without one, put an
+8−0
239239 for an outside collaborator is told the project's own memory, never the
240240 workspace's, and names only the projects around it that they can read.
241241
242+What extends an agent beyond the hub lives in
243+[Customize](/guides/customize/): the skills, connectors and plugins a
244+workspace adds. A run on a
245+[g1t runner](/guides/self-hosted-runners/#plugins-and-skills-on-runners)
246+gets them as one bundle beside its Context section: the plugins to fetch at
247+their pinned commits, the library's skills as files, and the connectors
248+that reach the agent, signed in.
249+
242250 ## Building it for a workspace
243251
244252 The first time anyone opens a workspace's Context page, g1t builds its hub:
+515−0
1+---
2+title: Customize
3+description: One place for what shapes how a workspace's agents work - Skills, Connectors and Plugins, each with what you have and what you can add. Everything installs in the formats Claude Code reads, so public marketplaces, plugins, skills, subagents and MCP servers work on g1t as they are, and what you customize reaches Claude Code when it runs on a g1t runner.
4+---
5+
6+**Customize** is where a workspace shapes what its agents know, what they
7+can reach, and what they bring with them. It has three parts, each in two
8+halves: **Yours**, what the workspace has, and **Discover**, what it can
9+add.
10+
11+| Part | What it holds |
12+| --- | --- |
13+| **Skills** | What agents know how to do: g1t's [foundational skills](/guides/agent-skills/#the-foundational-skills), your workspace's [skill library](/guides/agent-skills/#the-skill-library), and every skill published in a marketplace. |
14+| **Connectors** | What agents can reach: MCP servers connected once and given to agents, and g1t's own [integrations](/guides/integrations/) (GitHub, trackers, alerts, model providers). |
15+| **Plugins** | Skills, agents and connectors published together, from any marketplace in the open plugin format. |
16+
17+Everything installable keeps the formats and the names the open ecosystem
18+uses. A marketplace is a repository with `.claude-plugin/marketplace.json`;
19+a plugin is a folder with `skills/`, `agents/`, `commands/` and `.mcp.json`;
20+a skill is a `SKILL.md` folder. g1t reads them exactly as Claude Code does
21+and invents no format of its own, so a plugin written for Claude Code
22+installs here unchanged, and what a workspace customizes reaches Claude
23+Code when it [runs on a g1t runner](#what-a-run-gets).
24+
25+Open Customize from **Apps** at the foot of the rail (the Apps page and
26+the launcher lead here), from **More** on a phone, or with
27+<kbd>Ctrl</kbd> <kbd>K</kbd> and *Customize*. Its address is
28+`g1t.sh/<workspace>/-/customize`; each part has its own, such as
29+`…/-/customize/skills`. The old `…/-/marketplace` addresses arrive here.
30+
31+## Who adds things
32+
33+Every member of a workspace browses Customize. Only its **owners** add
34+marketplaces, install plugins, connect connectors and decide which agents
35+get what, because each of those reaches the workspace's data or spends
36+its money. Everyone else sees **Request** where an owner sees **Install**
37+or **Connect**, and asks (see [requests](#requests)). Writing and
38+importing skills stays with owners and team maintainers, as in the
39+[skill library](/guides/agent-skills/#who-can-do-what).
40+
41+Every page has a search, filters by category, by source (which
42+marketplace) and by availability, and an **Add** menu: **Write a skill**,
43+**Import**, **Add a marketplace** and **Add a connector by URL**.
44+
45+## Skills
46+
47+**Skills → Yours** is the skill library, the same one as **Agents →
48+Skills**: g1t's foundational skills, the skills your workspace wrote,
49+imported or saved from a session, and the skills installed plugins brought.
50+Writing, importing, versions and the linked repository are in
51+[agent skills](/guides/agent-skills/).
52+
53+**Skills → Discover** lists every skill the marketplaces your workspace
54+reads publish, one card per skill, with the plugin and the marketplace it
55+comes from. Each card says whether your library has it already, and
56+whether the plugin it is in can be installed here (see
57+[what g1t can read](#where-plugins-come-from)).
58+
59+### Add a skill from Discover
60+
61+1. Open **Customize → Skills → Discover** and find the skill, by name or
62+ by what it does.
63+2. Choose **Add**. g1t installs the plugin that carries it, at its current
64+ commit, and the plugin's skills join the library.
65+3. Choose who gets it under **Available to**: every agent in the
66+ workspace, a team (every agent on it), or one agent. This is a skill
67+ [attachment](/guides/agent-skills/#the-skill-library), so the same
68+ rules apply: owners attach anywhere, team maintainers on the teams they
69+ maintain.
70+
71+A skill does nothing until it is available to someone. Owners can turn any
72+skill off for one agent from its **Skills** tab, as with every skill.
73+
74+### Skills from plugins
75+
76+A skill a plugin brings is named `plugin:skill`, after the plugin's name
77+(`pdf@anthropic-agent-skills` brings `pdf:pdf`), on the library's page, on
78+each agent's Skills tab and in the agent's own instructions. Agents call it
79+with `use_skill` by that name, or by its bare name when only one plugin
80+brings a skill called that. A plain name always means the workspace's own
81+skill first.
82+
83+| | A skill written here | A skill from a plugin |
84+| --- | --- | --- |
85+| Name | Lowercase letters, digits and single hyphens, at most 64 characters; the foundational skills' names are taken | As Claude Code takes it: letters, digits, dots, underscores and hyphens, at most 64, case kept; may reuse a foundational name, since it is always called as `plugin:name` |
86+| Edited | In the editor, or from the linked repository | Read-only. It changes when the plugin is reinstalled at a newer commit, and leaves with the plugin |
87+| Size | 1 MB and 200 files | 1.5 MB and 1,000 files kept here. A larger folder keeps its text and leaves its biggest files out, binaries first, each named on the plugin's page; Claude Code on a runner gets the whole plugin |
88+| `description` | At most 1,024 characters | Kept as written; a longer one is shortened for the listing |
89+| `tools:` | Only tools agents have are accepted | Tools agents don't have are kept aside rather than refused |
90+| `commands/*.md` | — | Each command file becomes a one-file skill named after the file, its `description` (or first line) saying when to use it |
91+
92+When a plugin is **switched off**, its skills leave every agent that had
93+them at once, and come back when it is switched on. When it is
94+**uninstalled**, its skills and their attachments go. **Update** reads the
95+plugin again at its marketplace's current listing: each skill gets a new
96+version, and every attachment follows it.
97+
98+## Connectors
99+
100+A **connector** is an MCP server the workspace connects once, then gives
101+to agents. **Connectors → Yours** lists the ones your workspace has, each
102+with its state and how many tools it offers, beside g1t's own integrations
103+connected here (GitHub, Linear, Jira, Sentry, Datadog, model providers and
104+the rest), which keep their own [setup pages](/guides/integrations/).
105+
106+**Connectors → Discover** is the [directory](/guides/integrations/#the-directory):
107+every tool g1t connects to, by category. A tool that publishes a remote
108+MCP server (Linear, Notion, Sentry, Stripe, Figma, Supabase, Cloudflare
109+and more) is connected from here; an integration g1t built itself
110+(trackers, alerts, model providers) opens its own page; one marked
111+**Soon** can be asked for.
112+
113+### Where a connector comes from
114+
115+| From | How |
116+| --- | --- |
117+| **The directory** | Choose the tool and **Connect**. g1t knows its address and how it signs in. |
118+| **A plugin** | A plugin's `.mcp.json` names servers. Installing it adds each remote (`http` or `sse`) server as a connector, named *Plugin: server*, waiting for an owner to sign in or paste a key, unless it is open. A `stdio` server runs only where Claude Code runs, and is marked so. |
119+| **An address** | **Add a connector by URL**: a name (2 to 32 lowercase letters, digits and hyphens) and an HTTPS address on a public host, with how it signs in. |
120+
121+Two connectors with the same name get numbered. A workspace keeps at most
122+50.
123+
124+### Signing in
125+
126+Each connector signs in the way its server asks. A directory card says
127+which, and anything to know first, such as where a key comes from:
128+
129+| Sign-in | What happens |
130+| --- | --- |
131+| **Browser** | OAuth. g1t finds the server's authorization server from the server's own metadata, registers itself as a client there when the server allows it, and sends you to sign in; the sign-in uses PKCE and comes back to g1t with a code, which g1t exchanges for tokens. Tokens are kept sealed and renewed before they expire. A sign-in that takes more than 15 minutes is started again. |
132+| **Key** | A key an owner pastes, sent as a header (`Authorization` unless the server names another). The key is kept sealed and never shown again. |
133+| **Open** | The server needs nothing. |
134+
135+A server that doesn't register clients on its own can't be signed in with
136+the browser from g1t; when it takes a key instead, add it by address with
137+a key. A server that answers a sign-in with `401` or `403` is marked, and
138+the connector asks to be signed in again or for its key to be checked.
139+
140+#### What each directory server needs
141+
142+| Sign-in | Servers |
143+| --- | --- |
144+| **Browser**, with g1t registering itself as a client | Atlassian, Canva, Cloudflare, Figma, Hugging Face, Intercom, Linear, Neon, Notion, PayPal, PostHog, Sentry, Stripe, Supabase and Vercel |
145+| **Browser**, but only for apps registered with them | Asana and HubSpot. g1t isn't registered with them yet, so signing in from here fails until it is; their cards say so |
146+| **Key** | GitHub's MCP server (a personal access token) and Zapier (the token from its MCP settings), each pasted as `Bearer <token>` |
147+| **Open** | Cloudflare docs, Context7, DeepWiki and Microsoft Learn |
148+
149+A server that speaks only the older event-stream transport isn't listed
150+until it serves the current one.
151+
152+On g1t.sh the sealing key is set. On a g1t you run yourself, the agents
153+service's `CONNECTORS_KEY` seals connectors' keys and tokens; without it,
154+open servers work and nothing that signs in can be kept. See
155+[run g1t yourself](/guides/self-hosting/).
156+
157+### States
158+
159+| State | What it means |
160+| --- | --- |
161+| **Connected** | Its tools were listed and agents that have it can use them. |
162+| **Needs sign-in** | A browser sign-in hasn't been done, or was signed out. |
163+| **Needs a key** | A key hasn't been pasted yet. |
164+| **Problem** | Its last check or renewal failed; the connector says why. **List its tools again** checks again. |
165+
166+Only a connected connector reaches agents. One in any other state is
167+listed, with what it needs, and gives agents nothing until it is fixed.
168+
169+### Tools and what agents may do with them
170+
171+When a connector connects, g1t lists the server's tools and keeps the
172+first 40. Each becomes an ability on every agent that has the connector,
173+offered as `<connector>__<tool>`, with the arguments the server
174+describes, under the same [levels](/guides/agent-abilities/#the-levels)
175+as every ability:
176+
177+- a tool the server marks read-only is a **read**, Alone by default;
178+- any other tool, including one the server says nothing about, is a
179+ **write outside g1t**, Ask first by default: the agent posts a card
180+ saying what it would do, and waits for Allow or Deny.
181+
182+A call carries the connector's sign-in. If the sign-in can't be renewed,
183+the agent is told that an owner needs to sign the connector in again, and
184+the connector shows **Problem**. Levels, Ask-first cards and the
185+[audit log](/guides/audit-log/) work exactly as for an agent's own MCP
186+servers.
187+
188+### Who gets a connector
189+
190+A connector reaches agents through **grants**, which work like a skill's
191+attachments. Open the connector and choose **Give to**:
192+
193+| Give to | Who gets it | Who can |
194+| --- | --- | --- |
195+| Every agent in the workspace | Every agent, `@g1t` included | Owners |
196+| A team | Every agent on the team | Owners |
197+| One agent | That agent | Owners |
198+
199+The same grant twice is one. An agent's **Abilities** tab lists each
200+connector that reaches it under **Connectors**, with how it has it (every
201+agent, a team, this agent) and a level for each tool, beside the MCP
202+servers added to that agent alone, marked **Only this agent**. See
203+[agent abilities](/guides/agent-abilities/#connectors).
204+
205+### Manage a connector
206+
207+On a connector's page, owners can:
208+
209+- **List its tools again**, to pick up a server that changed, or to check
210+ one that showed a problem.
211+- **Sign out** (browser sign-ins): the tokens go, and the connector needs a
212+ sign-in again. **Replace the key** (keys): the new key is sealed and the
213+ tools listed again.
214+- **Take back** a grant, or **Remove** the connector, which removes every
215+ grant with it. Uninstalling a plugin removes the connectors it brought.
216+
217+Adding, keying, signing in and out, granting, taking back and removing a
218+connector are in the audit log under `customize/connectors/<name>`.
219+
220+## Plugins
221+
222+A **plugin** is a folder in the open plugin format, listed in a
223+**marketplace**. Installing one spreads its parts to where g1t's agents use
224+them, and keeps the rest for Claude Code on a runner.
225+
226+### Marketplaces
227+
228+Every workspace starts with three marketplaces, built in, kept current by
229+g1t and not removable:
230+
231+| Marketplace | What it lists |
232+| --- | --- |
233+| `claude-plugins-official` | Anthropic's reviewed plugins: tools, connectors and skills from Anthropic and the companies behind them. |
234+| `anthropic-agent-skills` | Anthropic's skills: documents, spreadsheets, presentations, PDFs, design, testing and more. |
235+| `g1t` | g1t's own plugins: what g1t is building for what a team does besides code (see [what g1t is building](#what-g1t-is-building)). |
236+
237+Owners add more with **Add a marketplace**, from **Plugins** or
238+`…/-/customize/marketplaces`, in the forms Claude Code's
239+`plugin marketplace add` takes:
240+
241+| Type | Read as |
242+| --- | --- |
243+| `owner/repo`, `owner/repo#ref` or `owner/repo@ref` | A public repository on GitHub, or a repository on this g1t when `owner/repo` names one you can read |
244+| A `github.com` address, with or without `.git` or `/tree/<ref>` | The same repository on GitHub |
245+| A git address on this g1t | The repository here |
246+| A link ending in `marketplace.json` | That file alone. Plugins it lists by a relative path can't be reached this way, so add such a marketplace by its repository instead |
247+
248+The marketplace is named as its `marketplace.json` names itself, unless
249+you give another name. Names Claude Code keeps for its own marketplaces
250+(and `g1t`) are refused, so `name@claude-plugins-official` always means
251+the real thing. A workspace reads at most 20 marketplaces of its own; a
252+`marketplace.json` is at most 4 MB.
253+
254+A marketplace's manifest is read when it is added, again when it is older
255+than six hours and someone opens Discover, and on **Read it again**.
256+When it can't be read, the marketplace stays listed with the problem, so
257+it can be read again later. **Remove** takes a marketplace off the
258+workspace; plugins installed from it stay installed.
259+
260+### Where plugins come from
261+
262+Each plugin entry in a marketplace says where its files are (Claude Code's
263+`source`). g1t reads some of those places and not others:
264+
265+| Source | On g1t |
266+| --- | --- |
267+| A relative path inside the marketplace's repository | Read, when the marketplace is on GitHub or on this g1t |
268+| `github` (`owner/repo`, with a `ref` or a `sha`) | Read: public repositories, at the commit the ref names |
269+| `url` or `git-subdir` on github.com or on this g1t | Read |
270+| `archive` (a zip over HTTPS, with an optional `sha256`) | Read; the checksum is checked when given |
271+| `npm` | Listed as **Not available here**: g1t doesn't install from npm yet. It still works in Claude Code |
272+| `command` | Listed as **Not available here**: it is produced by a command on the installing machine, which g1t can't run. It still works in Claude Code |
273+| A git host other than GitHub or this g1t | Listed as **Not available here** |
274+
275+Private GitHub repositories aren't read yet. A plugin read here holds at
276+most 2,000 files and 24 MB together; a file over 2 MB is left out and
277+named on the plugin's page. Claude Code on a runner fetches the plugin
278+whole for itself, so nothing is lost there.
279+
280+### Discover and a plugin's page
281+
282+**Plugins → Discover** lists every plugin every marketplace lists, by
283+category, each with its availability:
284+
285+| Availability | What it means |
286+| --- | --- |
287+| **Available** | Installable now. Owners see **Install**; everyone else **Request**. |
288+| **Installed**, or **Switched off** | The workspace has it. |
289+| **Not available here** | g1t can't fetch it, with why (above). |
290+| **Soon** | g1t's own, announced but not released. Nothing can be pressed. |
291+
292+A plugin's page, `…/-/customize/plugins/<name@marketplace>`, shows its
293+README, its marketplace and who owns it, its manifest (name, version,
294+license, repository), and what it brings: skills, agents, commands, MCP
295+servers, and whether it carries hooks, LSP servers, output styles or
296+`bin/`. Anything that couldn't be read is listed too.
297+
298+### Install a plugin
299+
300+1. Open the plugin's page and choose **Install**. Owners only.
301+2. g1t fetches the plugin at a **pinned commit**: the commit the
302+ marketplace's `sha` or `ref` names, or the head it is at now. The
303+ install remembers that commit; the plugin never changes under you.
304+3. Each part goes where g1t's agents use it:
305+
306+| In the plugin | Becomes |
307+| --- | --- |
308+| `skills/<name>/SKILL.md` | A library skill, `plugin:name`, read-only, ready to be made available to agents |
309+| `commands/<name>.md` | A one-file skill, `plugin:name` |
310+| `agents/<name>.md` | A template under **Agents → Templates → From plugins**, to [hire an agent from](/guides/agents/#role-templates) |
311+| `.mcp.json` servers of type `http` or `sse` | A connector each, waiting for an owner to sign in or paste a key |
312+| `.mcp.json` servers of type `stdio` | Marked **Claude Code only**; nothing on g1t's agents |
313+| `hooks/hooks.json`, `.lsp.json`, `output-styles/`, `bin/` | Kept for Claude Code on a runner, which fetches the plugin whole at the same commit |
314+
315+The plugin's files aren't copied into g1t beyond its skills and
316+subagents. A runner fetches them when a job runs (see
317+[what a run gets](#what-a-run-gets)).
318+
319+### Switch off, uninstall and update
320+
321+| | |
322+| --- | --- |
323+| **Switch off** | The kill switch. The plugin's skills leave every agent at once, its agents leave Templates, the connectors it brought stop reaching agents, and runners stop getting it. **Switch on** brings them all back. |
324+| **Uninstall** | Its skills and their attachments go, and the connectors it brought are removed. The record of who installed and removed it stays. |
325+| **Update** | Shown as **Update available** when its marketplace lists a newer version or commit than the one installed. An install never moves on its own. **Update** reads the plugin again at that commit: its skills get new versions and their attachments follow, skills it no longer has are archived, its agents and connectors are refreshed (a connector it already brought is left as it is), and the install's version and commit move. |
326+
327+A workspace installs at most 100 plugins. Installing, updating, switching on and
328+off and uninstalling are in the audit log under
329+`customize/plugins/<name@marketplace>`, and adding or removing a
330+marketplace under `customize/marketplaces/<name>`.
331+
332+### What g1t is building
333+
334+g1t's own marketplace, `g1t`, is where g1t's own plugins are listed as
335+they are released. The ones announced are marked **Soon** and can't be
336+installed until their first release:
337+
338+| Plugin | What it adds |
339+| --- | --- |
340+| **Mail** | Email on your own domain, with shared inboxes agents work in. |
341+| **Support** | Conversations, escalations, macros and a knowledge base; bug reports become issues in Code. |
342+| **CRM** | Accounts, deals by stage and a forecast, kept current by agents. |
343+| **Recruiting** | Openings, candidates, scorecards and interview loops. Agents screen; people decide. |
344+| **On-call** | Rotations, pages and incidents. |
345+| **Helpdesk bridge**, **CRM bridge**, **ERP bridge** | Each bridges a system your team already runs, so agents work across it and g1t together. Anything that changes money or stock waits for a person. |
346+
347+## The formats
348+
349+g1t reads the files below exactly as Claude Code does, and ignores keys it
350+doesn't know, so a plugin never needs a g1t version. Paths are inside the
351+repository or the plugin's folder.
352+
353+### `.claude-plugin/marketplace.json`
354+
355+| Key | Required | What it is |
356+| --- | --- | --- |
357+| `name` | Yes | The marketplace's name: letters, digits, `.`, `_` and `-`, starting with a letter or digit, at most 64. Plugins are `name@marketplace` by it. |
358+| `owner` | Yes | `{ "name", "email", "url" }`, or a name alone. |
359+| `plugins` | Yes | The plugins it lists (below). An entry that can't be read is noted, not refused, and the others are listed. |
360+| `description`, `version` | No | Shown on the marketplace. `metadata.description` and `metadata.version` are read too. |
361+| `metadata.pluginRoot` | No | The folder bare plugin paths resolve under. |
362+
363+Each entry in `plugins`:
364+
365+| Key | Required | What it is |
366+| --- | --- | --- |
367+| `name` | Yes | The plugin's name, like `pr-review-toolkit`: letters, digits, `.`, `_` and `-`, at most 64. Listed once per marketplace. |
368+| `source` | Yes | A relative path (`./plugins/pdf`), or an object: `{ "source": "github", "repo", "ref", "sha" }`, `url`, `git-subdir` (`url`, `path`), `archive` (`url`, `sha256`), `npm` (`package`, `version`), `command` (`command`). See [where plugins come from](#where-plugins-come-from). |
369+| `description`, `version`, `author`, `homepage`, `repository`, `license`, `keywords`, `category`, `tags`, `displayName` | No | Shown on the card and the page. |
370+| `skills`, `commands`, `agents` | No | Component paths, added to the plugin's standard folders. A skills-only plugin that names its skills here is listed in Discover's Skills without being fetched. |
371+| `mcpServers` | No | Servers, in `.mcp.json`'s shape, for a plugin without a `plugin.json`. |
372+| `strict` | No | `false` means the entry names its components outright and the standard folders aren't read, as a marketplace that keeps many plugins in one repository does. |
373+
374+### `.claude-plugin/plugin.json` and the plugin's layout
375+
376+`plugin.json` is optional: a folder with the layout alone is a plugin, and
377+only `name` is required when the file is there.
378+
379+| Key | What it is |
380+| --- | --- |
381+| `name` | The plugin's name, as above. |
382+| `displayName`, `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` | Shown on the page. |
383+| `skills`, `commands`, `agents`, `hooks` | Component paths besides the standard folders (`./` is kept off; `.` is the root). |
384+| `mcpServers` | Servers inline, in `.mcp.json`'s shape, or a path to a file holding them. |
385+| `lspServers` | Whether it brings LSP servers (Claude Code only). |
386+
387+| In the folder | What g1t does with it |
388+| --- | --- |
389+| `skills/<name>/SKILL.md` | A skill each |
390+| `commands/*.md` | A one-file skill each |
391+| `agents/*.md` | A hireable template each |
392+| `.mcp.json` | Connectors (remote servers) |
393+| `hooks/hooks.json`, `.lsp.json`, `output-styles/`, `bin/` | Kept for Claude Code on a runner |
394+| `README.md` | Shown on the plugin's page |
395+
396+### `SKILL.md`
397+
398+The [skill format](/guides/agent-skills/#the-skill-format), unchanged:
399+YAML front-matter, then the instructions.
400+
401+| Key | Required | What it is |
402+| --- | --- | --- |
403+| `name` | Yes (for a plugin's skill, the folder's name when absent) | The skill's name. |
404+| `description` | Yes | When to use it, at most 1,024 characters. |
405+| `license`, `compatibility`, `metadata`, `allowed-tools` | No | Kept as written; change nothing. |
406+| `tools` | No | g1t's own key: the agent tools the skill uses. |
407+| `requires_computer` | No | g1t's own key: `true` when the skill needs the agent's own computer. |
408+
409+### `agents/<name>.md`
410+
411+A subagent file: front-matter, then the system prompt as the body. Both
412+`name` and `description` are required, and the body can't be empty.
413+
414+| Key | What it is | On g1t |
415+| --- | --- | --- |
416+| `name` | At most 256 characters, no colon, not starting with a hyphen | The template's name, `plugin:name` |
417+| `description` | When to use it | The template's description |
418+| `model` | The model it prefers | Kept with the template, for the agent's model limits |
419+| `tools`, `disallowedTools` | Claude Code's or MCP tool names, as a list or separated by commas | Kept with the template; g1t's agents use g1t's own tools and abilities |
420+| `skills`, `mcpServers`, `color` | Skills to preload, servers it uses, its colour | Kept as written |
421+| The body | The system prompt | The agent's instructions when you hire from it |
422+
423+### `.mcp.json`
424+
425+```json
426+{
427+ "mcpServers": {
428+ "linear": { "type": "http", "url": "https://mcp.linear.app/mcp" },
429+ "search": { "type": "sse", "url": "https://example.com/sse", "headers": { "Authorization": "Bearer ${SEARCH_KEY}" } },
430+ "local": { "command": "npx", "args": ["-y", "some-server"] }
431+ }
432+}
433+```
434+
435+| Server | On g1t |
436+| --- | --- |
437+| `type: http` (or `streamable-http`) or `sse`, with a `url` | A connector. One whose `url` or `headers` reference a `${VARIABLE}` asks for a key; one without asks for a browser sign-in. |
438+| `type: stdio`, or a `command` with no type | Claude Code only. |
439+| A `url` with no `type` | Noted as a problem: a remote server says its type. |
440+| `ws`, `sdk` | Not a connector. |
441+
442+A server's name is letters, digits, dots, underscores and hyphens, at
443+most 64.
444+
445+## Requests
446+
447+Anyone who isn't an owner asks for what they need from the same place an
448+owner would add it:
449+
450+1. Choose **Request** on a plugin, a skill from Discover, or a connector.
451+2. Check what you are asking for: the dialog shows where it comes from and
452+ that it is available.
453+3. Say why you want it, if you like (280 characters), and choose
454+ **Send request**.
455+
456+Every owner is notified. The button then reads **Requested**, and **Your
457+requests** lists it until an owner answers; you are notified when one does.
458+You can't ask twice for the same thing while a request is waiting, and you
459+can have 20 waiting at once.
460+
461+Owners answer requests from **Requests** in Customize's header, at
462+`…/-/customize/requests`, which shows how many are waiting:
463+
464+| | |
465+| --- | --- |
466+| **Install** or **Connect** | Opens where it is added. Installing a plugin, or signing a connector in, answers every request for it by itself. |
467+| **Mark added** | After you added it another way. |
468+| **Turn down** | The person who asked is told. They can ask again. |
469+
470+Answered requests stay listed for 30 days. A request names its listing as
471+`plugin:<name@marketplace>`, `skill:<name@marketplace>/<skill>`,
472+`connector:<id>` (from the directory) or `integration:<id>` (one of g1t's
473+own integrations). When an agent needs a connector or an integration that
474+isn't connected, it posts a **Request** card in the conversation that
475+opens the same request (see
476+[when something is missing](/guides/agent-abilities/#when-something-is-missing)).
477+Open requests also appear on owners'
478+[Home](/guides/home/#where-youre-needed).
479+
480+## What a run gets
481+
482+When Claude Code runs a job on a [g1t runner](/guides/self-hosted-runners/),
483+it is given what the workspace customized, as the job's agent would have
484+it: every installed plugin that is switched on, to fetch at its pinned
485+commit; the library's own skills that reach the agent, as files; and the
486+connected connectors that reach it, with what signs them in. The runner
487+writes the plugins to disk and starts Claude Code with each as a plugin
488+directory, puts the skills where it reads them, and merges the connectors
489+into its MCP configuration. See
490+[what a run gets from Customize](/guides/self-hosted-runners/#what-a-run-gets-from-customize).
491+
492+Chat replies and [sessions](/guides/agent-sessions/) on g1t get the same
493+things in their own way: skills through `use_skill`, connectors as
494+abilities with levels.
495+
496+## API
497+
498+Customize is in the REST API under `/workspaces/{workspace}/customize/…`
499+and in the MCP server as the `customize` tool, one action per thing an
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,
506+`GET /workspaces/{workspace}/customize/bundle`, needs `agents:run` and is
507+read with a job's agent token, since it carries the connectors' sign-ins.
508+See the [MCP tools](/reference/mcp/) and the [API reference](/reference/api/).
509+
510+## Next
511+
512+- [Agent skills](/guides/agent-skills/): the library, the skill format, versions and the linked repository.
513+- [Agent abilities](/guides/agent-abilities/): the levels every connector's tools run under.
514+- [Agents](/guides/agents/): hiring an agent from a plugin's template.
515+- [Self-hosted runners](/guides/self-hosted-runners/): where what you customize reaches Claude Code.
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.