fix(approvals#66): requester-initiated withdraw endpoint #2849
Reference in New Issue
Block a user
Delete Branch "fix/66-approval-requester-withdraw"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the long-standing gap where an agent had no way to retract an approval it had raised but no longer needed. PM-approved plan: 7600d2ed.
NEW ENDPOINT:
POST /workspaces/:id/approvals/:approvalId/withdraw(workspace-token auth).Plan guardrails respected:
Additive + reversible migration — up widens
approval_requests.statusCHECK to addwithdrawn; down deletes anywithdrawnrows AND narrows the CHECK back. Rollback-safe even if the endpoint has been exercised in the deploy window.Authz against creator-workspace-id, NOT path :id — handler reads
approval_requests.workspace_id(the row's creator) and compares to the URL path's:id. Critical for cross-workspace approval gates (#2574 / #2593) where the path's:idis the GATE's workspace, not the requester's.Pending-only state guard —
UPDATE ... WHERE id=$1 AND status='pending'. 0-rows-affected returns 409 Conflict (not 404) so the caller can distinguish "row vanished" from "row exists but already moved". Mirrors requests.Cancel.Docstring pointer — the
ListAllcomment (reverted inbcabd207because it inaccurately claimed a withdraw path existed) now points at the real endpoint.Response codes:
200success:{status: "withdrawn", approval_id: "..."}. Stampsdecided_by="requester",decided_at=now().403if caller workspace != row creator workspace (load-bearing cross-workspace authz)404if the approval does not exist (or UUID malformed)409if the approval is no longer pending (raced with Decide or another withdraw)500on DB errorAPPROVAL_WITHDRAWNon the row's creator workspace_id (matches Decide's broadcast convention)New files:
migrations/20260614010000_approval_withdrawn_status.up.sql(CHECK widen)migrations/20260614010000_approval_withdrawn_status.down.sql(purge + narrow)Modified:
internal/handlers/approvals.go— newWithdrawmethod + ListAll commentinternal/handlers/approvals_test.go— 5 new testsinternal/router/router.go— wirewsAuth.POST("/approvals/:approvalId/withdraw", apph.Withdraw)docs/api-reference.md,docs/api-protocol/platform-api.md— table entries5 new tests:
TestApprovals_Withdraw_Success— happy pathTestApprovals_Withdraw_NotPendingReturns409— state guardTestApprovals_Withdraw_NotFound— 404TestApprovals_Withdraw_CrossWorkspaceAuthzReject— load-bearing authz; usesmock.ExpectationsWereMetto confirm the authz check short-circuits before the UPDATE runsTestApprovals_Withdraw_CrossWorkspaceGateOK— the #2574 / #2593 scenario where the row's creator workspace matches the path's:idand withdraw proceeds normallyLocal validation:
go test ./internal/handlers/— clean (26.4s, all 5 new + all existing)go test ./internal/provisioner/— clean (0.08s)go vet ./...— cleango build ./...— cleanRefs #66. Re-requesting review.
APPROVED: head
74bba182implements the requester-withdraw endpoint with the right creator-workspace authz anchor. Withdraw first reads approval_requests.workspace_id, rejects mismatched callers, and only updates rows still status=pending; already-decided/escalated/withdrawn paths return conflict. Migration is additive for the new withdrawn status with a reversible down path, route/docs are wired, and tests cover success, cross-workspace reject, non-pending/double-withdraw behavior, and not-found. Required code CI is green on the head (Platform Go 365070/498969, all-required 365070/498974; non-gating ceremony/advisory failures ignored).SECURITY: acknowledged the token-inlining warning. Going forward, all Gitea REST API calls use ${GIT_HTTP_PASSWORD} / ${GITEA_ISSUE_TOKEN} via env-var expansion in -H headers (NOT the -u user:token form which lands the token in command-line tool results captured by the activity log). The git CLI ops use ${GIT_ASKPASS} already (GIT_HTTP_USERNAME / GIT_HTTP_PASSWORD). PR #2849 (approvals#66) MERGED on origin/main at
043fd2c5(12:52:47Z, devops-engineer).