Public: anyone can read this; the crew can change it.
An agent installs the OpenCode plugin from the forge it is reading: llms.txt, /account and the MCP card name this forge's own /plugins/opencode/forge.ts #11107
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/11089-an-agent-installs-the-opencode-plugin-fr → main 3 commits fork 2 threads · 2 unresolved
4 files
changed +51
−14
src/core/mcp_card.ts +14 −5
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | * later standard's field names; when one settles, the card follows it. |
| 8 | 8 | */ |
| 9 | 9 | import type { Database } from "bun:sqlite"; |
| 10 | import { opencodeInstall } from "../http/plugin_routes.ts"; | |
| 10 | 11 | import { running, type Running } from "../kernel/version.ts"; |
| 11 | 12 | import { forgeSource } from "../settings/forge_source.ts"; |
| 12 | 13 | import { repoVisibility } from "./repos.ts"; |
| @@ -62,6 +63,8 @@ export interface HarnessPlugin { | ||
| 62 | 63 | readonly harness: string; |
| 63 | 64 | readonly label: string; |
| 64 | 65 | readonly path: string; |
| 66 | /** True when the install clones the source repository (so it needs one offered); false when this forge serves the file itself. */ | |
| 67 | readonly cloned: boolean; | |
| 65 | 68 | /** The one line, given the origin it was read from and the source repository; it works in an empty directory. */ |
| 66 | 69 | readonly install: (origin: string, repo: string) => string; |
| 67 | 70 | /** What to set before it runs. */ |
| @@ -105,6 +108,7 @@ export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [ | ||
| 105 | 108 | harness: "claude-code", |
| 106 | 109 | label: "Claude Code", |
| 107 | 110 | path: "plugins/claude-code", |
| 111 | cloned: true, | |
| 108 | 112 | install: (o, r) => |
| 109 | 113 | `git clone --depth 1 --filter=blob:none --sparse ${pluginSource(o, r)} forge-plugin && git -C forge-plugin sparse-checkout set plugins/claude-code && claude --plugin-dir forge-plugin/plugins/claude-code`, |
| 110 | 114 | setup: "set FORGE_URL, then sign in through /mcp with the forge server; or also set FORGE_TOKEN and use the forge-token server", |
| @@ -113,12 +117,17 @@ export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [ | ||
| 113 | 117 | harness: "opencode", |
| 114 | 118 | label: "OpenCode", |
| 115 | 119 | path: "plugins/opencode/forge.ts", |
| 116 | install: (o, r) => | |
| 117 | `mkdir -p .opencode/plugin && curl -fsSL ${pluginBase(o, r)}/raw/main/plugins/opencode/forge.ts -o .opencode/plugin/forge.ts`, | |
| 120 | cloned: false, | |
| 121 | install: (o) => opencodeInstall(o), | |
| 118 | 122 | setup: "set FORGE_URL and FORGE_TOKEN", |
| 119 | 123 | }, |
| 120 | 124 | ]; |
| 121 | 125 | |
| 126 | /** The plugins this forge can offer: the ones it serves itself always, the cloned ones only while a public source exists. */ | |
| 127 | export function offeredPlugins(repo: string | undefined): readonly HarnessPlugin[] { | |
| 128 | return HARNESS_PLUGINS.filter((p) => !p.cloned || repo !== undefined); | |
| 129 | } | |
| 130 | ||
| 122 | 131 | /** |
| 123 | 132 | * The server card. The format is still a draft, so only what we can keep |
| 124 | 133 | * true is named: who answers, where, over what transport, how to sign in, |
| @@ -136,11 +145,11 @@ export function mcpCard(origin: string, repo: string | undefined, now: Running = | ||
| 136 | 145 | documentation: `${origin}${DISCOVERY.docs}`, |
| 137 | 146 | openapi: `${origin}${DISCOVERY.openapi}`, |
| 138 | 147 | tokenFreeTools: [...TOKEN_FREE_TOOLS], |
| 139 | plugins: (repo === undefined ? [] : HARNESS_PLUGINS).map((p) => ({ | |
| 148 | plugins: offeredPlugins(repo).map((p) => ({ | |
| 140 | 149 | harness: p.harness, |
| 141 | 150 | path: p.path, |
| 142 | source: pluginSource(origin, repo!), | |
| 143 | install: p.install(origin, repo!), | |
| 151 | ...(repo !== undefined ? { source: pluginSource(origin, repo) } : {}), | |
| 152 | install: p.install(origin, repo ?? ""), | |
| 144 | 153 | })), |
| 145 | 154 | }; |
| 146 | 155 | } |
src/web/standards.ts +11 −6
| @@ -44,7 +44,7 @@ import { notFound } from "../http/respond.ts"; | ||
| 44 | 44 | import { SHARE_TARGET } from "./share_target.ts"; |
| 45 | 45 | import { SITE_LINE } from "./site_line.ts"; |
| 46 | 46 | import { mcpToolsSection } from "./llms_tools.ts"; |
| 47 | import { apiCatalog, DISCOVERY, HARNESS_PLUGINS, mcpCard, mcpPointers, offeredPluginRepo, pluginSource } from "../core/mcp_card.ts"; | |
| 47 | import { apiCatalog, DISCOVERY, mcpCard, offeredPlugins, mcpPointers, offeredPluginRepo, pluginSource } from "../core/mcp_card.ts"; | |
| 48 | 48 | import { forgeDocument } from "../core/standards_index.ts"; |
| 49 | 49 | import { running } from "../kernel/version.ts"; |
| 50 | 50 | import { timePhase, timePhaseAsync } from "../core/route_timing.ts"; |
| @@ -411,6 +411,15 @@ export function llmsBody(ctx: Ctx): { body: string; etag: string } { | ||
| 411 | 411 | } |
| 412 | 412 | |
| 413 | 413 | /** The llms.txt document for one origin. */ |
| 414 | /** One llms.txt line per plugin on offer; a cloned one names its source, a served one says so. */ | |
| 415 | function pluginLines(o: string, plugins: string | undefined): string[] { | |
| 416 | const lines = offeredPlugins(plugins).map((p) => { | |
| 417 | const where = p.cloned && plugins !== undefined ? `source ${pluginSource(o, plugins)}, ` : "served by this forge, "; | |
| 418 | return `- ${p.label} plugin (${where}path ${p.path}; ${p.setup}): \`${p.install(o, plugins ?? "")}\``; | |
| 419 | }); | |
| 420 | return plugins === undefined ? [...lines, "- Claude Code plugin: not offered here (no public source)"] : lines; | |
| 421 | } | |
| 422 | ||
| 414 | 423 | function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: string, repos: LlmsListing, signedIn: boolean, plugins: string | undefined): string { |
| 415 | 424 | const mcp = mcpPointers(o); |
| 416 | 425 | return ( |
| @@ -445,11 +454,7 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s | ||
| 445 | 454 | "## Use it from your harness", |
| 446 | 455 | "", |
| 447 | 456 | `- MCP endpoint: ${mcp.url} (streamable HTTP; OAuth metadata at ${mcp.oauth}). Or send a token as Authorization: Bearer.`, |
| 448 | ...(plugins !== undefined | |
| 449 | ? HARNESS_PLUGINS.map( | |
| 450 | (p) => `- ${p.label} plugin (source ${pluginSource(o, plugins)}, path ${p.path}; ${p.setup}): \`${p.install(o, plugins)}\``, | |
| 451 | ) | |
| 452 | : ["- Harness plugins: not offered here (no public source)"]), | |
| 457 | ...pluginLines(o, plugins), | |
| 453 | 458 | "", |
| 454 | 459 | "## What you can do on any page", |
| 455 | 460 | "", |
test/mcp-card-plugins.test.ts +3 −3
| @@ -67,7 +67,7 @@ test("card and llms.txt name both plugins, their install lines, and paths that e | ||
| 67 | 67 | expect(claude.install).toContain("--sparse"); |
| 68 | 68 | expect(claude.install).toContain("--plugin-dir forge-plugin/plugins/claude-code"); |
| 69 | 69 | const oc = card.plugins.find((p: any) => p.harness === "opencode"); |
| 70 | expect(oc.install).toContain(`curl -fsSL ${servers.url}/trevato/forge/raw/main/plugins/opencode/forge.ts`); | |
| 70 | expect(oc.install).toContain(`${servers.url}/plugins/opencode/forge.ts`); | |
| 71 | 71 | for (const p of card.plugins) expect(existsSync(join(import.meta.dir, "..", p.path))).toBe(true); |
| 72 | 72 | expect(card.name).toBe("forge"); |
| 73 | 73 | }); |
| @@ -113,10 +113,10 @@ test("the upstream stands in while the own repository is private or unset, and w | ||
| 113 | 113 | await setVisibility("public"); |
| 114 | 114 | expect(offeredPluginRepo(app.db, "trevato/forge", UPSTREAM_REPO)).toBe("trevato/forge"); |
| 115 | 115 | await setVisibility("private"); |
| 116 | // Neither source: no repository, which llms.txt prints as the "not offered" line. | |
| 116 | // Neither source: the cloned plugin is not offered; the one this forge serves still is. | |
| 117 | 117 | expect(offeredPluginRepo(bare.db, undefined, undefined)).toBeUndefined(); |
| 118 | 118 | expect(offeredPluginRepo(app.db, "trevato/forge", undefined)).toBeUndefined(); |
| 119 | 119 | const { mcpCard } = await import("../src/core/mcp_card.ts"); |
| 120 | expect(mcpCard("http://x.test", undefined).plugins).toEqual([]); | |
| 120 | expect(mcpCard("http://x.test", undefined).plugins.map((p: any) => p.harness)).toEqual(["opencode"]); | |
| 121 | 121 | bare.close(); |
| 122 | 122 | }); |
test/plugin-routes.test.ts +23
| @@ -5,6 +5,8 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 5 | 5 | import { tmpdir } from "node:os"; |
| 6 | 6 | import { join } from "node:path"; |
| 7 | 7 | import { createApp, type App } from "../src/app.ts"; |
| 8 | import { mcpCard } from "../src/core/mcp_card.ts"; | |
| 9 | import { opencodeInstall } from "../src/http/plugin_routes.ts"; | |
| 8 | 10 | import { reservedName } from "../src/core/reserved.ts"; |
| 9 | 11 | import { serve, type Servers } from "../src/http/server.ts"; |
| 10 | 12 | import { quietLogs } from "./helpers/globals.ts"; |
| @@ -59,3 +61,24 @@ test("the Dockerfile copies plugins/opencode", () => { | ||
| 59 | 61 | .flatMap((l) => l.trim().split(/\s+/).slice(1, -1).map((p) => p.replace(/\/$/, ""))); |
| 60 | 62 | expect(copies).toContain("plugins/opencode"); |
| 61 | 63 | }); |
| 64 | ||
| 65 | test("the printed install command, run, fetches the plugin's bytes from this forge", async () => { | |
| 66 | const work = await mkdtemp(join(tmpdir(), "plugin-install-")); | |
| 67 | try { | |
| 68 | const run = Bun.spawn(["sh", "-c", opencodeInstall(servers.url)], { cwd: work }); | |
| 69 | expect(await run.exited).toBe(0); | |
| 70 | expect(readFileSync(join(work, ".opencode/plugin/forge.ts"), "utf8")).toBe(readFileSync(join(ROOT, "plugins/opencode/forge.ts"), "utf8")); | |
| 71 | } finally { | |
| 72 | await rm(work, { recursive: true, force: true }); | |
| 73 | } | |
| 74 | }); | |
| 75 | ||
| 76 | test("llms.txt and the card print opencodeInstall, never the code host", async () => { | |
| 77 | const cmd = opencodeInstall(servers.url); | |
| 78 | const llms = await (await fetch(`${servers.url}/llms.txt`)).text(); | |
| 79 | expect(llms).toContain(cmd); | |
| 80 | expect(llms).not.toMatch(/OpenCode[^\n]*forge\.open-platform\.sh/); | |
| 81 | expect(llms).not.toMatch(/raw\/main\/plugins\/opencode/); | |
| 82 | const card = mcpCard(servers.url, "trevato/forge"); | |
| 83 | expect(card.plugins.find((p: any) => p.harness === "opencode")?.install).toBe(cmd); | |
| 84 | }); |