feat: dismiss failed runs from inbox#765
feat: dismiss failed runs from inbox#765russellju16-afk wants to merge 1 commit intopaperclipai:masterfrom
Conversation
Adds the ability to dismiss failed heartbeat run alerts from the Inbox, removing them from the attention queue and the sidebar badge count. - Add `dismissed_at` column to `heartbeat_runs` (migration 0026) - Add `dismissRun` service method that sets `dismissedAt` - Add `POST /heartbeat-runs/:runId/dismiss` board endpoint - Filter dismissed runs out of sidebar badge failed-run count - Add `heartbeatsApi.dismiss()` client call - Inbox: dismiss button on FailedRunCard, optimistic local dismiss, filter out runs with `dismissedAt` set - Inbox: new/all tabs, category filters, touched-issues section, aggregate agent-error alert - Process adapter: inject `PAPERCLIP_API_KEY` from auth token - CLI: add `disableSignUp: false` to defaultConfig
Greptile SummaryThis PR implements a "dismiss failed run" feature end-to-end: a new
Confidence Score: 2/5
Important Files Changed
|
| router.post("/heartbeat-runs/:runId/dismiss", async (req, res) => { | ||
| assertBoard(req); | ||
| const runId = req.params.runId as string; | ||
| const run = await heartbeat.dismissRun(runId); | ||
| res.json(run); | ||
| }); |
There was a problem hiding this comment.
Missing company-level access check
The dismiss endpoint only calls assertBoard(req), which verifies the caller is an authenticated board user, but it never checks whether the caller actually belongs to the same company as the run being dismissed. In a multi-tenant deployment this allows any authenticated board user to dismiss heartbeat runs that belong to another organisation.
Compare to the events endpoint just below (line 1588), which correctly calls assertCompanyAccess(req, run.companyId) after fetching the run:
router.post("/heartbeat-runs/:runId/dismiss", async (req, res) => {
assertBoard(req);
const runId = req.params.runId as string;
const run = await heartbeat.dismissRun(runId);
if (!run) {
res.status(404).json({ error: "Heartbeat run not found" });
return;
}
assertCompanyAccess(req, run.companyId);
res.json(run);
});Note: the same issue exists on the pre-existing cancel route, but the new dismiss endpoint should not repeat the pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/routes/agents.ts
Line: 1574-1579
Comment:
**Missing company-level access check**
The dismiss endpoint only calls `assertBoard(req)`, which verifies the caller is an authenticated board user, but it never checks whether the caller actually belongs to the same company as the run being dismissed. In a multi-tenant deployment this allows any authenticated board user to dismiss heartbeat runs that belong to another organisation.
Compare to the events endpoint just below (line 1588), which correctly calls `assertCompanyAccess(req, run.companyId)` after fetching the run:
```ts
router.post("/heartbeat-runs/:runId/dismiss", async (req, res) => {
assertBoard(req);
const runId = req.params.runId as string;
const run = await heartbeat.dismissRun(runId);
if (!run) {
res.status(404).json({ error: "Heartbeat run not found" });
return;
}
assertCompanyAccess(req, run.companyId);
res.json(run);
});
```
Note: the same issue exists on the pre-existing cancel route, but the new dismiss endpoint should not repeat the pattern.
How can I resolve this? If you propose a fix, please make it concise.
Summary
dismissed_atcolumn toheartbeat_runs(migration 0026) and filters dismissed runs from sidebar badge countsPOST /heartbeat-runs/:runId/dismissboard endpoint anddismissRunservice methodChanges
DB / Shared
packages/db/src/schema/heartbeat_runs.ts: adddismissedAtcolumn0026_awesome_gateway.sql:ALTER TABLE heartbeat_runs ADD COLUMN dismissed_at TIMESTAMP WITH TIME ZONEpackages/shared/src/types/heartbeat.ts: adddismissedAt: Date | nulltoHeartbeatRunServer
server/src/services/heartbeat.ts:dismissRun(runId)setsdismissedAt = now()server/src/routes/agents.ts:POST /heartbeat-runs/:runId/dismissserver/src/services/sidebar-badges.ts: filterisNull(heartbeatRuns.dismissedAt)so dismissed runs don't count toward the inbox badgeserver/src/adapters/process/execute.ts: injectPAPERCLIP_API_KEYfromauthTokenin process adapter envUI
ui/src/api/heartbeats.ts: addheartbeatsApi.dismiss(runId)ui/src/pages/Inbox.tsx: dismiss button + mutation on FailedRunCard; filter runs by!r.dismissedAt; new/all tabs; category filter dropdown; touched-issues section; aggregate agent-error alert bannerCLI
disableSignUp: falsetodefaultConfig()in configure commandTest plan
pnpm -r typecheck && pnpm test:run && pnpm build🤖 Generated with Claude Code