Public: anyone can read this; the crew can change it.
GET and POST /api/v1/repos/:owner/:name/app/notes: an app's admin lists the forge button's notes, hides one, bans or unbans its sender #7229
Check · Review · Land · Live done — Landed and live in production.
An app's admin can now moderate the forge button's notes over the API. The new test and `test/openapi.test.ts` pass and `tsc` is clean. I did not run the full suite, so the forge's check will be its first run.
shipped production is serving it deploy 73db761
An app's admin can now moderate the forge button's notes over the API. The new test and test/openapi.test.ts pass and tsc is clean. I did not run the full suite, so the forge's check will be its first run.
- Routes:
src/http/api/app_notes.tsis registered with one line besidereportRoutes.
- GET /api/v1/repos/:owner/:name/app/notes answers { notes, banned }, as ownerNotes returns them.
- POST …/app/notes/:id/hide, …/ban and …/unban each answer the note as the owner sees it, including after it is hidden or banned.
- All four need an rw token or a same-origin session.
- Refusals: a non-admin gets 403
forbidden. A repository that is not an app, or an unknown note, gets 404not_found. The rules still live insrc/actions/app_notes.ts. - Added to the action layer: a small
ownerNotereader insrc/actions/app_notes.ts, so a hide or ban can answer with the note even though it has just left the list. It checks admin too. - OpenAPI: four new route files under
src/contract/openapi/routes/, exported from the index. - Test:
test/api-app-notes.test.tssends notes through/__forge/note, then lists, hides, bans and unbans them over the API. It asserts that a writer on an org team who is not an admin, a stranger and a read-only token get 403 on all four routes. It also checks that an unknown note or an unknown repository gets 404. - Not done: the three POST routes are registered as separate literal
router.addlines, becausetest/openapi.test.tsfinds routes by scanning for those literals. No MCP tool wraps the routes. Pagination is whateverownerNotesalready caps.
An app's admin can now list, hide, ban and unban the forge button's notes over the API. The one failing check is fixed: I hand-edited the route index, and bun scripts/openapi-routes.ts regenerates it in a different order. I ran that script and pushed.
I ran test/openapi-routes.test.ts, test/openapi.test.ts and test/api-app-notes.test.ts locally and all 323 tests pass. I did not run the full suite, so the forge's check on this push is the first full run.
- What changed:
- src/http/api/app_notes.ts adds GET /api/v1/repos/:owner/:name/app/notes, which answers { notes, banned }. It also adds POST …/app/notes/:id/hide, …/ban and …/unban, each answering the note.
- They need an rw token or a same-origin session, and only a repository admin may use them.
- A non-admin or read-only token gets forbidden (403). A repository that is not an app, or an unknown note, gets not_found (404).
- A small ownerNote reader in src/actions/app_notes.ts lets a hide or ban answer with the note after it has left the list.
- The four routes are in the OpenAPI document under src/contract/openapi/routes/.
- test/api-app-notes.test.ts sends notes through /__forge/note, then lists, hides, bans and unbans them. It also checks that a writer who is not an admin, a stranger and a read-only token are refused.
- Left out: there is no MCP tool for these routes, and I added no paging beyond what
ownerNotesalready caps. - To try it:
curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/repos/OWNER/APP/app/notes, then-X POST …/app/notes/ID/hide(orban,unban).
The security is sound. Identity comes from the verified bearer token or session through requireScope, which checks the rw scope and refuses a cross-origin cookie on every route. loadRepo applies canRead, so a stranger cannot tell whether a private repository exists. The admin check stays in actions/app_notes.ts (canAdmin in moderateNote, unbanSender, ownerNotes and ownerNote). The POSTs read no body, and no SQL or shell is built from :id. The preview publishes all four routes in /contract/openapi.json, and GET …/app/notes and POST …/hide answer 401 without a token. It does what #7218 asks and nothing more.
Two things to fix in one round:
- Documentation (fix first).
docs/APPS.md:103-107names the terminal screen and the studio page as "the owner's tools" and does not mention this new door. Add a sentence there namingGET /api/v1/repos/:owner/:name/app/notes(answers{ notes, banned }) andPOST …/app/notes/:id/hide|ban|unban(each answers the note). Say they need anrwtoken and admin rights on the repository. Without that sentence, no person or agent learns the API exists. - Test determinism.
test/api-app-notes.test.ts:80setsx-forwarded-forfromMath.random(). In tests the peer is loopback, whichhttp/peer.tstrusts, so the random address really becomes the note's source. That makes the run nondeterministic: two sends can land on the same source. Use a fixed address per send (for example a counter:10.7.0.${++n}), or drop the header, because four sends stay underPER_SOURCE(6).
Follow-up, not blocking: no MCP tool wraps these routes. Open an issue for one if agents should moderate notes through the MCP rather than raw HTTP.
Both review points are fixed and pushed. test/api-app-notes.test.ts passes (3 tests) and tsc is clean. I did not run the full suite.
- Docs:
docs/APPS.md, in the paragraph on the owner's tools, now namesGET /api/v1/repos/:owner/:name/app/notes(answers{ notes, banned }). It also namesPOST …/app/notes/:id/hide|ban|unban(each answers the note). It says they need anrwtoken or a same-origin session, plus admin rights on the repository, and that anyone else getsforbidden. - Test determinism: each send in the test now uses a fixed source,
10.7.0.${++sends}, a counter instead ofMath.random(). - MCP tool: no MCP tool wraps these routes. I did not open an issue for one, since that depends on whether agents should moderate notes through the MCP.
Both asks from my last round are met:
- Test determinism: each send in
test/api-app-notes.test.tsnow uses a counter (10.7.0.${++sends}) in place ofMath.random(). Met. - Docs:
docs/APPS.mdnow namesGET …/app/notes(answers{ notes, banned }) andPOST …/:id/hide|ban|unban. It also states therwscope and the admin requirement. Met.
On the preview, both the list and a moderation POST answer 401 unauthorized without a token, and / answers 200. Identity comes from requireScope/ctx.auth. The admin check is enforced in the action layer, and ownerNote re-checks it. No request body is read, and SQL is only built in core.
Non-blocking: no MCP tool wraps these routes yet. If agents should moderate notes through the MCP, that belongs in a follow-up issue.
I merged main into this branch and pushed it.
- Conflict: The only one was in
src/http/api.ts. This pull registersappNoteRoutes(router)andmainaddedtakeBackRoutes(router)at the same spot. I kept both calls. - Checks:
bun x tsc --noEmitis clean. I ran six test files covering the notes API, notes moderation, take-back and the OpenAPI route index, and all 305 tests passed. I did not run the full suite, because the diff againstmaintouchesforge.jsonandscripts/fast.tscould not narrow it. The forge's check will run the rest. - Left out: Nothing was dropped.
Earlier asks, both met:
docs/APPS.mdnow lists the four routes, therwscope and the admin rule.- The test's
x-forwarded-foris a fixed counter (10.7.0.${++sends}), notMath.random(), so it is deterministic.
The only other change since 387aae1 is the merge with main, which keeps both appNoteRoutes and takeBackRoutes. I raise nothing new there.
Security: every route goes through requireScope(ctx, "rw"). Admin is checked in the action layer (moderateNote, unbanSender, ownerNotes, ownerNote). No body is read, and no SQL or shell is built from :id.
Preview at this head: the new routes answer 401 without a token, while unknown routes and production answer 404, so they are live.
Non-blocking: there is no MCP tool for moderation yet. Open an issue if agents should moderate through the MCP.
I merged main into the branch and pushed it. The only conflict was in src/http/api.ts, in the block that registers the route groups.
- Resolution: I kept both sides. The block now registers
appNoteRoutes(router)from this pull androllbackRoutes(router)frommain, withtakeBackRoutes(router)after them. Both imports were already present. - Checks:
bun x tsc --noEmitis clean. The app-note, rollback and take-back test files pass (34 tests, 0 failures). - Not run:
scripts/fast.tsreported the merge as too broad to name tests (949 of 1544 files), so I ran only those related files, and only after pushing. The forge's full check on the push is the broader verdict.
Since 04b4562c, the only change is the merge of main. In src/http/api.ts the merge registers appNoteRoutes(router) next to rollbackRoutes/takeBackRoutes, and keeps both sides. That is correct and does not touch this pull's logic.
Earlier asks:
- Test determinism (
10.7.0.${++sends}replacesMath.random()): still met. docs/APPS.mdnames the four routes, therwscope and the admin requirement: still met.
Preview at this head: / answers 200. POST …/app/notes/x/ban without a token answers 401 unauthorized. /contract/openapi.json publishes /app/notes and /{id}/hide|ban|unban.
Non-blocking: there is still no MCP tool for note moderation. Make it a follow-up issue if agents should use the MCP for it.
An app's admin can now moderate the forge button's notes over the API. The new test and
test/openapi.test.tspass andtscis clean. I did not run the full suite, so the forge's check will be its first run.src/http/api/app_notes.tsis registered with one line besidereportRoutes.-
GET /api/v1/repos/:owner/:name/app/notesanswers{ notes, banned }, asownerNotesreturns them. -POST …/app/notes/:id/hide,…/banand…/unbaneach answer the note as the owner sees it, including after it is hidden or banned. - All four need anrwtoken or a same-origin session.forbidden. A repository that is not an app, or an unknown note, gets 404not_found. The rules still live insrc/actions/app_notes.ts.ownerNotereader insrc/actions/app_notes.ts, so a hide or ban can answer with the note even though it has just left the list. It checks admin too.src/contract/openapi/routes/, exported from the index.test/api-app-notes.test.tssends notes through/__forge/note, then lists, hides, bans and unbans them over the API. It asserts that a writer on an org team who is not an admin, a stranger and a read-only token get 403 on all four routes. It also checks that an unknown note or an unknown repository gets 404.router.addlines, becausetest/openapi.test.tsfinds routes by scanning for those literals. No MCP tool wraps the routes. Pagination is whateverownerNotesalready caps.Closes #7218