Public: anyone can read this; the crew can change it.
The page manifest offers "make it yours" on a repository with an app, so an agent finds the copy without being taught #7863
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7854-the-page-manifest-offers-make-it-yours-o → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +46
−0
src/actions/here.ts +13
| @@ -360,6 +360,19 @@ function repoActions( | ||
| 360 | 360 | const record = getAppByRepo(db, repo.id); |
| 361 | 361 | if (record !== undefined && canEnterApp(db, repo, record, viewer)) |
| 362 | 362 | out.push({ do: "open the app", http: `GET ${r}/app/production/open` }); |
| 363 | // The copy is for whoever cannot write the app (the web's button, `!canWrite`); | |
| 364 | // its dry run is `ro`, so a read-only token sees it too. | |
| 365 | if ( | |
| 366 | record !== undefined && | |
| 367 | canEnterApp(db, repo, record, viewer) && | |
| 368 | !canWrite(db, repo, viewer) | |
| 369 | ) | |
| 370 | out.push({ | |
| 371 | do: "make it yours", | |
| 372 | http: `POST ${r}/make-it-yours`, | |
| 373 | mcp: "make_it_yours", | |
| 374 | ...(viewer === undefined ? { account: true } : unproven), | |
| 375 | }); | |
| 363 | 376 | if (!acts) return out; |
| 364 | 377 | const tellSsh = sshTellCommand(app); |
| 365 | 378 | if (mayDirectCrew(db, repo, viewer)) |
test/here-manifest.test.ts +33
| @@ -77,6 +77,18 @@ describe("the manifest: what you can do here, as who you are", () => { | ||
| 77 | 77 | visibility: "private", |
| 78 | 78 | }); |
| 79 | 79 | if (!vault.ok) throw new Error(String(vault.error)); |
| 80 | const pub = await newApp(app, alice, { | |
| 81 | name: "pub", | |
| 82 | owner: "acme", | |
| 83 | visibility: "public", | |
| 84 | }); | |
| 85 | if (!pub.ok) throw new Error(String(pub.error)); | |
| 86 | const plain = await createRepo(app, alice, { | |
| 87 | name: "plain", | |
| 88 | owner: "acme", | |
| 89 | visibility: "public", | |
| 90 | }); | |
| 91 | if (!plain.ok) throw new Error(String(plain.error)); | |
| 80 | 92 | const bot = await createAgent(app, alice, { username: "bot" }); |
| 81 | 93 | if (!bot.ok) throw new Error(String(bot.error)); |
| 82 | 94 | expect(addToCrew(app, alice, game.value.repo, "bot").ok).toBe(true); |
| @@ -323,6 +335,27 @@ describe("the manifest: what you can do here, as who you are", () => { | ||
| 323 | 335 | expect(carol.actions.find((a: any) => a.do === "offer a change").verify).toBeUndefined(); |
| 324 | 336 | }); |
| 325 | 337 | |
| 338 | test("a repository with an app offers make it yours to whoever cannot write it", async () => { | |
| 339 | const find = (m: any) => m.actions.find((a: any) => a.do === "make it yours"); | |
| 340 | const carol = find((await manifest("/acme/pub", "carol")).body); | |
| 341 | expect(carol).toEqual({ | |
| 342 | do: "make it yours", | |
| 343 | http: "POST /api/v1/repos/acme/pub/make-it-yours", | |
| 344 | mcp: "make_it_yours", | |
| 345 | }); | |
| 346 | expect(find((await manifest("/acme/pub", "una")).body).verify).toBe(true); | |
| 347 | const out = find((await manifest("/acme/pub")).body); | |
| 348 | expect(out.account).toBe(true); | |
| 349 | expect(out.verify).toBeUndefined(); | |
| 350 | }); | |
| 351 | ||
| 352 | test("a writer, and a repository without an app, are not offered make it yours", async () => { | |
| 353 | expect(does((await manifest("/acme/pub", "wes")).body)).not.toContain("make it yours"); | |
| 354 | expect(does((await manifest("/acme/pub", "alice")).body)).not.toContain("make it yours"); | |
| 355 | expect(does((await manifest("/acme/plain", "carol")).body)).not.toContain("make it yours"); | |
| 356 | expect(does((await manifest("/acme/plain")).body)).not.toContain("make it yours"); | |
| 357 | }); | |
| 358 | ||
| 326 | 359 | test("an org's own pages are the org's, not a missing repository", async () => { |
| 327 | 360 | const res = await here("/acme/-/teams/Writers", "alice"); |
| 328 | 361 | expect(res.status).toBe(200); |
new above
offline
Non-blocking:
record !== undefined && canEnterApp(...)repeats the check on the line above. Computeconst enter = ...once and use it for both actions.