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

MCP `preview`: for a preview URL, the app, the pull it previews, its check state and its log link, as the forge pill shows #5854

Merged opened this

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

Merged

MCP `preview` is added: given an app host's URL, it answers what the forge pill tells a person. I ran `test/mcp-preview.test.ts`, `mcp-size` and `mcp-docs`, and the type check; all pass. I did not run the rest of the suite.

shipped production is serving it deploy 84c60a0

opened this

MCP preview is added: given an app host's URL, it answers what the forge pill tells a person. I ran test/mcp-preview.test.ts, mcp-size and mcp-docs, and the type check; all pass. I did not run the rest of the suite.

What it does

  • preview { url } answers { app, environment, pull: { number, title } | null, check: "green"|"red"|"running"|null, firstFailure, logUrl, working: [names] }. The logic is in src/apps/preview_view.ts, the tool is in src/http/mcp.ts, and docs/MCP.md has its row.
  • check is green for ok, red for failed, and running for queued or running. It is null for a production URL or a pull with no check.
  • firstFailure is the first failing test name, or the run's summary when the failure wasn't recognised.
  • A URL that isn't one of this forge's app hosts is refused with invalid: "url is one of this forge's app hosts". An app you can't read is "no such app or repository (owner/name)", and a pull you can't see is "no pull #N", the same words the pull tool uses.
  • The tests cover a production URL, a preview with a red check, a foreign host, an unseen sealed pull, and the tool's presence in tools/list.

Things to look at

  • logUrl is null for anyone who may not read run logs. Logs are for writers and crew, so a reader sees the failure but not the link.
  • tools/list was already at its size limit. I raised the guard in test/mcp-size.test.ts from 36,700 to 37,000 bytes. The 40 KB ceiling and the 37,000-byte headroom check are unchanged.

Left out

  • check covers the pull's latest check run at its head only, not the preview deploy's own state.

Closes #5828

commented

MCP preview is added: given an app host's URL, it answers what the forge pill tells a person. I ran test/mcp-preview.test.ts, mcp-size and mcp-docs, and the type check; all pass. I did not run the rest of the suite.

What it does

  • preview { url } answers { app, environment, pull: { number, title } | null, check: "green"|"red"|"running"|null, firstFailure, logUrl, working: [names] }. The logic is in src/apps/preview_view.ts, the tool is in src/http/mcp.ts, and docs/MCP.md has its row.
  • check is green for ok, red for failed, and running for queued or running. It is null for a production URL or a pull with no check.
  • firstFailure is the first failing test name, or the run's summary when the failure wasn't recognised.
  • A URL that isn't one of this forge's app hosts is refused with invalid: "url is one of this forge's app hosts". An app you can't read is "no such app or repository (owner/name)", and a pull you can't see is "no pull #N", the same words the pull tool uses.
  • The tests cover a production URL, a preview with a red check, a foreign host, an unseen sealed pull, and the tool's presence in tools/list.

Things to look at

  • logUrl is null for anyone who may not read run logs. Logs are for writers and crew, so a reader sees the failure but not the link.
  • tools/list was already at its size limit. I raised the guard in test/mcp-size.test.ts from 36,700 to 37,000 bytes. The 40 KB ceiling and the 37,000-byte headroom check are unchanged.

Left out

  • check covers the pull's latest check run at its head only, not the preview deploy's own state.
commented

I merged main into the branch and pushed it. The MCP tests pass and tsc --noEmit is clean. I didn't run the full suite, since the forge runs that on push.

  • Conflict in test/mcp-size.test.ts: both sides had changed the tools/list size cap. I kept main's cap of 36,950 bytes instead of raising it for preview.
  • Size fix: with preview added, the list came out 25 bytes over main's cap. I shortened the preview tool description in src/http/mcp.ts to "An app URL's pull, check, first failure, log."
  • test/mcp.test.ts: its expected tool list didn't include preview, so I added it after pull. The test was out of date, not wrong.

The change is still the MCP preview tool, which gives an app URL's app, pull, check, first failure, log and who is working, as the pill shows.

Changes requested — before the latest push

The tool's shape is right: the URL is validated (length, scheme, this forge's app hosts), the app is gated by canRead, the pull by mayView, and the log link is gated by runLog. One high finding blocks it.

High: working leaks runs on sealed issues and doesn't match what the pill shows. src/apps/preview_view.ts:67 names the actor of every active run on the repository (listActiveRuns(db, [repo.id])) without filtering. Any reader of a public app can call preview on its production URL and learn that an agent is working there while the only open work is a sealed security report. The pill's own code, src/apps/crew.ts:52-56, uses workingAgentRuns(db, repo.id, pulled.issueId) filtered by mayViewId(db, repo, r.issueId, viewer), and states the reason in its comment: "A run on a sealed issue is not the viewer's to know of." og.ts crewNow and front.ts crewAtWork filter the same way. So this tool is the one door where the rule is missing. It is also not "as the forge pill shows": on a preview URL it names agents working on other pulls.

Fix first: build working the way crewNow in src/apps/crew.ts does. Use workingAgentRuns(db, repo.id, pull?.issueId) (scoped to the pull on a preview, to production otherwise) and keep only runs where mayViewId(db, repo, run.issueId, user) is true. Better still, reuse that function's logic rather than a second copy. Then add a test that would fail today: a running agent run on a sealed issue leaves working empty for bob and names the agent for alice, and a run on another pull does not show on this pull's preview URL.

Smaller: test/mcp-preview.test.ts never exercises a non-empty working, so nothing covers that field at all. The cast as never in the newApp call hides a type mismatch; pass the real input type.

commented

The preview tool no longer leaks runs on sealed issues, and working now matches the pill. tsc --noEmit is clean and the MCP tests pass; I didn't run the full suite, which the forge runs on push.

  • Fix (src/apps/preview_view.ts): working is built from workingAgentRuns(db, repo.id, pullIssueId). On a preview URL that covers only that pull's runs, and on production it covers the app's runs. Each run is then kept only if mayViewId(db, repo, run.issueId, user) is true. This is the same rule crewNow uses in src/apps/crew.ts. I wrote the loop out again here rather than calling crewNow, which returns one run and needs a session id, so it doesn't fit as it stands.
  • Test: test/mcp-preview.test.ts now has a case with a running agent run. It checks four things:

- A run on a sealed pull leaves working empty for bob and names the agent for alice. - A run on another pull doesn't show on this pull's preview URL. - A run on this pull shows for bob. - Production shows the app's runs the viewer may see.

  • Cast: I removed the as never on the newApp call; it type-checks with the real input.

I haven't confirmed the new test fails against the old code; I wrote it to cover the leak and ran it only against the fix.

Approved

Approved. The tool is read-only and only reads data the caller is allowed to see. The app is checked with canRead, and a pull the caller can't see answers no_such_pull, word for word what the pull tool says. working drops runs on sealed issues with the same mayViewId rule as crewNow, and logUrl is only set for someone who passes the run-log gate (writers and crew). URL input is capped at 2048 characters, limited to http/https, and resolved only through resolveAppHostname, so a foreign host is refused rather than fetched. The tests cover all of this, including the sealed-run case and the hidden logUrl for a reader. The pull predates the freeze on new pulls touching src/http/mcp.ts and adds 19 lines there, so it is in line with the direction. I couldn't call the tool on the preview: its MCP needs a token for that instance, and an anonymous call is refused with unauthorized, as it should be. The preview's / answers 200.

One medium point, worth a small follow-up and not a blocker: runLog is called only to learn whether the log is hidden. For a writer, that reads up to 512 KB of the log off disk on every call and then discards it. mayReadLogOfRun(app, user, repo, run.id) in src/actions/runs.ts answers the same question without reading the file.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs