Skip to content
499 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

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.1/**
2 * Customize's RPC methods (@g1t/contracts workspace-agents.ts, the
3 * `marketplaces`, `plugins` and `connectors` calls): marketplaces a
4 * workspace reads, what Discover lists, plugins installed and what they
5 * brought, and connectors with their sign-ins and grants. The rules are
6 * in marketplaces.ts, plugins.ts and connectors.ts; this file reads what
7 * they need from identity and repos, checks who may do what (members see,
8 * owners change), and writes the audit log.
9 */
10import { type RepoPath, type Result, type User, fail, identityClient, ok, reposClient } from "@g1t/contracts";
11
12import { type McpServer } from "../../../packages/contracts/src/abilities.ts";
13import { type McpServerConfig, parseMarketplaceAddress, parsePluginRef } from "../../../packages/contracts/src/claude-plugin.ts";
14import type { AgentConnector, ConnectorInput, CustomizeBundle, Marketplace, PluginAgentTemplate, PluginDetail, PluginInstall, PluginListing, SkillListing, WorkspaceConnector } from "../../../packages/contracts/src/customize.ts";
15import type { SkillScope } from "../../../packages/contracts/src/skill-library.ts";
16import type { SkillFile } from "../../../packages/contracts/src/skill-format.ts";
17import * as connectors from "./connectors.ts";
18import type { FetchDeps } from "./customize-fetch.ts";
19import { definitionOf } from "./store.ts";
20import * as marketplaces from "./marketplaces.ts";
21import * as plugins from "./plugins.ts";
22import { answerListing } from "./installs.ts";
23import type { SessionEnv } from "./sessions.ts";
24import { listingRef } from "../../../packages/contracts/src/marketplace.ts";
25import type { ViewContext } from "./views.ts";
26
27type CustomizeEnv = SessionEnv & { CONNECTORS_KEY?: string; GITHUB_TOKEN?: string; SITE_URL?: string };
28
29const env = (ctx: ViewContext): CustomizeEnv => ctx.env as CustomizeEnv;
30
31/** This g1t's host, for reading its own repositories by address. */
32function hostOf(ctx: ViewContext): string | null {
33 try {
34 return new URL(env(ctx).SITE_URL ?? "https://g1t.sh").hostname;
35 } catch {
36 return null;
37 }
38}
39
40/** Where plugins and marketplaces are read from: GitHub with the token if set, and repositories here the viewer can read. */
41function fetchDeps(ctx: ViewContext): FetchDeps {
42 const repos = reposClient(env(ctx).REPOS);
43 return {
44 githubToken: env(ctx).GITHUB_TOKEN ?? null,
45 repos: {
46 repo: async (full) => {
47 const [namespace, name] = full.split("/") as [string, string];
48 const found = await repos.get({ namespace, name } as RepoPath, ctx.viewer).catch(() => null);
49 return found?.ok ? { id: found.value.id, default_branch: found.value.defaultBranch } : null;
50 },
51 listFiles: (repoId, ref) => repos.listFiles(repoId, ref, 10_000),
52 blobs: async (repoId, hashes) => (await repos.rawBlobs(repoId, hashes, 2 * 1024 * 1024)).map((blob) => ({ hash: blob.hash, data: blob.data })),
53 },
54 };
55}
56
57async function connectorDeps(ctx: ViewContext): Promise<connectors.Deps> {
58 return { key: await connectors.sealingKey(env(ctx).CONNECTORS_KEY), site: (env(ctx).SITE_URL ?? "https://g1t.sh").replace(/\/+$/, "") };
59}
60
61const owners = (ctx: ViewContext, what: string): Result<null> => (ctx.owner ? ok(null) : fail("forbidden", `Only the workspace's owners ${what}.`));
62
63type Labels = { agents: Map<string, { handle: string; display_name: string }>; teams: Map<string, string> };
64
65async function labels(ctx: ViewContext): Promise<Labels> {
66 const [agents, teams] = await Promise.all([
67 ctx.db.prepare("SELECT id, handle, display_name FROM agents WHERE workspace_id = ? AND archived_at IS NULL").bind(ctx.workspaceId).all<{ id: string; handle: string; display_name: string }>(),
68 identityClient(env(ctx).IDENTITY)
69 .listTeams(ctx.viewer, ctx.slug)
70 .catch(() => null),
71 ]);
72 return { agents: new Map(agents.results.map((a) => [a.id, a])), teams: new Map(teams?.ok ? teams.value.map((t) => [t.slug, t.name]) : []) };
73}
74
75// ── Marketplaces and Discover ────────────────────────────────────────────
76
77async function loaded(ctx: ViewContext) {
78 return marketplaces.loadAll(ctx.db, ctx.workspaceId, fetchDeps(ctx), hostOf(ctx));
79}
80
81async function marks(ctx: ViewContext): Promise<marketplaces.InstallMarks> {
82 return new Map((await plugins.listInstalls(ctx.db, ctx.workspaceId)).map((row) => [row.ref, { enabled: row.enabled === 1 }]));
83}
84
85async function listMarketplaces(ctx: ViewContext): Promise<Result<Marketplace[]>> {
86 return ok(marketplaces.marketplacesOut(await loaded(ctx), ctx.owner));
87}
88
89async function addMarketplace(ctx: ViewContext, address: unknown, name: unknown): Promise<Result<Marketplace>> {
90 const may = owners(ctx, "add marketplaces");
91 if (!may.ok) return may;
92 const deps = fetchDeps(ctx);
93 const parsed = parseMarketplaceAddress(String(address ?? ""), { host: hostOf(ctx) ?? undefined, here: () => false });
94 if (!parsed.ok) return fail("invalid", parsed.message);
95 // `workspace/name` that is a repository here is read here; otherwise GitHub.
96 let source = parsed.value;
97 if (source.source === "github" && !String(address).includes("://") && (await deps.repos!.repo(source.repo))) source = { source: "g1t", repo: source.repo, ...(source.ref ? { ref: source.ref } : {}) };
98 const added = await marketplaces.addMarketplace(ctx.db, ctx.workspaceId, typeof name === "string" ? name : null, source, ctx.viewer.username, deps, hostOf(ctx));
99 if (added.ok) ctx.audit?.("add_marketplace", `customize/marketplaces/${added.value.name}`, `Added the marketplace ${added.value.name} (${added.value.plugins} plugins)`);
100 return added;
101}
102
103async function removeMarketplace(ctx: ViewContext, name: unknown): Promise<Result<null>> {
104 const may = owners(ctx, "remove marketplaces");
105 if (!may.ok) return may;
106 const removed = await marketplaces.removeMarketplace(ctx.db, ctx.workspaceId, String(name ?? ""));
107 if (removed.ok) ctx.audit?.("remove_marketplace", `customize/marketplaces/${String(name)}`, `Removed the marketplace ${String(name)}`);
108 return removed;
109}
110
111async function refreshMarketplace(ctx: ViewContext, name: unknown): Promise<Result<Marketplace>> {
112 const may = owners(ctx, "read marketplaces again");
113 if (!may.ok) return may;
114 const entry = await marketplaces.marketplaceByName(ctx.db, ctx.workspaceId, String(name ?? ""));
115 if (!entry) return fail("not_found", "The workspace reads no marketplace by that name.");
116 const row = await marketplaces.refreshRow(ctx.db, entry, fetchDeps(ctx), hostOf(ctx));
117 return ok(marketplaces.marketplacesOut([{ row, builtin: !!entry.builtin, manifest: marketplaces.cachedManifest(row) }], ctx.owner)[0]!);
118}
119
120export type Discover = { plugins: PluginListing[]; skills: SkillListing[]; marketplaces: Marketplace[] };
121
122async function discover(ctx: ViewContext): Promise<Result<Discover>> {
123 const all = await loaded(ctx);
124 const listings = marketplaces.discoverPlugins(all, await marks(ctx), hostOf(ctx));
125 const { results } = await ctx.db.prepare("SELECT plugin, name FROM skills WHERE workspace_id = ? AND plugin IS NOT NULL AND archived_at IS NULL").bind(ctx.workspaceId).all<{ plugin: string; name: string }>();
126 const added = new Set(results.map((r) => `${r.plugin}/${r.name}`));
127 return ok({ plugins: listings, skills: marketplaces.discoverSkills(listings, added), marketplaces: marketplaces.marketplacesOut(all, ctx.owner) });
128}
129
130// ── Plugins ──────────────────────────────────────────────────────────────
131
132async function listingFor(ctx: ViewContext, ref: unknown) {
133 const parsed = parsePluginRef(ref);
134 if (!parsed) return fail("invalid", "Name a plugin as name@marketplace.");
135 const all = await loaded(ctx);
136 const listings = marketplaces.discoverPlugins(all, await marks(ctx), hostOf(ctx));
137 const listing = marketplaces.findListing(listings, ref as string);
138 const entry = marketplaces.entryOf(all, parsed);
139 if (!listing || !entry) return fail("not_found", `No marketplace the workspace reads lists ${String(ref)}.`);
140 const manifest = all.find((l) => l.row.name === parsed.marketplace)?.manifest ?? null;
141 return ok({ listing, entry, owner: manifest?.owner.name ?? (parsed.marketplace === "g1t" ? "g1t" : null), listings });
142}
143
144async function pluginDetail(ctx: ViewContext, ref: unknown): Promise<Result<PluginDetail>> {
145 const found = await listingFor(ctx, ref);
146 if (!found.ok) return found;
147 const { listing, entry, owner } = found.value;
148 return ok(await plugins.detail(ctx.db, ctx.workspaceId, listing, entry.entry, { source: entry.source, pluginRoot: entry.pluginRoot, owner }, hostOf(ctx), fetchDeps(ctx)));
149}
150
151async function pluginInstalls(ctx: ViewContext): Promise<Result<PluginInstall[]>> {
152 const rows = await plugins.listInstalls(ctx.db, ctx.workspaceId);
153 const listings = rows.length ? marketplaces.discoverPlugins(await loaded(ctx), await marks(ctx), hostOf(ctx)) : [];
154 return ok(plugins.installsOut(rows, listings));
155}
156
157async function installPlugin(ctx: ViewContext, ref: unknown): Promise<Result<PluginInstall>> {
158 const may = owners(ctx, "install plugins");
159 if (!may.ok) return may;
160 const found = await listingFor(ctx, ref);
161 if (!found.ok) return found;
162 const { listing, entry } = found.value;
163 const done = await plugins.install(ctx.db, { workspaceId: ctx.workspaceId, listing, entry: entry.entry, marketplace: { name: listing.marketplace, source: entry.source, pluginRoot: entry.pluginRoot }, host: hostOf(ctx), by: ctx.viewer.username, deps: fetchDeps(ctx) });
164 if (!done.ok) return done;
165 await pluginConnectors(ctx, listing, done.value.plugin.servers, done.value.install.id);
166 ctx.audit?.("install_plugin", `customize/plugins/${listing.ref}`, `Installed the plugin ${listing.ref}${done.value.install.version ? ` ${done.value.install.version}` : ""} (${done.value.skills.filter((s) => !s.problem).length} skills, ${done.value.plugin.agents.length} agents)`);
167 await answerListing(ctx, listingRef("plugin", listing.ref));
168 const fresh = await plugins.installByRef(ctx.db, ctx.workspaceId, listing.ref);
169 return ok(fresh ? plugins.installOut(fresh) : done.value.install);
170}
171
172/** A plugin's remote servers become connectors, for an owner to sign in or give a key; one the workspace has from this plugin already is left as it is. */
173async function pluginConnectors(ctx: ViewContext, listing: PluginListing, servers: McpServerConfig[], installId: string): Promise<void> {
174 const deps = await connectorDeps(ctx);
175 const have = new Set((await connectors.listConnectors(ctx.db, ctx.workspaceId)).map((row) => JSON.stringify(JSON.parse(row.from_json))));
176 for (const server of servers) {
177 if ((server.type !== "http" && server.type !== "sse") || !server.url) continue;
178 const from = { kind: "plugin" as const, ref: listing.ref, server: server.name };
179 if (have.has(JSON.stringify(from))) continue;
180 const auth = server.variables.length ? "header" : "oauth";
181 const headerName = Object.keys(server.headers)[0] ?? "Authorization";
182 const added = await connectors.addConnector(ctx.db, deps, ctx.workspaceId, { from, auth, header: auth === "header" ? { name: headerName, value: "" } : null }, ctx.viewer.username, { title: `${listing.display_name ?? listing.name}: ${server.name}`, url: server.url });
183 if (!added.ok) await ctx.db.prepare("UPDATE plugin_installs SET problems = json_insert(problems, '$[#]', ?) WHERE id = ?").bind(`${server.name}: ${added.error.message}`, installId).run();
184 }
185}
186
187async function updatePlugin(ctx: ViewContext, ref: unknown): Promise<Result<PluginInstall>> {
188 const may = owners(ctx, "update plugins");
189 if (!may.ok) return may;
190 const found = await listingFor(ctx, ref);
191 if (!found.ok) return found;
192 const { listing, entry } = found.value;
193 const done = await plugins.update(ctx.db, { workspaceId: ctx.workspaceId, listing, entry: entry.entry, marketplace: { name: listing.marketplace, source: entry.source, pluginRoot: entry.pluginRoot }, host: hostOf(ctx), by: ctx.viewer.username, deps: fetchDeps(ctx) });
194 if (!done.ok) return done;
195 if (!done.value.unchanged) {
196 await pluginConnectors(ctx, listing, done.value.plugin.servers, done.value.install.id);
197 ctx.audit?.("update_plugin", `customize/plugins/${listing.ref}`, `Updated the plugin ${listing.ref}${done.value.install.version ? ` to ${done.value.install.version}` : ""}${done.value.install.sha ? ` (${done.value.install.sha.slice(0, 8)})` : ""}`);
198 }
199 const fresh = await plugins.installByRef(ctx.db, ctx.workspaceId, listing.ref);
200 return ok(fresh ? plugins.installOut(fresh) : done.value.install);
201}
202
203async function setPluginEnabled(ctx: ViewContext, ref: unknown, enabled: unknown): Promise<Result<PluginInstall>> {
204 const may = owners(ctx, "switch plugins on and off");
205 if (!may.ok) return may;
206 const changed = await plugins.setEnabled(ctx.db, ctx.workspaceId, String(ref ?? ""), enabled === true);
207 if (changed.ok) ctx.audit?.(enabled === true ? "enable_plugin" : "disable_plugin", `customize/plugins/${String(ref)}`, `${enabled === true ? "Switched on" : "Switched off"} the plugin ${String(ref)}`);
208 return changed;
209}
210
211async function uninstallPlugin(ctx: ViewContext, ref: unknown): Promise<Result<null>> {
212 const may = owners(ctx, "uninstall plugins");
213 if (!may.ok) return may;
214 const removed = await plugins.uninstall(ctx.db, ctx.workspaceId, String(ref ?? ""), ctx.viewer.username);
215 if (!removed.ok) return removed;
216 const gone = await connectors.removePluginConnectors(ctx.db, await connectorDeps(ctx), ctx.workspaceId, String(ref));
217 ctx.audit?.("uninstall_plugin", `customize/plugins/${String(ref)}`, `Uninstalled the plugin ${String(ref)} (${removed.value.skills.length} skills, ${gone} connectors)`);
218 return ok(null);
219}
220
221async function pluginAgents(ctx: ViewContext): Promise<Result<PluginAgentTemplate[]>> {
222 return ok(await plugins.pluginAgents(ctx.db, ctx.workspaceId));
223}
224
225// ── Connectors ───────────────────────────────────────────────────────────
226
227async function listConnectors(ctx: ViewContext): Promise<Result<WorkspaceConnector[]>> {
228 return ok(await connectors.connectorsOut(ctx.db, ctx.workspaceId, await labels(ctx)));
229}
230
231async function oneConnector(ctx: ViewContext, id: unknown): Promise<Result<WorkspaceConnector>> {
232 const all = await connectors.connectorsOut(ctx.db, ctx.workspaceId, await labels(ctx));
233 const found = all.find((c) => c.id === id);
234 return found ? ok(found) : fail("not_found", "There is no such connector.");
235}
236
237async function addConnector(ctx: ViewContext, input: unknown): Promise<Result<WorkspaceConnector>> {
238 const may = owners(ctx, "add connectors");
239 if (!may.ok) return may;
240 const given = (input ?? {}) as Partial<ConnectorInput>;
241 if (!given.from || typeof given.from !== "object") return fail("invalid", "Say where the connector comes from.");
242 const auth = given.auth === "header" || given.auth === "oauth" ? given.auth : "none";
243 let pluginServer: { title: string; url: string } | null = null;
244 if (given.from.kind === "plugin") {
245 const install = await plugins.installByRef(ctx.db, ctx.workspaceId, given.from.ref);
246 const server = install ? plugins.installOut(install).components.mcp_servers.find((s) => s.name === (given.from as { server: string }).server) : null;
247 if (!install || !server?.url) return fail("not_found", "That plugin isn't installed here, or names no such server.");
248 pluginServer = { title: `${install.display_name ?? install.name}: ${server.name}`, url: server.url };
249 }
250 const added = await connectors.addConnector(ctx.db, await connectorDeps(ctx), ctx.workspaceId, { from: given.from, auth, header: given.header ?? null }, ctx.viewer.username, pluginServer);
251 if (!added.ok) return added;
252 ctx.audit?.("add_connector", `customize/connectors/${added.value.name}`, `Added the connector ${added.value.title} (${added.value.url})`);
253 return oneConnector(ctx, added.value.id);
254}
255
256async function rowFor(ctx: ViewContext, id: unknown): Promise<Result<connectors.ConnectorRow>> {
257 const row = await connectors.connectorById(ctx.db, ctx.workspaceId, String(id ?? ""));
258 return row ? ok(row) : fail("not_found", "There is no such connector.");
259}
260
261async function setConnectorKey(ctx: ViewContext, id: unknown, headerName: unknown, value: unknown): Promise<Result<WorkspaceConnector>> {
262 const may = owners(ctx, "change connectors");
263 if (!may.ok) return may;
264 const row = await rowFor(ctx, id);
265 if (!row.ok) return row;
266 const done = await connectors.setKey(ctx.db, await connectorDeps(ctx), row.value, typeof headerName === "string" ? headerName : null, String(value ?? ""));
267 if (!done.ok) return done;
268 ctx.audit?.("set_connector_key", `customize/connectors/${row.value.name}`, `Put a key on the connector ${row.value.title}`);
269 return oneConnector(ctx, row.value.id);
270}
271
272async function removeConnector(ctx: ViewContext, id: unknown): Promise<Result<null>> {
273 const may = owners(ctx, "remove connectors");
274 if (!may.ok) return may;
275 const row = await rowFor(ctx, id);
276 if (!row.ok) return row;
277 const done = await connectors.removeConnector(ctx.db, await connectorDeps(ctx), ctx.workspaceId, row.value.id);
278 if (done.ok) ctx.audit?.("remove_connector", `customize/connectors/${row.value.name}`, `Removed the connector ${row.value.title}`);
279 return done;
280}
281
282async function refreshConnector(ctx: ViewContext, id: unknown): Promise<Result<WorkspaceConnector>> {
283 const may = owners(ctx, "change connectors");
284 if (!may.ok) return may;
285 const row = await rowFor(ctx, id);
286 if (!row.ok) return row;
287 await connectors.refreshTools(ctx.db, await connectorDeps(ctx), row.value);
288 return oneConnector(ctx, row.value.id);
289}
290
291async function startSignIn(ctx: ViewContext, id: unknown, redirectUri: unknown): Promise<Result<{ url: string }>> {
292 const may = owners(ctx, "sign connectors in");
293 if (!may.ok) return may;
294 const row = await rowFor(ctx, id);
295 if (!row.ok) return row;
296 const redirect = String(redirectUri ?? "");
297 const site = (env(ctx).SITE_URL ?? "https://g1t.sh").replace(/\/+$/, "");
298 if (!redirect.startsWith(`${site}/`)) return fail("invalid", "The sign-in must come back to this g1t.");
299 return connectors.startSignIn(ctx.db, await connectorDeps(ctx), row.value, redirect);
300}
301
302async function finishSignIn(ctx: ViewContext, state: unknown, code: unknown): Promise<Result<WorkspaceConnector>> {
303 const may = owners(ctx, "sign connectors in");
304 if (!may.ok) return may;
305 const id = connectors.connectorOfState(String(state ?? ""));
306 if (!id) return fail("invalid", "This sign-in isn't one that was started here.");
307 const row = await rowFor(ctx, id);
308 if (!row.ok) return row;
309 const done = await connectors.finishSignIn(ctx.db, await connectorDeps(ctx), row.value, String(state), String(code ?? ""), ctx.viewer.username);
310 if (!done.ok) return done;
311 ctx.audit?.("sign_in_connector", `customize/connectors/${row.value.name}`, `Signed the connector ${row.value.title} in (${JSON.parse(done.value.tools || "[]").length} tools)`);
312 await answerListing(ctx, listingRef("connector", row.value.name));
313 return oneConnector(ctx, row.value.id);
314}
315
316async function signOut(ctx: ViewContext, id: unknown): Promise<Result<WorkspaceConnector>> {
317 const may = owners(ctx, "change connectors");
318 if (!may.ok) return may;
319 const row = await rowFor(ctx, id);
320 if (!row.ok) return row;
321 const done = await connectors.signOut(ctx.db, await connectorDeps(ctx), row.value);
322 if (!done.ok) return done;
323 ctx.audit?.("sign_out_connector", `customize/connectors/${row.value.name}`, `Signed the connector ${row.value.title} out`);
324 return oneConnector(ctx, row.value.id);
325}
326
327/** `target`: an agent's handle or a team's slug, resolved to what grants keep (the agent's id, the team's slug). */
328async function grantTarget(ctx: ViewContext, scope: SkillScope, target: unknown): Promise<Result<string>> {
329 if (scope === "workspace") return ok("");
330 const key = String(target ?? "").trim().replace(/^@/, "").toLowerCase();
331 if (!key) return fail("invalid", "Say which team or agent.");
332 if (scope === "agent") {
333 const agent = await ctx.db.prepare("SELECT id FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL").bind(ctx.workspaceId, key).first<{ id: string }>();
334 return agent ? ok(agent.id) : fail("not_found", `There is no agent called @${key}.`);
335 }
336 const teams = await identityClient(env(ctx).IDENTITY)
337 .listTeams(ctx.viewer, ctx.slug)
338 .catch(() => null);
339 return teams?.ok && teams.value.some((t) => t.slug === key) ? ok(key) : fail("not_found", `There is no team called ${key}.`);
340}
341
342async function grantConnector(ctx: ViewContext, id: unknown, scope: unknown, target: unknown): Promise<Result<WorkspaceConnector>> {
343 const may = owners(ctx, "give connectors to agents");
344 if (!may.ok) return may;
345 const row = await rowFor(ctx, id);
346 if (!row.ok) return row;
347 const where = scope === "team" || scope === "agent" ? scope : "workspace";
348 const resolved = await grantTarget(ctx, where, target);
349 if (!resolved.ok) return resolved;
350 const done = await connectors.grant(ctx.db, await connectorDeps(ctx), ctx.workspaceId, row.value.id, where, resolved.value, ctx.viewer.username);
351 if (!done.ok) return done;
352 ctx.audit?.("grant_connector", `customize/connectors/${row.value.name}`, `Gave the connector ${row.value.title} to ${where === "workspace" ? "every agent" : where === "team" ? `the team ${resolved.value}` : `@${String(target).replace(/^@/, "")}`}`);
353 return oneConnector(ctx, row.value.id);
354}
355
356async function revokeConnector(ctx: ViewContext, id: unknown, grantId: unknown): Promise<Result<WorkspaceConnector>> {
357 const may = owners(ctx, "change where connectors reach");
358 if (!may.ok) return may;
359 const row = await rowFor(ctx, id);
360 if (!row.ok) return row;
361 const done = await connectors.revoke(ctx.db, ctx.workspaceId, String(grantId ?? ""));
362 if (!done.ok) return done;
363 ctx.audit?.("revoke_connector", `customize/connectors/${row.value.name}`, `Took the connector ${row.value.title} back from somewhere it reached`);
364 return oneConnector(ctx, row.value.id);
365}
366
367async function agentConnectors(ctx: ViewContext, handle: unknown): Promise<Result<AgentConnector[]>> {
368 const key = String(handle ?? "").trim().replace(/^@/, "").toLowerCase();
369 const agent = await ctx.db.prepare("SELECT * FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL").bind(ctx.workspaceId, key).first<Parameters<typeof definitionOf>[0]>();
370 if (!agent) return fail("not_found", `There is no agent called @${key}.`);
371 const teams = await identityClient(env(ctx).IDENTITY)
372 .agentTeams(ctx.slug, agent.id)
373 .catch(() => []);
374 const reach = await connectors.reaching(ctx.db, ctx.workspaceId, agent.id, teams.map((t) => t.slug));
375 const names = new Map(teams.map((t) => [t.slug, t.name]));
376 return ok(connectors.agentConnectorsOut(reach, definitionOf(agent).abilities.mcp_servers ?? [], { teamName: (slug) => names.get(slug) ?? slug }));
377}
378
379// ── The bundle a runner gets ─────────────────────────────────────────────
380
381/**
382 * What a job's Claude Code receives: read with the job's agent token (or a
383 * workspace token), so what reaches it is what reaches that agent. Secrets
384 * are in it: the API hands it to the runner only.
385 */
386async function bundle(ctx: ViewContext): Promise<Result<CustomizeBundle>> {
387 const kind = ctx.viewer.kind ?? "user";
388 if (kind !== "agent" && kind !== "workspace" && !ctx.owner) return fail("forbidden", "The bundle is read by a job's agent token.");
389 const agent = kind === "agent" ? await ctx.db.prepare("SELECT id FROM agents WHERE workspace_id = ? AND handle = ? AND archived_at IS NULL").bind(ctx.workspaceId, ctx.viewer.username.toLowerCase()).first<{ id: string }>() : null;
390 const teams = agent
391 ? await identityClient(env(ctx).IDENTITY)
392 .agentTeams(ctx.slug, agent.id)
393 .then((t) => t.map((x) => x.slug))
394 .catch(() => [])
395 : [];
396 const installs = (await plugins.listInstalls(ctx.db, ctx.workspaceId)).filter((row) => row.enabled === 1);
397 const skillRows = await ctx.db
398 .prepare(
399 `SELECT DISTINCT s.id, s.name, s.plugin, a.version FROM skill_attachments a
400 JOIN skills s ON s.id = a.skill_id AND s.archived_at IS NULL AND s.status = 'published'
401 WHERE a.workspace_id = ?1 AND (a.scope = 'workspace' OR (a.scope = 'agent' AND a.target = ?2) OR (a.scope = 'team' AND a.target IN (SELECT value FROM json_each(?3))))
402 LIMIT 200`,
403 )
404 .bind(ctx.workspaceId, agent?.id ?? "", JSON.stringify(teams))
405 .all<{ id: string; name: string; plugin: string | null; version: number }>();
406 const skills: CustomizeBundle["skills"] = [];
407 for (const row of skillRows.results) {
408 // A plugin's skills reach Claude Code with the plugin itself.
409 if (row.plugin) continue;
410 const version = await ctx.db.prepare("SELECT skill_md, files FROM skill_versions WHERE skill_id = ? AND version = ?").bind(row.id, row.version).first<{ skill_md: string; files: string }>();
411 if (!version) continue;
412 let files: SkillFile[] = [];
413 try {
414 files = JSON.parse(version.files) as SkillFile[];
415 } catch {
416 files = [];
417 }
418 skills.push({ name: row.name, plugin: null, files: [{ path: "SKILL.md", content: version.skill_md, encoding: "utf8" }, ...files.map((f) => ({ path: f.path, content: f.content, encoding: f.encoding ?? "utf8" }))] });
419 }
420 const deps = await connectorDeps(ctx);
421 const reach = agent ? await connectors.reaching(ctx.db, ctx.workspaceId, agent.id, teams) : (await connectors.listConnectors(ctx.db, ctx.workspaceId)).map((row) => ({ row, via: "workspace" as const }));
422 const out: CustomizeBundle["connectors"] = [];
423 for (const { row } of reach) {
424 if (connectors.stateOf(row) !== "connected") continue;
425 const headers = await connectors.headersFor(ctx.db, deps, row);
426 if (headers) out.push({ name: row.name, url: row.url, headers });
427 }
428 return ok({
429 plugins: installs.map((row) => ({ ref: row.ref, name: row.name, fetch: plugins.installOut(row).fetch, version: row.version })),
430 skills,
431 connectors: out,
432 });
433}
434
435// ── Dispatch ─────────────────────────────────────────────────────────────
436
437/** Answers a Customize method, or null when `method` isn't one. */
438export async function customizeRpc(
439 method: string,
440 args: any,
441 view: <T>(a: { workspace: string; viewer: User | null }, run: (ctx: ViewContext) => Promise<Result<T>>) => Promise<Result<T>>,
442): Promise<Result<unknown> | null> {
443 switch (method) {
444 case "marketplaces":
445 return view(args, listMarketplaces);
446 case "add_marketplace":
447 return view(args, (ctx) => addMarketplace(ctx, args.address, args.name));
448 case "remove_marketplace":
449 return view(args, (ctx) => removeMarketplace(ctx, args.name));
450 case "refresh_marketplace":
451 return view(args, (ctx) => refreshMarketplace(ctx, args.name));
452 case "discover":
453 return view(args, discover);
454 case "plugin":
455 return view(args, (ctx) => pluginDetail(ctx, args.ref));
456 case "plugin_installs":
457 return view(args, pluginInstalls);
458 case "install_plugin":
459 return view(args, (ctx) => installPlugin(ctx, args.ref));
460 case "set_plugin_enabled":
461 return view(args, (ctx) => setPluginEnabled(ctx, args.ref, args.enabled));
462 case "uninstall_plugin":
463 return view(args, (ctx) => uninstallPlugin(ctx, args.ref));
464 case "update_plugin":
465 return view(args, (ctx) => updatePlugin(ctx, args.ref));
466 case "plugin_agents":
467 return view(args, pluginAgents);
468 case "connectors":
469 return view(args, listConnectors);
470 case "connector":
471 return view(args, (ctx) => oneConnector(ctx, args.id));
472 case "add_connector":
473 return view(args, (ctx) => addConnector(ctx, args.input));
474 case "set_connector_key":
475 return view(args, (ctx) => setConnectorKey(ctx, args.id, args.header_name, args.value));
476 case "remove_connector":
477 return view(args, (ctx) => removeConnector(ctx, args.id));
478 case "refresh_connector":
479 return view(args, (ctx) => refreshConnector(ctx, args.id));
480 case "start_connector_sign_in":
481 return view(args, (ctx) => startSignIn(ctx, args.id, args.redirect_uri));
482 case "finish_connector_sign_in":
483 return view(args, (ctx) => finishSignIn(ctx, args.state, args.code));
484 case "sign_out_connector":
485 return view(args, (ctx) => signOut(ctx, args.id));
486 case "grant_connector":
487 return view(args, (ctx) => grantConnector(ctx, args.id, args.scope, args.target));
488 case "revoke_connector":
489 return view(args, (ctx) => revokeConnector(ctx, args.id, args.grant));
490 case "agent_connectors":
491 return view(args, (ctx) => agentConnectors(ctx, args.handle));
492 case "customize_bundle":
493 return view(args, bundle);
494 default:
495 return null;
496 }
497}
498
499export type { McpServer };

This file's history is long; its oldest lines are credited to the oldest commit read.