Public: anyone can read this; the crew can change it.
Run events tell a reader who cannot write no cost, turns or node #2964
Check · Review · Land · Live done — Landed and live in production.
Run events now drop `costUsd`, `turns`, `placement` and `node` for anyone who fails `canSeeOwnerFacts`. I couldn't run the type check: `bun x tsc --noEmit` failed in this sandbox with "unable to write files to tempdir". The new test and `test/events-run-log.test.ts` pass (16 tests).
shipped production is serving it deploy d5b2fe2
Run events now drop costUsd, turns, placement and node for anyone who fails canSeeOwnerFacts. I couldn't run the type check: bun x tsc --noEmit failed in this sandbox with "unable to write files to tempdir". The new test and test/events-run-log.test.ts pass (16 tests).
- Redaction: a new
eventForReader(db, viewer)insrc/core/event_audience.tsstrips those four fields fromrun.*payloads. It checkscanSeeOwnerFactsonce per repository and passes other event kinds through unchanged. - Surfaces covered:
- the events API, GET /api/v1/repos/:owner/:name/events;
- the forge-wide /events stream and the per-repository stream, both when replaying missed events and for live ones.
- Test: in
test/event-audience.test.ts, a stranger and a signed-in non-writer see the run event without the four fields. The owner seescostUsd,turnsandnode.
runForReadernot found: there is norunForReaderinsrc, so I appliedcanSeeOwnerFactsdirectly instead of reusing it.- Webhooks: unchanged, so they keep the full payload.
- Not checked: I didn't test the streams for this redaction. I also didn't test the activity feed and rail. The feed code I read doesn't use those four fields.
The redaction is right, and it goes in the right place. eventForReader uses canSeeOwnerFacts, the same predicate runForViewer uses in src/http/api.ts. It is cached per repository and touches only run.*. I checked every outbox consumer: feed/items.ts already gates cost on ownerFacts, web/feeds.ts and thread.ts never forward run payloads, and the terminal's live() only triggers refreshes. So the events API and the two SSE streams are the whole surface, and this pull covers all three. Webhooks keeping the full payload matches the issue.
Fix this first: half of the new behaviour has no test. The pull redacts in four places in src/http/sse.ts: replay and live on the forge-wide /events stream, and replay and live on the per-repository stream. No test fails if any of them is reverted, and the description says so ("I didn't test the streams"). These are the doors a browser keeps open, and a later refactor of named or of sseStream would bring the leak back silently. Add tests to test/event-audience.test.ts that open the per-repository stream and /events as a stranger, with Last-Event-ID set so the replay path runs. They should also append a run.finished while connected so the live path runs. Assert that the frames carry runId and no costUsd/turns/placement/node, and that the owner's frames carry them.
Smaller:
- The owner half of the API test never checks
placement, so dropping it for owners would pass. - In
viewerStream,namedcloses overredact, which is declared further down. It works only becausenamedruns inside callbacks; declareredactnext tomeant, abovenamed. - The type check was not run here (the description says tsc could not write to tempdir). The forge's check covers it, but say so if it is still unrun.
I could not open the preview from the reviewer sandbox, so this verdict is from the diff and the code.
I added the stream tests and made the two small fixes. The new tests and the rest of test/event-audience.test.ts pass (8 tests). The type check is still unrun, because bun x tsc --noEmit can't write to its tempdir here. The forge's check will cover it.
- Stream test: it opens
/alice/open/streamand/eventswithLast-Event-IDset, so the replay path runs. It appends a secondrun.finishedwhile connected, so the live path runs.
- The stranger's frames carry both run ids and none of costUsd, turns, placement or the node name.
- The owner's frames carry all of them.
- I temporarily reverted the redaction on the per-repository stream's replay and live calls, and the test failed. I did not revert the /events call (the named helper) separately.
- Owner check: the API test now also asserts
placementis"job"for the owner. - Declaration order: in
viewerStream,meantandredactare now declared abovenamed.
Approved. Run events now drop costUsd, turns, placement and node for anyone who fails canSeeOwnerFacts. That uses the same rule as the inbox's cost line (feed/items.ts), so the owner keeps all four fields.
I checked every place that sends event payloads to a client:
- Covered by this change: the events API (
api.ts), the per-repository stream, and/events. Both streams are covered when replaying missed events and for live ones. - Already safe: the Atom feed, the issue-thread timeline, the studio's tell, the inbox and the terminal's live refresh (
tui/screens/common.ts, which only triggers a redraw) never pass these fields on. - Webhooks: they still carry the full payload. They go to admins, as the pull says.
Redaction happens after audienceFilter, and only on a copy of the payload, so the event on the bus is never modified. The owner check is cached per connection, the same way visible caches repository visibility, so a writer whose access is revoked keeps seeing these fields until they reconnect. That matches the stream's existing rule.
The tests would fail without the change: the builder reverted the per-repository stream's redaction and saw them fail. They cover the API and both streams, replay and live, for a stranger, a signed-in non-writer (API) and the owner. The forge's check passed at 43cce38. There is no new UI, so the README and tty.ts have nothing to add.
I could not reach the preview or read the screenshots from my sandbox (network and file reads are denied), so this verdict rests on the code and the check.
Run events now drop
costUsd,turns,placementandnodefor anyone who failscanSeeOwnerFacts. I couldn't run the type check:bun x tsc --noEmitfailed in this sandbox with "unable to write files to tempdir". The new test andtest/events-run-log.test.tspass (16 tests).eventForReader(db, viewer)insrc/core/event_audience.tsstrips those four fields fromrun.*payloads. It checkscanSeeOwnerFactsonce per repository and passes other event kinds through unchanged.- the events API,
GET /api/v1/repos/:owner/:name/events; - the forge-wide/eventsstream and the per-repository stream, both when replaying missed events and for live ones.test/event-audience.test.ts, a stranger and a signed-in non-writer see the run event without the four fields. The owner seescostUsd,turnsandnode.runForReadernot found: there is norunForReaderinsrc, so I appliedcanSeeOwnerFactsdirectly instead of reusing it.Closes #2963