Skip to content

bug(approvals): the resubmit path skips action validation, so a target can change after a human reviewed it #17349

Open

Description

@mrveiss

What

#17315 / PR #17323 close the unregistered-action hole on POST /approval-gates by routing it through _reject_unregistered_action. The resubmit path is a second entry point to the same defect and was not covered, because it is pre-existing code the PR did not touch.

Found during the pre-merge review of #17323. Not a regression from that PR, and explicitly not a fail-open of the deletion gate — see scope below.

Where

  • autobot-backend/api/approval_gates.py:323-336 (resubmit endpoint)
  • autobot-backend/services/approval_gate_service.py (resubmit())

Both write approval.context = context verbatim. Neither calls _reject_unregistered_action, and neither checks who is allowed to resubmit.

Confirmed pre-existing: identical on origin/main:autobot-backend/api/approval_gates.py:296-315.

Why it matters

An approval that has reached REVISION_REQUESTED can be resubmitted with an arbitrary context.action:

  1. It reproduces bug(approvals-seam): the orphan-storage deletion gate has a detector and an executor but no proposer — the registered post-approval action is unreachable #17315's defect. An unregistered action reaches the dispatcher and mints a post_action_failed anomaly — exactly what fix(approvals): wire the orphan-storage deletion gate end to end and validate the action at creation (#17315) #17323 just closed on the other route. The validation now exists and this path routes around it.
  2. The subtler one, and the reason this is worth more than a tidy-up. For an action that is registered and destructive, a caller can swap the provider / candidate_id between a human's revision request and their subsequent approve. The human reviews one target and approves a different one. The approval record and the audit trail both stay internally consistent, so nothing downstream looks wrong — the substitution is invisible at the point it matters.

Scope — what this is not

Deletion still requires the unchanged require_interactive_human-gated approve() call (api/approval_gates.py:220-221, #17042), and execute_orphan_storage_delete re-validates provider and candidate at execution time rather than trusting the proposal. So this does not let anyone delete without a human, and #17038 is not breached. It lets a caller change what the human is approving after they have looked at it.

Acceptance criteria

  • resubmit rejects an unregistered context.action through the same _reject_unregistered_action path as POST /approval-gates, with the same status and detail.
  • A test asserts the specific refusal — exact status code and detail substring — and that no state transition is recorded. Not != 200.
  • An ownership or authorship check governs who may resubmit a given approval.
  • For an already-registered destructive action, a test asserts that resubmit cannot change provider / candidate_id after a revision request — or, if changing them is intended, that doing so resets the approval to PENDING so the human re-reviews the new target rather than approving the old one.

Activity

  1. mrveiss commented on Oct 12, 2026

    @mrveiss
    OwnerAuthor

    Follow-up from #18453 (merged approach): resubmit now locks context.action and every reviewed field (context_change_violation), and only the original requester may resubmit. Remaining LOW: the requester match compares the verified id or the username (or its suffix after the last colon) against requested_by_agent. With the action and reviewed fields frozen, this only governs who may edit description and free text, but it should match on the verified user id only, as #18453's decide fix does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions