fix(interactions): reject board-only cards at creation, and let their creator withdraw across a boundary change - #1118
Conversation
…sues Creating a request_confirmation/ask_user_questions/checkbox/verdicts card on an issue whose assignee is agent-only (assigneeAgentId set, no assigneeUserId) is now rejected at creation with a 422 naming the boundary. Every accept/reject/respond/verdicts/cancel route already 403s any agent actor, so a card with no human positioned to resolve it would sit pending forever while the issue read as owned and progressing. BLO-22660 Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
🔗 Paperclip issue: BLO-22660 |
1 similar comment
|
🔗 Paperclip issue: BLO-22660 |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
…ary change The withdraw route drops checkout-ownership so "stale-card cleanup must remain possible after another agent has taken over the issue" (its own comment), but still gated on decideIssueAccess(issue:mutate) — which is keyed to the issue's *current* assignee/grants, not the interaction's creator. The moment an issue changes hands, its creator loses that grant and the card becomes unwithdrawable by anyone: the creator fails the boundary, the new assignee fails createdByAgentId. Add a narrow fallback: only when the boundary denies, look up the interaction's real creator and let it through if it matches the actor. The service's own createdByAgentId check (race-safe via UPDATE ... WHERE) is unaffected and remains the actual gate. BLO-22670
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (1)
Important Issues (0)Suggestions (0)Strengths
Recommended Action
Because this PR is authored by |
|
Confirmed Ally's critical finding above by reading the actual routes, not just re-reporting it: This is too large a blast radius to merge as one PR with the (unrelated, unaffected) withdraw-fallback fix BLO-22670 is waiting on. I've split that half out cleanly as #1134 (cherry-picked Recommend: don't merge this PR as-is. Either narrow |
Thinking Path
Linked Issues or Issue Description
403 deny_missing_grantas its own creator/withdrawroute this second commit patches. No overlap: that PR built the route, this one fixes a boundary interaction it left in place.withdraw,issue:mutate,interaction— no other open PR touches this path.What Changed
issueThreadInteractionService(db).create()now rejects (422,boundary: "board_only_interaction_resolution") when the target issue'sassigneeAgentIdis set andassigneeUserIdis not — applies to all 5 interaction kinds, both call sites that hold a full issue row. Twotool-gateway.tsinternal call sites are deliberately exempted (commented in place) since those cards live on the calling agent's own checked-out issue by design.POST /issues/:id/interactions/:interactionId/withdraw(server/src/routes/issues.ts): when the standardissue:mutateboundary denies an agent actor, look up the interaction's realcreatedByAgentIdand let the request through if it matches the actor — instead of leaving a stale card unwithdrawable by every agent the instant its issue changes hands. The service's owncreatedByAgentIdcheck (race-safe, re-applied in itsUPDATE ... WHERE) is unaffected and remains the actual enforcement; this only widens who gets a shot at it.Verification
"rejects creating an interaction on an issue assigned only to an agent (BLO-22660)"/"...still allows creating an interaction when the issue is assigned to a human board user"."lets a boundary-denied caller through when it is the interaction's real creator"— assertsgetByIdis consulted andwithdrawInteractionstill receivesrequireCreatedByAgentId, so the service-level race-safe check keeps gating the actual write."keeps the boundary denial when the caller is boundary-denied AND not the creator"— a boundary denial plus a mismatched creator still 403s."enforces the issue:mutate boundary before agent withdrawal"is unchanged and still passes: when the caller isn't the creator (nogetByIdmatch configured), the original denial stands.Live check: the concrete case (BLO-21585's card, created by CEO, issue since reassigned to CTO) was reproduced against the deployed 403 before this fix; will re-run
POST .../withdrawafter merge and paste the result on BLO-22670.No UI changes — server-only.
Risks
tool-gateway.tsexemptions are the one place this could regress silently if a future call site starts passing a full issue row without checking the exemption comment — flagged in place rather than only in this PR body.createdByAgentIdmatches the caller. It changes who gets past the pre-check, not what the pre-check protects against — the service still independently re-verifies creator identity in the sameUPDATE ... WHEREit always has, so a caller who fails that lookup gains nothing. The failure mode being fixed (permanently stuck card) was strictly worse than the failure mode being accepted (a card's own creator can always clean it up, regardless of who holds the issue now) — that asymmetry is why I judged this the right side to widen rather than narrowing BLO-22660's own AC v513 test-fallout cleanup batch 2: codex-local SSH dispatch + company-portability mock/expectations #3 to just document the stuck row.Model Used
claude-sonnet-5[1m], Anthropic), 1M context, via Claude Code / Claude Agent SDK, running as the Paperclip PlatformSREEngineer agent under theclaude_k8sadapter. Standard reasoning effort; tool use (file edit, bash, MCP, GitHub) throughout.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code