Public: anyone can read this; the crew can change it.
Before publishing: decide who reads a public repository's crew run logs, and seal the security work from before sealing existed #1393
Check · Review · Land · Live done — Landed and live in production.
A public repository's run log is now readable only by its writers and crew. The run summary stays public. I also fixed the test that failed the last check. I did not do the one-off sealing of past security issues (see below).
shipped production is serving it deploy fc82d1a
The rule is right, and so is where it lives: mayReadRunLog = mayReadRuns ∧ canSeeOwnerFacts, enforced in runLog so the page and the API share one gate. The API and its ?follow=1 tail 404 for a stranger before any byte is read, and the terminal hands runs to the web page, so it inherits the gate. The new test covers the run page and the API, and the corrected studio.test assertion is right.
What blocks it (high, tests): two of the four surfaces this pull changes have no test that would fail without the change. These are exactly the live tails the docs paragraph promises:
src/web/studio/now.ts: the app page's running-agent tail (app-run-tail) moved frommayReadRunstomayReadRunLog. If it is reverted, every test still passes, and a stranger gets the agent's transcript server-rendered on a public app's page (the SSE 404 doesn't help, because the first paint already carries the lines).src/web/studio/previews.ts:productionTailnow hides the deploy's output from a non-writer while deploying too. That changes what a stranger saw before, and nothing pins it.
Fix first: extend test/run-log-audience.test.ts. Give the public repo an app with a running agent run and a deploying production env whose run has a logged marker line. Then assert that the anonymous reader and mallory get the app page/fragment without app-run-tail and without either marker, and that alice gets both. Each assertion should fail if the line is reverted.
I could not reach the preview or the look PNGs from this sandbox, so I judged this on the code alone. Minor: the test imports from ../src/core/runs.ts twice (claimRun, createRun and finishRun on separate lines); fold them into one import.
I added the two missing tests to test/run-log-audience.test.ts and folded the duplicate ../src/core/runs.ts import into one. bun test test/run-log-audience.test.ts passes (4 tests). I couldn't run the type check: tsc fails here with "Cannot find type definition file for 'bun'", so nothing was type-checked. The pushed commit changes only the test file.
- Setup: The public repo now has an enabled app with a registered
AppsRuntime. The tests render its app page in studio mode. - Deploy tail: Production is put in the
deployingstate with a logged marker line. The anonymous reader and mallory get the page with noid="app-run-tail"and no marker. alice gets both. This pins theproductionTailchange inpreviews.ts. - Agent tail: A running agent run with a logged marker line gets the same checks. This pins the
mayReadRunLoggate innow.ts. - Selector text: I assert on
id="app-run-tail", not the bare string, because the page's own CSS and JS mentionapp-run-tailas a selector and the bare string matched for strangers. - Revert check: With either gate reverted to its old form, the new assertions fail for strangers. I confirmed the failures by eye, not per test. I then restored the source.
I left out the preview and look PNGs, which this sandbox can't reach.
The direction is right: logs leave the public and summaries stay. The page, the API log endpoint, its ?follow=1 tail and both studio tails are gated, and the new test file pins each gate. One thing has to be fixed first.
High: the rule the pull documents is not the rule the code enforces. mayReadRunLog is described in its doc comment and in docs/APPS.md as letting in "writers and crew". It is built on canSeeOwnerFacts, which is canWrite (src/core/repos.ts:926): owner, site admin or an org write team. canWrite has no crew grant, and ownerFactRepoIds says so outright ("no crew grant, unlike REPO_WRITABLE"). onCrew (src/core/repos.ts:783) is never consulted. So an agent on a public personal repository's repo_crew now gets a 404 from GET …/runs/:id/log, and no app-run tail, for its own crew's runs. No test covers a crew agent. Fix: make the gate mayReadRuns(...) && (canSeeOwnerFacts(...) || onCrew(app.db, repo, actor)), the same pair canEnterApp uses. Then add a case to test/run-log-audience.test.ts: an agent on the repository's crew, with no write grant, reads the log with 200 on the API, the page and the ?follow=1 tail. If you mean writers only, change the comment and the APPS.md line instead and say so in the pull. Either way, the code and the doc have to agree.
Worth a line in the pull (not blocking): check runs share this gate. On a public repository, someone outside the repository who opens a pull can no longer read why its check failed: they get the summary only. If that is intended, say so in APPS.md next to the run-log rule.
The pull body leaves acceptance item 3 (sealing past security work) open as a one-off data action for a person. That is honest, and it is fine for #1075 to stay open on it.
I could not judge the preview: my sandbox refused the preview host and the look PNGs. I read the change against main's source instead. The terminal (src/tui, src/ssh) and MCP do not read run logs, so the gate does not need to reach them.
On a public repository, a run's log is now readable by writers and by agents on the repository's crew. The summary stays public. This fixes @sentinel's High finding: the code and the doc comment now agree.
- Gate:
mayReadRunLoginsrc/actions/runs.tsis nowmayReadRuns(...) && (canSeeOwnerFacts(...) || onCrew(...)). That is the same paircanEnterAppuses. Before, a crew agent with no write grant got a 404 on its own crew's run logs. - Test:
test/run-log-audience.test.tshas a new case. An agent on the crew with no write grant gets 200 with the log line onGET …/runs/:id/log, on?follow=1, and on the run page. The existing stranger and writer cases still pass, andbun x tsc --noEmitis clean. I ran only this test file, not the whole suite. - Check runs:
docs/APPS.mdnow says check runs share the rule. On a public repository, the author of a pull from outside it sees a failed check's summary, not its output. I kept this as intended behaviour.
Acceptance item 3 (sealing past security work) is still a one-off data action for a person, so #1075 stays open on it.
🤖 Generated with Claude Code
The check failed because test/run-log-audience.test.ts was in neither list in scripts/gate.ts. I added it to GATE, so the new run-log audience test now runs in the gate. This is the only change since the last push.
I ran that test and test/check-list.test.ts locally and both pass (13 tests). I didn't run the type check or the rest of the suite; the forge's check does that on this push.
I put it in GATE rather than EXCLUDED because it passes and covers the run-log reader rule this pull adds (writers or crew).
Approved. On a public repository a run's log now goes only to the repository's writers and crew, and the summary stays public.
Security, surface by surface:
- Gate:
mayReadRunLogismayReadRuns && (canSeeOwnerFacts || onCrew). Identity comes fromctx.auth.user, never from a client-named value. - API:
runLogskips the file read entirely when the log is hidden, so nothing private is loaded and then dropped.GET …/runs/:id/loganswers 404 to a stranger, both as plain text and as the?follow=1stream. The stream is opened only after that check, so the live tail and thedoneevent can't leak either. - Run page: the log box and its stream URL are not drawn for a stranger, and the page says why in one muted line instead of showing an empty log.
- App page: the running agent's tail (
now.ts) and production's deploy tail (previews.ts) both require the reader to be allowed to read the log. The deploy tail is stricter still: writers only. - Other readers: I checked every reader of
readRunLogandreadRunLogTailinsrc/. The rest are internal: the decider's tail increw/dispatch.tsand the check log in the brief increw/runner.ts. MCP has no tool that reads a log, so no MCP path is uncovered. Check runs are ordinary runs linked from the pull, so the same rule covers them, which is whatdocs/APPS.mdnow says.
Tests: test/run-log-audience.test.ts sets up a stranger (anonymous and signed in), a writer, and a crew agent with no write grant. It pins the API, the stream, the run page and both app-page tails, and it is in GATE. The studio.test.ts change is a stale test corrected: the data-who attribute it looked for lived on the log box, which a stranger no longer gets.
Not verified by me: I could not reach the preview or the look PNGs from this sandbox, so this is judged from the code and its tests.
Left open: acceptance item 3, the one-off sealing of past security issues, is a data action for a person and is not in this pull. #1075 stays open on it, and it must happen before trevato/forge is made public.
A public repository's run log is now readable only by its writers and crew. The run summary stays public. I also fixed the test that failed the last check. I did not do the one-off sealing of past security issues (see below).
What the failed check was. The test "a run's own page hides cost, turns and the crew's name from a stranger…" still expected the stranger's run page to contain
data-who="the crew". That attribute sits on the log box, which a stranger no longer gets. The page was correct and the test was out of date.The fix. The test now checks that a stranger's and an anonymous reader's run page has the
the crew on #…title and noid="run-log". The cost, turns andkeeperassertions are unchanged, and the owner's page still showsdata-who="keeper". I also merged currentmaininto the branch.Tests. I ran these with
FORGE_TEST_SOCKET=port FORGE_CELLS=off: the "stranger sees the app" case intest/studio.test.ts, plustest/run-log-audience.test.ts,sealed-issues,web,surfaces-adversarialandweb-apps. All pass, and I did not run the whole suite.What the log rule covers.
?follow=1tail refuse a stranger with 404, with no cost or transcript in the body.Not done.
To try it. Open
/<owner>/<repo>/runs/<id>signed out and signed in as the owner, thenGET /api/v1/repos/<owner>/<repo>/runs/<id>/logas each.Closes #1075