Project

General

Profile

Actions

Feature #1524

closed
RA

Feature #1510: Implement internal user roles with ticket progression and assignment controls

Role-gated transition on REST + MCP, and the MCP capability-parity fix

Feature #1524: Role-gated transition on REST + MCP, and the MCP capability-parity fix

Added by Redmine Admin 4 days ago. Updated 4 days ago.

Status:
Shipped
Priority:
Normal
Assignee:
-
Start date:
07/31/2026
Due date:
% Done:

0%

Estimated time:
spec_ref:

docs/superpowers/specs/2026-07-31-internal-roles-design.md

build_status:
build_number:
ci_run_url:
scan_status:
scan_report_url:
deploy_status:
preprod_url:
deployed_at:
branch:
pr_url:
security_key:
severity:
paused:

Description

**Surface:** backend-only

Wire the target-aware predicate into the REST transition route and the MCP `transition_ticket` tool so both planes enforce the identical rule, and close the standing MCP contract breach: an ordinary client can no longer transition a ticket at all.

`TransitionTask` takes the caller (client id, `internal_role`, `is_admin`), resolves the ticket's Redmine assignee back to a dashboard client via `clients.redmine_user_id`, and raises a new `ProgressionForbidden` when the predicate refuses. The route maps it to 403; the MCP handler maps it to a ToolError.

**The credit gate is untouched:** `BacklogNotSubmitted` still runs FIRST, before any role evaluation, and Backlog(16) remains sealed in both directions for everyone.

Also enriches `TaskDetailOut` with `next_status_id` and `can_move_forward`, computed server-side from the domain predicates. (`can_assign` belongs to the assign subtask, not here.)

## Acceptance criteria
- [ ] The route resolves the caller's role, admin flag and assignee-ness and calls the predicate **with the requested target status**; a refusal is a 403 whose message says what the caller may do instead.
- [ ] **Developer picks a non-next status → refused**: an assigned developer requesting anything other than `next_status(current, allowed)` (backward, a skip, or Shipped(13)) gets 403 and **no PUT is issued to Redmine** — asserted for at least three distinct non-next targets.
- [ ] Assigned developer requesting exactly the next status → 200 and the ticket moves.
- [ ] Developer who is not the assignee → 403 even for the next status; an unassigned ticket likewise.
- [ ] cto and admin can set any target in `allowed_statuses`, including backward → 200.
- [ ] cto who is not a project member still gets the unchanged 403 from `owned_project` — the tenant boundary did not widen.
- [ ] **HARD:** a Backlog(16) ticket raises `BacklogNotSubmitted` before any role evaluation — asserted for CTO, admin, assigned Developer and ordinary client, on REST (409) **and** through MCP. A test pins the guard ordering.
- [ ] **HARD:** no path introduced here moves a ticket out of status 16; `submit_ticket` remains the only door and `ticket_charges` is untouched by every test in this subtask.
- [ ] MCP `transition_ticket` called by an ordinary client → ToolError that **names the remaining doors explicitly** (`submit_ticket`, reopen, comment, pause, edit), not a bare "forbidden". Old behaviour removed outright: no flag, no grace path.
- [ ] A shared parameterised test table drives both the REST and the MCP tests so the two planes cannot drift.
- [ ] The `transition_ticket` tool description **and** the capability-parity claim in `docs/superpowers/specs/2026-07-22-client-mcp-server-design.md` are updated in the same change.
- [ ] Regression test: an ordinary client's full remaining verb set (list/read, create, edit, comment, reopen, pause, submit) still works end to end.
- [ ] Before merge, the MCP request logs are grepped for ordinary-client `transition_ticket` calls; the result is recorded in the PR. If a tenant is using it, a one-line release note ships with the deploy.
- [ ] The progression rule appears in no `application` or `presentation` module — only `domain/tasks/progression.py` encodes it.
- [ ] `TaskDetailOut` gains `next_status_id: int | null` and `can_move_forward: bool`.
- [ ] `python -m pytest -q` (serial), `ruff check .`, `mypy src` green.

RA Updated by Redmine Admin 4 days ago Actions #1

  • Status changed from Backlog to Submitted
  • spec_ref updated (diff)

RA Updated by Redmine Admin 4 days ago Actions #2

  • Status changed from Submitted to Shipped

Shipped in master build #107 (2026-07-31). Verified live in production from an ordinary-client MCP identity: `transition_ticket` refused with the actionable message naming the remaining doors, ticket status unchanged, and `comment_on_ticket` still working immediately after. Prod MCP logs showed 6 requests in 30 days, so no tenant was relying on the removed capability and no release note was warranted.

Actions

Also available in: PDF Atom