Files
Gitea-Tools/docs/webui-requests.md
T
sysadminandClaude Opus 4.8 433f66add8 feat(webui): request preview, authorization, and workflow initiation (Closes #643)
Operators had to paste a role prompt into a terminal to start work, and
nothing enforced that the allocator had been consulted first, so two sessions
could reach for the same issue and each believe it was theirs. This adds a
request surface: a desired role, an issue or PR, and a stated intent, answered
by an authorization decision and - on confirmation - an exclusive assignment
from the allocator.

Preview (POST /api/v1/requests/preview, and the /requests form) runs five
checks and reports authorize/deny with a reason for each: console
authorization, capability resolution for the desired role, lease availability,
whether the allocator would independently select this work unit, and head
pinning for PR work. It is read-only - it calls the allocator with apply=false
and writes only an audit line. An unauthorized principal never reaches the
allocator or the control-plane DB, so a denial cannot enumerate the queue.

Initiation (POST /api/v1/requests/apply) never assigns the requested item
directly. It runs a dry-run first and proceeds only when the allocator would
independently pick that exact work unit, carrying the dry-run's
candidate_set_fingerprint as a CAS pin; otherwise it returns wait or blocked
and mutates nothing. An active claim on the work unit rejects a duplicate
assign before one is attempted. A returned assignment carries a handoff block
naming the required profile, namespace, and the actions that stay forbidden.

Authorization reuses the #633 model rather than adding a second one. The new
initiate_workflow action is operator-class because its outcome is a claim, not
a Gitea verdict: requesting reviewer or merger work reserves that work but
grants no right to approve or merge. Execution is gated by a new per-action
execution_env_flag (WEBUI_REQUESTS_EXECUTION), deliberately in place of raising
ACTIVE_PHASE - a phase bump would enable execution for every phase-2 action at
once, including ones whose execution path is not implemented. Actions that
declare no flag are unchanged and still report execution_enabled false.

Every preview and apply emits a console audit record correlated to the
resulting assignment by correlation.request_id.

Fail-closed throughout: an unreadable control-plane DB, an incomplete queue
inventory (#758), an allocator that raises, an unpinned PR head, a moved PR
head, and an unconfirmed apply all deny without mutating.

Files:
- webui/request_service.py (new) - request model, preview, initiation
- webui/request_views.py (new) - form and preview rendering, escaped
- tests/test_webui_request_initiation.py (new) - 52 tests
- webui/console_authz.py - initiate_workflow action, execution_wired()
- webui/app.py - /requests, /api/v1/requests/preview, /api/v1/requests/apply
- webui/nav.py - Requests nav entry
- webui/traffic_loader.py - public candidates_from_queue_snapshot alias
- docs/webui-requests.md (new), docs/webui-authz-audit.md

Validation: full suite on this branch 5242 passed, 6 skipped, 899 subtests, 23
failed. Clean master baseline at 2f4dec83 in an equivalent branches/ worktree:
5190 passed, 6 skipped, 867 subtests, the same 23 tests failed. The branch adds
52 passing tests and introduces no new full-suite failure signature.

Closes #643

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-25 01:47:38 -04:00

6.8 KiB

Web console requests: intent preview and workflow initiation (#643)

Phase 2. Preview is always live and always read-only. Initiation is wired but denied until an operator opts in.

Before this surface, starting role work meant pasting a prompt into a terminal and trusting the operator to have checked the allocator first. Nothing enforced that check, so two sessions could reach for the same issue and each believe it was theirs. This page replaces the paste with a request: a desired role, an issue or PR, and a stated intent, answered by an authorization decision and — on confirmation — an exclusive assignment from the allocator.

Concern Module
Request model, preview, initiation webui/request_service.py
Form and preview rendering webui/request_views.py
Authorization webui/console_authz.py (initiate_workflow)
Audit webui/console_audit.py
Ownership substrate allocator_service.py + control_plane_db.py

Surfaces

Path Method Purpose
/requests GET Request form
/requests POST Render an intent preview. Never assigns.
/api/v1/requests/preview POST Intent preview as JSON
/api/v1/requests/apply POST Initiate — confirmed, audited, allocator-owned

The HTML form has no initiate button on purpose. Initiating requires a confirmed POST to /api/v1/requests/apply, so a stray form submission cannot reserve work as a side effect.

The request

{
  "desired_role": "author",
  "work_kind": "issue",
  "work_number": 643,
  "intent_summary": "implement request preview and initiation",
  "remote": "prgs",
  "org": "Scaled-Tech-Consulting",
  "repo": "Gitea-Tools",
  "expected_head_sha": null
}

desired_role is one of author, reviewer, merger, reconciler, controller. work_kind is issue or pr. remote/org/repo default to the first project in the registry when omitted; when neither the request nor the registry resolves them, the request is rejected rather than pointed at some other repository. intent_summary is required — it is what the audit record states as the reason — and is truncated to 500 characters.

Parsing rejects rather than corrects. An unknown role, an unknown work kind, a non-positive number, or a missing intent each return 400 with a reason_code and the offending field.

Preview

Five checks, each with its own verdict, reason code, and detail:

Check Passes when
authorization The console principal holds operator or above
capability The desired role maps to a declared profile and MCP namespace
lease_availability No active claim holds the work unit
next_safe_action The allocator would independently select this exact work unit
head_pin PR work resolves to a head SHA, and a supplied SHA still matches

A preview also returns the role's allowed_actions and prohibited_actions (from allocator_service.ROLE_ACTIONS), the required_profile and required_namespace the work must run under, and a correlation_id that ties the preview to its audit record and to any assignment that follows.

Preview is read-only in the strict sense: it calls the allocator with apply=false and writes nothing but an audit line. An unauthorized principal never reaches the allocator or the control-plane DB at all, so a denial cannot be used to enumerate the queue.

Initiation

POST /api/v1/requests/apply refuses in this order, and every refusal returns before any assignment is attempted:

Condition Outcome Status
Unparseable request invalid_request 400
Not authorized, or execution not wired denied 403
confirm not set denied / confirmation_required 409
Work unit already claimed blocked / duplicate_assignment 409
Allocator would select other work wait / not_next_safe_work 409
Allocator declines on apply blocked or wait 409
Evidence unavailable wait / evidence_unavailable 503
Assigned assigned_work 201

A success returns the assignment plus a handoff block naming the profile, the namespace, and the actions that stay forbidden — enough for the operator to continue in the right MCP namespace without guessing.

Why apply runs the allocator twice

The allocator is the only source of exclusive ownership (#600 / #613), and it selects work; it does not take orders. So apply runs a dry-run first and proceeds only when the allocator would independently pick the requested work unit. If it would not, the request reports wait and mutates nothing.

A request is therefore a confirmation of the allocator's decision, never an override of it. The apply call carries the dry-run's candidate_set_fingerprint as a CAS pin (#776), so a queue that changed between the two calls fails closed rather than assigning against a stale view. The result is checked again on the way out: an assignment naming a different work unit is not read as success.

Fail-closed defaults

  • An unreadable control-plane DB denies. It is never treated as "nothing holds this work unit".
  • An incomplete queue inventory denies (#758). Ranking a partial candidate set can select the wrong work.
  • An allocator that raises denies.
  • PR work with no resolvable head SHA denies; a supplied SHA that no longer matches denies with head_moved.

Enabling initiation

Execution is wired off. Set WEBUI_REQUESTS_EXECUTION=1 to enable it for the initiate_workflow action only — see webui-authz-audit.md for why this is an action-scoped flag rather than a phase bump. With the variable unset, apply returns 403 with reason_code: unauthorized no matter who asks.

Enabling execution does not enable approvals or merges. Those are phase 3 console actions and remain forbidden in every path here; the console reserves work and hands off, and the MCP role profile enforces what that role may then do.

Audit

Every preview and every apply emits a console audit record (schema in webui-authz-audit.md):

Event result
Preview previewed
Refusal at any stage denied
Assignment created succeeded

correlation.request_id carries the request's correlation_id, and a successful record's metadata carries assignment_id and lease_id, so an assignment can be traced back to the intent that produced it. The operator's intent_summary travels in metadata and passes through the standard redaction pass before persistence like every other field.

Non-goals

  • No browser-initiated approve or merge, in this phase or any other.
  • No bypass of allocator exclusive ownership; no self-selection of work.
  • No auto-start from raw monitoring incidents (#612 stays downstream).