Public: anyone can read this; the crew can change it.

A forge whose own repository is private still offers the Claude Code and OpenCode plugins, from its upstream #10901

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/10895-a-forge-whose-own-repository-is-private → main 3 commits fork 4 threads · 4 unresolved

  • Merged. mason/10895-a-forge-whose-own-repository-is-private was merged into main as bc5b7ef.

Review

Sign in to review this change.
11 files changed +65 −34
src/core/mcp_card.ts +18 −10
@@ -38,16 +38,24 @@ export interface HarnessPlugin {
3838 }
3939
4040 /**
41 * The repository that holds the plugins, as `owner/name`: the forge's
42 * configured source (admin setting, else `FORGE_REPO`), and only while it is
43 * public here, so every printed line works for whoever reads it. The card,
44 * /llms.txt, Settings › Tokens and the terminal all ask this one question.
41 * The repository that holds the plugins, as `owner/name`, or the upstream's
outdated

The comment is run together and still says 'only while it is public here', which is no longer true. Rewrite it.

42 * full URL: the forge's own source (admin setting, else `FORGE_REPO`) while it
43 * is public here, else `upstream`, so every printed line works for whoever
44 * reads it. Undefined only when neither is available. The card, /llms.txt,
45 * Settings › Tokens and the terminal all ask this one question.
4546 */
46 export function offeredPluginRepo(db: Database, fromEnv: string | undefined): string | undefined {
47 export function offeredPluginRepo(db: Database, fromEnv: string | undefined, upstream: string | undefined): string | undefined {
4748 const repo = forgeSource(db, fromEnv);
48 if (repo === undefined) return undefined;
49 const [owner = "", name = ""] = repo.split("/");
50 return repoVisibility(db, owner, name) === "public" ? repo : undefined;
49 if (repo !== undefined) {
50 const [owner = "", name = ""] = repo.split("/");
51 if (repoVisibility(db, owner, name) === "public") return repo;
52 }
outdated

This always returns a string, so the 'not offered' path in standards.ts is dead and box 1's 'undefined only when neither is available' is not met. Narrow the type and remove the dead branches, or make the upstream optional.

53 return upstream;
54 }
55
56 /** Where a plugin repository lives: `owner/name` is on this forge's origin, a full URL is its own. */
outdated

This is the third copy of the upstream URL (see src/web/studio/home.ts:686 and standards.ts). Export a single constant and use it everywhere.

57 function pluginBase(origin: string, repo: string): string {
58 return repo.startsWith("https://") ? repo : `${origin}/${repo}`;
5159 }
5260
5361 /** The two Claude Code lines that install the plugin from the marketplace. */
@@ -57,7 +65,7 @@ export function marketplaceLines(origin: string, repo: string): { add: string; i
5765
5866 /** The clone URL of that repository on this forge's origin. */
5967 export function pluginSource(origin: string, repo: string): string {
60 return `${origin}/${repo}.git`;
68 return `${pluginBase(origin, repo)}.git`;
6169 }
6270
6371 /** The one list the card and /llms.txt both read, so they cannot drift. */
@@ -75,7 +83,7 @@ export const HARNESS_PLUGINS: readonly HarnessPlugin[] = [
7583 label: "OpenCode",
7684 path: "plugins/opencode/forge.ts",
7785 install: (o, r) =>
78 `mkdir -p .opencode/plugin && curl -fsSL ${o}/${r}/raw/main/plugins/opencode/forge.ts -o .opencode/plugin/forge.ts`,
86 `mkdir -p .opencode/plugin && curl -fsSL ${pluginBase(o, r)}/raw/main/plugins/opencode/forge.ts -o .opencode/plugin/forge.ts`,
7987 setup: "set FORGE_URL and FORGE_TOKEN",
8088 },
8189 ];
src/core/upstream.ts added +2
@@ -0,0 +1,2 @@
1 /** The public repository every forge comes from; the one place its address is written. */
2 export const UPSTREAM_REPO = "https://forge.open-platform.sh/trevato/forge";
src/tui/screens/signins.ts +2 −1
@@ -21,6 +21,7 @@
2121 * ended from here.
2222 */
2323
24 import { UPSTREAM_REPO } from "../../core/upstream.ts";
2425 import { accountSignIns, endAccountSession } from "../../actions/signins.ts";
2526 import {
2627 CHANGE_PASSWORD_WORDS,
@@ -213,7 +214,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
213214 return;
214215 }
215216 note = `Made ${name} (${scope}).`;
216 const repo = offeredPluginRepo(f.db, f.app.config.forgeRepo);
217 const repo = offeredPluginRepo(f.db, f.app.config.forgeRepo, UPSTREAM_REPO);
217218 const origin = publicOrigin(f.app.config.url, `http://${f.app.config.host ?? "localhost"}:${f.app.config.port ?? 3000}`);
218219 ctx.push(secretScreen(ctx, made.value.token, repo === undefined ? null : marketplaceLines(origin, repo)));
219220 };
src/web/pages/account.ts +2 −1
@@ -9,6 +9,7 @@
99 * has been read once.
1010 */
1111
12 import { UPSTREAM_REPO } from "../../core/upstream.ts";
1213 import { resendVerification } from "../../actions/verify.ts";
1314 import { clientAddress } from "../../http/peer.ts";
1415 import { emailVerified } from "../../core/verify.ts";
@@ -718,7 +719,7 @@ export function connectCard(ctx: Ctx, token: string | undefined): Html {
718719 const url = publicOrigin(ctx.app.config.url, ctx.url.origin);
719720 const env = `FORGE_URL=${url} FORGE_TOKEN=${token ?? "<token>"}`;
720721 // The plugin marketplace is the forge's configured source, offered only while anyone can clone it.
721 const repo = offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo);
722 const repo = offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo, UPSTREAM_REPO);

Non-blocking: stale comment; the marketplace is now the forge own source while public, else the upstream.

722723 const market = repo === undefined ? null : marketplaceLines(url, repo);
723724 const clone = copyField("connect-claude", `${env} claude --plugin-dir plugins/claude-code`, "Claude Code command", "Copy command");
724725 return block(
src/web/standards.ts +5 −4
@@ -25,6 +25,7 @@
2525 * FORGE_SECURITY_CONTACT.
2626 */
2727
28 import { UPSTREAM_REPO } from "../core/upstream.ts";
2829 import { CRAWL_DELAY_S, DEEP_SEGMENTS } from "../http/crawl.ts";
2930 import { markPng, type MarkShape } from "../design/mark.ts";
3031 import { metaThemeColor } from "../design/themes.ts";
@@ -109,7 +110,7 @@ export function securityTxt(ctx: Ctx): Response {
109110
110111 /** GET /.well-known/mcp.json — the MCP server card (a draft format; see docs/MCP.md). */
111112 export function mcpJson(ctx: Ctx): Response {
112 const res = text(JSON.stringify(mcpCard(ctx.url.origin, offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo)), null, 2), "application/json");
113 const res = text(JSON.stringify(mcpCard(ctx.url.origin, offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo, UPSTREAM_REPO)), null, 2), "application/json");
113114 res.headers.set("access-control-allow-origin", "*");
114115 return res;
115116 }
@@ -378,7 +379,7 @@ export function llmsBody(ctx: Ctx): { body: string; etag: string } {
378379 const o = ctx.url.origin;
379380 const tell = sshTellCommand(ctx.app);
380381 const running = runningSentence();
381 const plugins = offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo);
382 const plugins = offeredPluginRepo(ctx.app.db, ctx.app.config.forgeRepo, UPSTREAM_REPO);
382383 const repos = timePhase(ctx.request, "repos", () => llmsRepos(ctx.app.db));
383384 // The tools section depends on who asks, so the signed-in and the anonymous body are kept apart.
384385 const signedIn = ctx.auth.user !== undefined;
@@ -411,8 +412,8 @@ function renderLlmsTxt(ctx: Ctx, o: string, tell: string | undefined, running: s
411412 "If you are an assistant helping a person: say plainly what you can and cannot do from where you are. If you cannot reach this forge or hold an account on it, give the person these steps to do by hand.",
412413 "",
413414 `- Help build a public app here: each one is a repository at ${o}/{owner}/{name}. A person makes an account at ${o}/signup, then follows "Offer a change" below. If /signup answers 403, new accounts here are by invitation for now: send a suggestion through "Report a problem" below, or work from a forge of your own.`,
414 `- Get your own forge: \`git clone https://forge.open-platform.sh/trevato/forge.git && cd forge && bun install && bun run src/main.ts\`, then in a second terminal \`bun src/main.ts --first-admin\`, which prints a one-time link that makes you the admin. It needs Bun; its crew needs the claude CLI and a model credential. Or a hosted one, free within limits while hosting is not sold: sign in and open ${o}/forges/new (or call the MCP \`spawn\` tool with the person's consent). It sleeps when idle and wakes on its next page load, push or API call; the person brings their own model credential, or its crew stays off.`,
415 "- Work from your own forge: connect it to this one as an origin, bring an app in, and send your work back. How: https://forge.open-platform.sh/trevato/forge/raw/main/docs/ORIGINS.md",
415 `- Get your own forge: \`git clone ${UPSTREAM_REPO}.git && cd forge && bun install && bun run src/main.ts\`, then in a second terminal \`bun src/main.ts --first-admin\`, which prints a one-time link that makes you the admin. It needs Bun; its crew needs the claude CLI and a model credential. Or a hosted one, free within limits while hosting is not sold: sign in and open ${o}/forges/new (or call the MCP \`spawn\` tool with the person's consent). It sleeps when idle and wakes on its next page load, push or API call; the person brings their own model credential, or its crew stays off.`,
416 `- Work from your own forge: connect it to this one as an origin, bring an app in, and send your work back. How: ${UPSTREAM_REPO}/raw/main/docs/ORIGINS.md`,
416417 `- Connect an assistant that can use tools: add a custom connector with the URL ${mcp.url}. It signs in with OAuth; only the report tool works without an account.`,
417418 "",
418419 "## Report a problem (no account)",
src/web/studio/home.ts +2 −1
@@ -6,6 +6,7 @@
66 * best of what is public (`actions/front.ts`).
77 */
88
9 import { UPSTREAM_REPO } from "../../core/upstream.ts";
910 import { foundryHref } from "../../actions/foundry_door.ts";
1011 import { forgeCeiling } from "../../flow/ceiling.ts";
1112 import { liveLine } from "../pages/releases.ts";
@@ -683,7 +684,7 @@ function doorPulse(days: FrontPage["pulse"]["days"]): Html | null {
683684 </figure>`;
684685 }
685686
686 const FORGE_REPO = "https://forge.open-platform.sh/trevato/forge";
687 const FORGE_REPO = UPSTREAM_REPO;
687688
688689 /**
689690 * One press for each shipped pack (#9572): what it is, and the way to its page,
test/connect-harness.test.ts +2 −2
@@ -66,14 +66,14 @@ test("the marketplace lines show only while the forge's plugin repository is pub
6666 const cookie = ((await fetch(`${base}/signin`, { method: "POST", body: new URLSearchParams({ username: "ada", password: "connect-password" }), redirect: "manual" })).headers.get("set-cookie") ?? "").split(";")[0]!;
6767 const get = async () => (await fetch(`${base}/settings/tokens`, { headers: { cookie } })).text();
6868 const url = (app.config.url ?? base).replace(/\/+$/, "");
69 expect(await get()).not.toContain("connect-claude-add");
69 expect(value(await get(), "connect-claude-add")).toBe("/plugin marketplace add https://forge.open-platform.sh/trevato/forge.git"); // no own source: the upstream's
7070 expect(await get()).toContain('id="connect-claude"');
7171
7272 const owner = await createUser(app.db, { origin: "seed", trusted: true, username: "trevato", email: "[email protected]", password: "connect-password" });
7373 if (!owner.ok) throw new Error(owner.error);
7474 const made = await initRepo(app.db, app.reposRoot, { ownerId: owner.value.id, name: "forge", visibility: "private", description: "" });
7575 expect(made).toMatchObject({ ok: true });
76 expect(await get()).not.toContain("connect-claude-add"); // private: no line a stranger could not run
76 expect(value(await get(), "connect-claude-add")).toBe("/plugin marketplace add https://forge.open-platform.sh/trevato/forge.git"); // private: the upstream's, which a stranger can run
7777 app.db.query("UPDATE repo SET visibility = 'public' WHERE name = 'forge'").run();
7878
7979 const page = await get();
test/llms-anonymous.test.ts +1 −1
@@ -46,7 +46,7 @@ const section = (body: string): string[] => {
4646 test("the anonymous text is small, varies by reader, and lists report plus one line", async () => {
4747 const res = await fetch(`${servers.url}/llms.txt`);
4848 const body = await res.text();
49 expect(Buffer.byteLength(body)).toBeLessThan(5 * 1024);
49 expect(Buffer.byteLength(body)).toBeLessThan(6 * 1024);
5050 expect(res.headers.get("vary")).toContain("Cookie");
5151 expect(res.headers.get("vary")).toContain("Authorization");
5252 const listed = section(body);
test/mcp-card-plugins.test.ts +26 −10
@@ -35,12 +35,16 @@ const setVisibility = async (visibility: "public" | "private") => {
3535 if (!r.ok) throw new Error(String(r.error));
3636 };
3737
38 test("a private plugin source offers no plugins in the card or llms.txt", async () => {
38 test("a private own repository offers the upstream's plugins, in the card and llms.txt alike", async () => {
3939 const card = (await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any;
4040 const llms = await fetch(`${servers.url}/llms.txt`).then((r) => r.text());
41 expect(card.plugins).toEqual([]);
42 expect(llms).toContain("Harness plugins: not offered here (no public source)");
43 expect(llms).not.toContain("Claude Code plugin");
41 expect(card.plugins.map((p: any) => p.harness).sort()).toEqual(["claude-code", "opencode"]);
42 for (const p of card.plugins) {
43 expect(p.source).toBe("https://forge.open-platform.sh/trevato/forge.git");
44 expect(llms).toContain(p.install);
45 }
46 expect(llms).not.toContain("not offered here");
47 expect(llms).not.toContain(`${servers.url}/trevato/forge.git`);
4448 expect(llms).not.toContain("needs that repository to be public");
4549 });
4650
@@ -72,8 +76,10 @@ test("llms.txt follows the source going private again, not its cached body", asy
7276 await setVisibility("public");
7377 expect(await fetch(`${servers.url}/llms.txt`).then((r) => r.text())).toContain("OpenCode plugin");
7478 await setVisibility("private");
75 expect(await fetch(`${servers.url}/llms.txt`).then((r) => r.text())).not.toContain("OpenCode plugin");
76 expect(((await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any).plugins).toEqual([]);
79 const text = await fetch(`${servers.url}/llms.txt`).then((r) => r.text());
80 expect(text).toContain("OpenCode plugin");
81 expect(text).not.toContain(`${servers.url}/trevato/forge.git`);
82 expect(((await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any).plugins[0].source).toBe("https://forge.open-platform.sh/trevato/forge.git");
7783 });
7884
7985 test("the plugin repository is the configured source: an admin's setting outranks FORGE_REPO, in the card and llms.txt alike", async () => {
@@ -96,11 +102,21 @@ test("the plugin repository is the configured source: an admin's setting outrank
96102 expect(((await fetch(`${servers.url}/.well-known/mcp.json`).then((r) => r.json())) as any).plugins[0].source).toBe(`${servers.url}/trevato/forge.git`);
97103 });
98104
99 test("a forge that names no source offers no plugins, whatever is public", async () => {
105 test("the upstream stands in while the own repository is private or unset, and with neither nothing is offered", async () => {
100106 const bare = createApp({ dataDir: join(dataDir, "bare"), port: 0 });
101107 const { offeredPluginRepo } = await import("../src/core/mcp_card.ts");
102 expect(offeredPluginRepo(bare.db, bare.config.forgeRepo)).toBeUndefined();
103 expect(offeredPluginRepo(app.db, "trevato/forge")).toBe("trevato/forge");
104 expect(offeredPluginRepo(app.db, "trevato/nothing")).toBeUndefined();
108 const { UPSTREAM_REPO } = await import("../src/core/upstream.ts");
109 await setVisibility("private");
110 expect(offeredPluginRepo(bare.db, bare.config.forgeRepo, UPSTREAM_REPO)).toBe(UPSTREAM_REPO);
111 expect(offeredPluginRepo(app.db, "trevato/forge", UPSTREAM_REPO)).toBe(UPSTREAM_REPO); // private here
112 expect(offeredPluginRepo(app.db, "trevato/nothing", UPSTREAM_REPO)).toBe(UPSTREAM_REPO);
113 await setVisibility("public");
114 expect(offeredPluginRepo(app.db, "trevato/forge", UPSTREAM_REPO)).toBe("trevato/forge");
115 await setVisibility("private");
116 // Neither source: no repository, which llms.txt prints as the "not offered" line.
117 expect(offeredPluginRepo(bare.db, undefined, undefined)).toBeUndefined();
118 expect(offeredPluginRepo(app.db, "trevato/forge", undefined)).toBeUndefined();
119 const { mcpCard } = await import("../src/core/mcp_card.ts");
120 expect(mcpCard("http://x.test", undefined).plugins).toEqual([]);
105121 bare.close();
106122 });
test/standards.test.ts +2 −2
@@ -277,8 +277,8 @@ describe("what the open web expects", () => {
277277 expect(section).toContain(`${base}/mcp`);
278278 expect(section).toContain(`${base}/.well-known/oauth-protected-resource`);
279279 expect(section).toContain("streamable HTTP");
280 // trevato/forge is not here, so no install line is printed that could not work.
281 expect(section).toContain("Harness plugins: not offered here (no public source)");
280 // trevato/forge is not here, so the lines come from the public upstream.
281 expect(section).toContain("Claude Code plugin (source https://forge.open-platform.sh/trevato/forge.git");
282282 expect(section).not.toContain("needs that repository to be public");
283283 });
284284
test/tui-token-make.test.ts +3 −2
@@ -249,16 +249,17 @@ describe("making a token in the terminal", () => {
249249 h.stop();
250250 return text;
251251 };
252 expect(await mint()).not.toContain("/plugin marketplace add");
252 expect(await mint()).toContain(`/plugin marketplace add https://forge.open-platform.sh/trevato/forge.git`); // no own source: the upstream's
253253 const { createRepo } = await import("../src/core/repos.ts");
254254 const owner = await createUser(forge.db, { origin: "seed", trusted: true, username: "trevato", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: false });
255255 if (!owner.ok) throw new Error(owner.error);
256256 const made = await createRepo(forge.db, forge.reposRoot, { ownerId: owner.value.id, name: "forge", visibility: "private", description: "" });
257257 expect(made.ok).toBe(true);
258 expect(await mint()).not.toContain("/plugin marketplace add");
258 expect(await mint()).toContain(`/plugin marketplace add https://forge.open-platform.sh/trevato/forge.git`); // private here: still the upstream's
259259 forge.db.query("UPDATE repo SET visibility = 'public' WHERE name = 'forge'").run();
260260 const shown = await mint();
261261 expect(shown).toContain("/plugin marketplace add");
262 expect(shown).not.toContain("forge.open-platform.sh");
262263 expect(shown).toContain("/trevato/forge.git");
263264 expect(shown).toContain("/plugin install forge@forge");
264265 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs