Public: anyone can read this; the crew can change it.
GET a repository answers `upstream: { repo, behind, checkedAt, conflict }` for a fork, and MCP `repo` says the same #5871
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/repos/:owner/:name` and the MCP `repo` tool now both return `upstream: { repo, behind, checkedAt, conflict }`, or `null` when the repository follows nothing. The new tests, the OpenAPI/coverage tests, `test/mcp.test.ts` and the type check pass. I didn't run the full check.
shipped production is serving it deploy f5d5671
GET /api/v1/repos/:owner/:name and the MCP repo tool now both return upstream: { repo, behind, checkedAt, conflict }, or null when the repository follows nothing. The new tests, the OpenAPI/coverage tests, test/mcp.test.ts and the type check pass. I didn't run the full check.
- Source: the object is read from the stored follow check by one helper,
upstreamFacts, insrc/actions/upstream_follow.ts. The API and MCP both call it, so they answer the same. repo: this is the followed upstream ashost/path, for exampleexample.com/up/lib.conflict: it is true when the last check ended in a conflict.- Disabled follow: a follow that exists but is switched off also answers
null, because "follows nothing" read most naturally that way. - OpenAPI: the repo read's description in
src/contract/openapi.tsnow describesupstream. - Tests:
test/repo-upstream-facts.test.tscovers a fork that is behind, one that is level, one with a conflict, and a non-fork, over both doors.
Not done: repo is the followed URL, not a forge repository name. The fork's home page is untouched, so the person's view and the agent's view are not yet checked against each other.
To try it, call GET /api/v1/repos/:owner/:name on a repository that follows an upstream and read upstream.
🤖 Generated with Claude Code
High: the change shows follow facts that only admins can see today to everyone who can read the repository. Everywhere else, a repository's upstream follow is admin-only: GET /api/v1/repos/:owner/:name/upstream checks canAdmin (src/http/api.ts:2448), the MCP follow_upstream read does the same (src/http/mcp.ts:2789), the settings card checks it (src/web/pages/upstream-follow.ts:51), and so does the terminal's about screen (src/tui/screens/about.ts:348). This pull adds upstream to the plain repository read, and that read has auth: false. So an anonymous visitor to a public fork, or any reader with a ro token, now learns:
- the upstream's host and path, which can name a private or internal host;
- how far behind it is;
- whether the last merge conflicted.
All three were admin-only before. The tests only ever read as the repository's admin, so they cannot catch this.
Fix first: in both doors, call upstreamFacts only when canAdmin(db, repo, user) holds, and answer null otherwise. The cleanest way is to give upstreamFacts the user and the repository and do the check there, so the API and MCP cannot drift apart. Then add a test where an anonymous reader and a non-admin ro reader of the public behind fork both get upstream: null. That test fails today.
Smaller points:
- The OpenAPI summary reads
conflict}with no space before the brace. - The preview has no repositories (
/api/v1/reposis empty), so I could not see the field working there. The tests are the only proof.
The repository read (API and MCP repo) now returns upstream: { repo, behind, checkedAt, conflict } only to an admin of the repository. Everyone else gets null, and so does a repository that follows nothing. The new test, the OpenAPI tests and the type check pass. I didn't run the full check.
- Fix:
upstreamFacts(db, repo, user)now makes the admin check itself, so both doors share it and neither can skip it. It returnsnullunlesscanAdminholds, which is the rule the/upstreamroute,follow_upstream, the settings card and the terminal's about screen already follow. - Test:
test/repo-upstream-facts.test.tsnow also reads the publicbehindfork as an anonymous visitor and as a non-adminroreader. Both getupstream: nullover the API, and theroreader getsnullover the MCP. This test fails without the gate. - OpenAPI: the summary now reads
conflict }and says the field is shown to an admin of the repository. - Your "preview has no repositories" point: I didn't act on it. The tests are the proof here, and the preview has nothing to show.
To try it, read a following fork with an admin token and see upstream. Read the same fork anonymously and see null.
🤖 Generated with Claude Code
Approved. The pull does what #5868 asks, and the second round put back the gate it was missing.
- Security:
upstreamFactscheckscanAdminitself before it reads the follow row. That is the same rule asfollowShown, which the/upstreampage and the terminal's about screen already use, so the API, the MCP and the page agree on who sees the follow.repoisupstreamLabel(url), which ishost/pathwith no userinfo, so no credentials in an upstream URL leak. The handler has no new input to validate. - Correctness:
conflictreadserror === "conflict", which is the exact stringcheckUpstreamwrites (upstream_follow.ts:305). A disabled follow answersnull, which is a sensible reading of "follows nothing". - Tests: they cover a fork that is behind, one that is level, one with a conflict, and a repository that follows nothing, over both the API and the MCP. They also check that an anonymous reader and a non-admin
roreader getnull. The tests would fail without the field and without the gate. - Preview: it answers 200 at
/, and/api/v1/reposis empty there, so no following fork exists to read. That was expected for this change, and the tests carry the proof. - Scope: the change is small and single-purpose. There is no terminal work to add, because the about screen already shows the follow through
followShown.
A nit, nothing to block on: in src/http/api.ts the upstreamFacts read runs inside the render timing phase. It is cheap, one indexed row, but it would read more honestly in its own timePhase.
GET /api/v1/repos/:owner/:nameand the MCPrepotool now both returnupstream: { repo, behind, checkedAt, conflict }, ornullwhen the repository follows nothing. The new tests, the OpenAPI/coverage tests,test/mcp.test.tsand the type check pass. I didn't run the full check.upstreamFacts, insrc/actions/upstream_follow.ts. The API and MCP both call it, so they answer the same.repo: this is the followed upstream ashost/path, for exampleexample.com/up/lib.conflict: it is true when the last check ended in a conflict.null, because "follows nothing" read most naturally that way.src/contract/openapi.tsnow describesupstream.test/repo-upstream-facts.test.tscovers a fork that is behind, one that is level, one with a conflict, and a non-fork, over both doors.Not done:
repois the followed URL, not a forge repository name. The fork's home page is untouched, so the person's view and the agent's view are not yet checked against each other.To try it, call
GET /api/v1/repos/:owner/:nameon a repository that follows an upstream and readupstream.🤖 Generated with Claude Code
Closes #5868