fix: reject empty payload.title on approval create (BLO-21032) - #975
fix: reject empty payload.title on approval create (BLO-21032)#975allyblockcast[bot] wants to merge 3 commits into
Conversation
|
🔗 Paperclip issue: BLO-21032 |
|
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. Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
|
Staff structural review at head
The three cited legacy approvals were re-read during this review; each has a non-empty |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Important Issues (3)
Strengths
Recommended Action
This PR is authored by |
ee10763 to
e9388e3
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Important Issues (3)
Strengths
Recommended Action
This PR is authored by |
kkroo
left a comment
There was a problem hiding this comment.
Reviewed approval payload title validation and fixed the stale MCP create-approval test fixture. Local MCP/shared checks pass.
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
Board approval requests are created with a caller-supplied `payload` object. The board UI, the Inbox approvals list, and Slack approval cards all render a subject/title derived from that payload, but nothing validated that it contained a title, so an agent that wrote a rich custom payload (summary, decision_requested, exact_human_action_required, ...) and omitted `title` could file a card with no server-side signal that anything was missing. Because `payload` is immutable after create (no update handler exists), such a card can never be repaired in place, only replaced — which duplicates the ask. - packages/shared/src/validators/approval.ts: createApprovalSchema's payload field now requires payload.title to be a non-empty, non-whitespace string, via a refinement nested on the payload field rather than a top-level superRefine on the object. This keeps createApprovalSchema a plain ZodObject so `.merge(createApprovalSchema)` in the paperclipCreateApproval MCP tool schema keeps working. The ZodError path is ["payload", "title"] and the message tells the caller what to add, so an agent can self-correct without a round trip. This one schema change covers the HTTP route, the CLI client, and the MCP tool at once. - Add regression tests (schema-level and route-level) for the 4xx rejection across all four approval types (absent / "" / whitespace-only title) and 2xx acceptance with a valid title. - Add a component test locking in the existing title -> name -> summary -> recommendedAction -> type fallback chain in ApprovalPayload.tsx (already used by ApprovalCard, the Inbox list, and ApprovalDetail) for a payload.title = null card, so pre-existing untitled records keep rendering non-blank without needing to be re-filed. Co-Authored-By: Paperclip <noreply@paperclip.ing>
7410d5a to
8ad30b9
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Critical Issues (0)Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Looks good. Prior Findings Dispositioned (2)
Critical Issues (0)Important Issues (0)Strengths
Recommended Action
|
Thinking Path
Linked Issues or Issue Description
This repository has GitHub Issues disabled, so per CONTRIBUTING.md the underlying issue is described inline below (tracked internally as Paperclip issue BLO-21032).
What happened?
Approval cards created via
POST /companies/:companyId/approvals(and the equivalent MCPpaperclipCreateApprovaltool / CLI client) were accepted even whenpayload.titlewas absent, empty, or whitespace-only. The board UI, Inbox approvals list, and Slack approval-card formatter all derive their rendered subject frompayload.title, so these cards rendered as blank, undecidable rows. Becausepayloadis immutable after create, the only "fix" available to an agent was to file a duplicate card with a title, which is how the same ask ended up scattered across four cards in a prior incident.Expected behavior
POST /companies/:companyId/approvals(and the shared Zod schema used by the MCP tool and CLI) should reject a request whosepayload.titleis absent, empty, or whitespace-only with a 4xx response that names the field and tells the caller what to do, for every approvaltype(hire_agent,approve_ceo_strategy,budget_override_required,request_board_approval). Already-filed pending approvals with an empty title should render with a non-blank, human-readable fallback in the board UI instead of a blank row, without mutating the underlying record.Steps to reproduce
paperclipCreateApproval/POST /companies/:companyId/approvalswith any validtypeand apayloadobject that omitstitle(or sets it to""/" ").pendingapproval.PATCH /approvals/{id}(404, no handler for any principal).What Changed
packages/shared/src/validators/approval.ts—createApprovalSchema'spayloadfield now requirespayload.titleto be a non-empty, non-whitespace string via a refinement nested on thepayloadfield (not a top-level.superRefine()on the whole object). This keepscreateApprovalSchemaa plainZodObject, which matters becausepackages/mcp-server/src/tools.tsdoesz.object({...}).merge(createApprovalSchema)to build thepaperclipCreateApprovaltool schema —.merge()isn't available on theZodEffectsa top-level refine would produce. One shared schema change closes the HTTP route, the CLI client, and the MCP tool at once. TheZodErrorpath is["payload", "title"]and the message tells the caller what to add, so an agent gets a 4xx it can act on without a round trip.""/ whitespace-only title, across all four approval types) and 2xx acceptance with a valid title; a UI component test locking in the existingtitle → name → summary → recommendedAction → typefallback chain inApprovalPayload.tsx(already used byApprovalCard, the Inbox list, andApprovalDetail) for apayload.title = nullcard, so already-filed untitled records keep rendering non-blank without being re-filed.No existing approval record is read or written by this change — only the create path gains validation, and the UI test targets already-shipped rendering code.
Verification
pnpm --filter @paperclipai/shared vitest run src/validators/approval.test.ts— 20/20 passpnpm --filter @paperclipai/server vitest run src/__tests__/approval-routes-idempotency.test.ts— 18/18 passpnpm --filter @paperclipai/ui vitest run src/components/ApprovalPayload.test.tsx— 7/7 passtsc --noEmitclean onpackages/sharedandpackages/mcp-serverpaperclipListApprovals(status:"pending"), filternot (payload.title or "").strip()→ 0 for cards created after this ships; the 3 named pre-existing records (2141dc60,d3d4b1a4,13c61f79) already render non-blank via the fallback chain covered by the new component test, without being re-filed or mutated.Risks
Low risk. This only tightens validation on the approval-create path — no existing approval record is read, written, or migrated. The UI fallback is presentation-only and only changes rendering for records that were already unreadable blank rows, so there's no regression risk for titled cards. The main behavioral shift is that a caller omitting
payload.titlenow gets a 4xx instead of a silent 201; this is intentional and is exactly what the linked issue asks for.Model Used
Claude Sonnet 5 (
claude-sonnet-5[1m], 1M context window), via Claude Code, agent role Platform/SRE Engineer, no extended-thinking mode.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code
Paperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-21032