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

A read-only mirror leaves no trace for a stranger or the crew: pages, changelog, Talk, search, briefs #10676

Merged opened this

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

Merged

mason/forge:mason/10660-a-read-only-mirror-leaves-no-trace-for-a → main 2 commits fork

  • Merged. mason/10660-a-read-only-mirror-leaves-no-trace-for-a was merged into main as f6c178a.

Review

Sign in to review this change.
1 file changed +125 −6
test/origin-read-only-private.test.ts +125 −6
@@ -11,9 +11,12 @@ import { tmpdir } from "node:os";
1111 import { join } from "node:path";
1212 import { createApp, type App } from "../src/app.ts";
1313 import { createSession } from "../src/core/sessions.ts";
14 import { createAgentRow } from "../src/core/agents.ts";
14 import { createAgentRow, type Agent } from "../src/core/agents.ts";
1515 import { createUser } from "../src/core/users.ts";
16 import { createIssue } from "../src/core/issues.ts";
1617 import { getRepo } from "../src/core/repos.ts";
18 import { repoDir } from "../src/git/repos.ts";
19 import { systemPrompt, taskPrompt } from "../src/crew/brief.ts";
1720 import { serve, type Servers } from "../src/http/server.ts";
1821 import { syncsSettled } from "../src/origins/sync.ts";
1922 import { mirrorAndWait, newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
@@ -38,6 +41,8 @@ describe("a mirror of a read-only origin is private to its owner", () => {
3841 let fake: FakeForge;
3942 let originId = "";
4043 const cookies: Record<string, string> = {};
44 let commit = "";
45 let crewAgent: Agent;
4146
4247 const api = (path: string, init?: RequestInit) => {
4348 const headers = new Headers(init?.headers);
@@ -84,8 +89,8 @@ describe("a mirror of a read-only origin is private to its owner", () => {
8489 if (!made.ok) throw new Error(made.error);
8590 const bob = await createUser(app.db, { username: "bob", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true });
8691 const crew = await createUser(app.db, { username: "crewmate", email: "[email protected]", agent: true, origin: "agent" });
87 if (crew.ok) createAgentRow(app.db, { userId: crew.value.id, brief: "builds." });
8892 if (!bob.ok || !crew.ok) throw new Error("users");
93 crewAgent = createAgentRow(app.db, { userId: crew.value.id, brief: "builds." });
8994 cookies["owner"] = `forge_session=${createSession(app.db, made.value.id).token}`;
9095 cookies["stranger"] = `forge_session=${createSession(app.db, bob.value.id).token}`;
9196 cookies["agent"] = `forge_session=${createSession(app.db, crew.value.id).token}`;
@@ -104,6 +109,12 @@ describe("a mirror of a read-only origin is private to its owner", () => {
104109 }),
105110 );
106111 await syncsSettled();
112 // A public repository beside it, so a count has something true to count.
113 await api("/api/v1/repos", { method: "POST", body: JSON.stringify({ name: "openbox", visibility: "public" }) });
114 const head = Bun.spawnSync(["git", "-C", repoDir(app.reposRoot, "alice", NAME), "rev-parse", "HEAD"]);
115 commit = head.stdout.toString().trim();
116 // An issue of its own, so the feeds and searches have a word to leak.
117 createIssue(app.db, { repoId: getRepo(app.db, "alice", NAME)!.id, authorId: made.value.id, title: "zephyrissue to fix", body: "zephyrissue body" });
107118 proveAll(app);
108119 });
109120
@@ -146,19 +157,113 @@ describe("a mirror of a read-only origin is private to its owner", () => {
146157 "/talk",
147158 "/alice",
148159 "/api/v1/users/alice/repos",
160 "/api/v1/repos",
161 "/api/v1/apps",
162 "/api/v1/front",
163 "/api/v1/forge",
164 "/api/v1/map",
165 "/api/v1/here",
166 "/api/v1/users",
167 "/api/v1/users/alice",
168 "/api/v1/crew/now",
169 "/api/v1/thoughts",
170 "/api/v1/family",
171 "/api/v1/fleet/rows",
172 "/api/v1/telemetry/home",
173 "/apps",
174 "/crew",
175 "/map",
176 "/roadmap",
177 "/people",
178 "/llms.txt",
179 "/sitemap.xml",
180 "/feed",
181 "/explore?sort=new",
182 "/explore?q=quillmo",
183 "/api/v1/explore?q=quillmo",
184 "/api/v1/users/alice/repos?q=quillmo",
185 "/talk?about=alice/quillmo",
186 "/changelog?q=zephyrm",
187 "/flow",
188 "/flows",
189 "/returns",
190 "/next",
191 "/now",
192 "/week",
193 "/waves",
194 "/pulls",
195 "/releases",
196 "/bets",
197 "/traffic",
198 "/usage",
199 "/api/v1/returns",
200 "/api/v1/fleet/graph",
201 "/api/v1/fleet/ops",
202 "/api/v1/crew/scorecard",
203 "/api/v1/usage",
204 "/search?q=zephyrm",
205 "/api/v1/search?q=zephyris&kind=all",
206 "/search?q=zephyris",
207 "/changelog?q=zephyris",
149208 ];
150209
151210 for (const who of ["anonymous", "stranger", "agent"]) {
152211 test(`${who} finds no trace of its name or commits`, async () => {
212 const leaks: string[] = [];
153213 for (const path of listings) {
154 const res = await as(who, path);
155 const body = await res.text();
156 expect(body, `${who} ${path}`).not.toContain(NAME);
157 expect(body, `${who} ${path}`).not.toContain("zephyrmarker");
214 const body = await (await as(who, path)).text();
215 if (body.includes(NAME) || body.includes("zephyrmarker") || body.includes("zephyrissue")) leaks.push(path);
158216 }
217 expect(leaks).toEqual([]);
159218 });
160219 }
161220
221 test("the mirror has a commit id to leak, and no listing shows it", async () => {
222 expect(commit).toMatch(/^[0-9a-f]{40}$/);
223 for (const who of ["anonymous", "stranger", "agent"])
224 for (const path of listings) {
225 const body = await (await as(who, path)).text();
226 expect(body.includes(commit) || body.includes(commit.slice(0, 7)), `${who} ${path}`).toBe(false);
227 }
228 });
229
230 test("counts leave it out: filtered before counted", async () => {
231 for (const who of ["anonymous", "stranger", "agent"]) {
232 const repos = (await (await as(who, "/api/v1/users/alice/repos")).json()) as { items: unknown[] };
233 expect(JSON.stringify(repos), who).toContain("openbox");
234 expect(repos.items.length, who).toBe(1);
235 const explore = (await (await as(who, "/api/v1/explore")).json()) as { items: unknown[] };
236 expect(explore.items.length, who).toBeLessThanOrEqual(1);
237 }
238 const owner = (await (await as("owner", "/api/v1/users/alice/repos")).json()) as { items: unknown[] };
239 expect(owner.items.length).toBe(2);
240 });
241
242 test("a crew brief for a run on another repository names nothing of the mirror", () => {
243 const repo = getRepo(app.db, "alice", "openbox")!;
244 const made = createIssue(app.db, { repoId: repo.id, authorId: repo.ownerId, title: "Make the box open", body: "It stays shut." });
245 if (!made.ok) throw new Error(made.error);
246 const input = {
247 agent: crewAgent,
248 repo,
249 issue: made.value,
250 branch: "crewmate/1",
251 forgeUrl: "http://forge.test",
252 isApp: false,
253 resuming: false,
254 };
255 const system = systemPrompt(input);
256 const task = taskPrompt({ ...input, reason: "assigned", comments: [], reviews: [], reviewComments: [], since: "" });
257 // The brief is real: it names the repository the run is on.
258 expect(system).toContain("alice/openbox");
259 expect(task).toContain("Make the box open");
260 for (const text of [system, task]) {
261 expect(text).not.toContain(NAME);
262 expect(text).not.toContain("zephyr");
263 expect(text).not.toContain(commit);
264 }
265 });
266
162267 for (const who of ["anonymous", "stranger", "agent"]) {
163268 test(`${who} is told the repository does not exist`, async () => {
164269 expect((await as(who, `/alice/${NAME}`)).status).toBe(404);
@@ -166,6 +271,20 @@ describe("a mirror of a read-only origin is private to its owner", () => {
166271 });
167272 }
168273
274 test("the owner finds the same reads: changelog, Talk, issue title and commit id", async () => {
275 const found = async (path: string) => (await as("owner", path)).text();
276 expect(await found("/api/v1/search?q=zephyrissue&kind=all")).toContain(NAME);
277 expect(await found("/search?q=zephyrm")).toContain(NAME);
278 expect(await found("/search?q=zephyris")).toContain(NAME);
279 expect(await found("/changelog?q=zephyrm")).toContain(NAME);
280 expect(await found("/changelog?q=zephyris")).toContain(NAME);
281 expect(await found("/talk?about=alice/quillmo")).toContain(NAME);
282 expect(await found("/explore?q=quillmo")).toContain(NAME);
283 expect(await found(`/alice/${NAME}/commits`)).toContain(commit.slice(0, 7));
284 expect(await found(`/api/v1/repos/alice/${NAME}/commits`)).toContain(commit);
285 expect(await found(`/api/v1/repos/alice/${NAME}/issues`)).toContain("zephyrissue");
286 });
287
169288 test("the owner finds it", async () => {
170289 for (const path of ["/search?q=quillmo", "/api/v1/search?q=quillmo&kind=all", `/search?q=${NAME}`, `/api/v1/search?q=${NAME}&kind=all`, "/alice", `/alice/${NAME}`, `/api/v1/repos/alice/${NAME}`]) {
171290 const res = await as("owner", path);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs