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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10660-a-read-only-mirror-leaves-no-trace-for-a → main 2 commits fork
1 file
changed +125
−6
test/origin-read-only-private.test.ts +125 −6
| @@ -11,9 +11,12 @@ import { tmpdir } from "node:os"; | ||
| 11 | 11 | import { join } from "node:path"; |
| 12 | 12 | import { createApp, type App } from "../src/app.ts"; |
| 13 | 13 | 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"; | |
| 15 | 15 | import { createUser } from "../src/core/users.ts"; |
| 16 | import { createIssue } from "../src/core/issues.ts"; | |
| 16 | 17 | import { getRepo } from "../src/core/repos.ts"; |
| 18 | import { repoDir } from "../src/git/repos.ts"; | |
| 19 | import { systemPrompt, taskPrompt } from "../src/crew/brief.ts"; | |
| 17 | 20 | import { serve, type Servers } from "../src/http/server.ts"; |
| 18 | 21 | import { syncsSettled } from "../src/origins/sync.ts"; |
| 19 | 22 | 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", () => { | ||
| 38 | 41 | let fake: FakeForge; |
| 39 | 42 | let originId = ""; |
| 40 | 43 | const cookies: Record<string, string> = {}; |
| 44 | let commit = ""; | |
| 45 | let crewAgent: Agent; | |
| 41 | 46 | |
| 42 | 47 | const api = (path: string, init?: RequestInit) => { |
| 43 | 48 | const headers = new Headers(init?.headers); |
| @@ -84,8 +89,8 @@ describe("a mirror of a read-only origin is private to its owner", () => { | ||
| 84 | 89 | if (!made.ok) throw new Error(made.error); |
| 85 | 90 | const bob = await createUser(app.db, { username: "bob", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true }); |
| 86 | 91 | 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." }); | |
| 88 | 92 | if (!bob.ok || !crew.ok) throw new Error("users"); |
| 93 | crewAgent = createAgentRow(app.db, { userId: crew.value.id, brief: "builds." }); | |
| 89 | 94 | cookies["owner"] = `forge_session=${createSession(app.db, made.value.id).token}`; |
| 90 | 95 | cookies["stranger"] = `forge_session=${createSession(app.db, bob.value.id).token}`; |
| 91 | 96 | 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", () => { | ||
| 104 | 109 | }), |
| 105 | 110 | ); |
| 106 | 111 | 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" }); | |
| 107 | 118 | proveAll(app); |
| 108 | 119 | }); |
| 109 | 120 | |
| @@ -146,19 +157,113 @@ describe("a mirror of a read-only origin is private to its owner", () => { | ||
| 146 | 157 | "/talk", |
| 147 | 158 | "/alice", |
| 148 | 159 | "/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", | |
| 149 | 208 | ]; |
| 150 | 209 | |
| 151 | 210 | for (const who of ["anonymous", "stranger", "agent"]) { |
| 152 | 211 | test(`${who} finds no trace of its name or commits`, async () => { |
| 212 | const leaks: string[] = []; | |
| 153 | 213 | 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); | |
| 158 | 216 | } |
| 217 | expect(leaks).toEqual([]); | |
| 159 | 218 | }); |
| 160 | 219 | } |
| 161 | 220 | |
| 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 | ||
| 162 | 267 | for (const who of ["anonymous", "stranger", "agent"]) { |
| 163 | 268 | test(`${who} is told the repository does not exist`, async () => { |
| 164 | 269 | 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", () => { | ||
| 166 | 271 | }); |
| 167 | 272 | } |
| 168 | 273 | |
| 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 | ||
| 169 | 288 | test("the owner finds it", async () => { |
| 170 | 289 | 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}`]) { |
| 171 | 290 | const res = await as("owner", path); |