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]>
This commit is contained in:
2026-07-25 01:47:38 -04:00
co-authored by Claude Opus 4.8
parent 2f4dec8323
commit 433f66add8
9 changed files with 2447 additions and 17 deletions
+967
View File
@@ -0,0 +1,967 @@
"""Operator work-request preview and initiation (#643, Phase 2).
An operator's alternative to pasting a role prompt into a terminal. A
*request* names three things — the role to run as, the issue or PR to run
against, and what the operator intends — and this module answers two questions
about it:
* **Preview** (:func:`preview_request`) — would that request be authorized,
is the work unit actually free, is it the next safe thing that role should
touch, and which actions stay prohibited? Read-only, always. It creates no
assignment and never mutates.
* **Initiate** (:func:`apply_request`) — turn an authorized request into an
*exclusive assignment*, and only ever through the allocator.
Three invariants hold and are the reason this module exists rather than a
direct call to :func:`allocator_service.allocate_next_work` from a route:
1. **The allocator remains the only source of exclusive ownership** (#600 /
#613). ``apply`` never assigns the requested item directly. It runs a
dry-run first and proceeds only when the allocator would independently pick
that exact item; otherwise it reports ``wait`` and mutates nothing. A
request is therefore a *confirmation* of the allocator's decision, never an
override of it.
2. **Duplicate assignment is rejected before it is attempted.** An active
claim on the work unit — held by any session, this one included — blocks.
3. **Fail closed at every unknown.** An unparseable request, an unavailable
control-plane DB, an incomplete queue inventory, or an unresolved
authorization all deny. There is no branch that proceeds on missing
evidence.
Authorization comes from :mod:`webui.console_authz` (``initiate_workflow``)
and every outcome is audited through :mod:`webui.console_audit`, correlated to
the resulting assignment by ``correlation_id``.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Mapping, Sequence
import allocator_service
from task_capability_map import required_permission, required_role
from webui import console_audit, console_authz
# The console action this module is gated by. Registered in console_authz.
ACTION_ID = "initiate_workflow"
KIND_ISSUE = "issue"
KIND_PR = "pr"
WORK_KINDS: tuple[str, ...] = (KIND_ISSUE, KIND_PR)
REQUESTABLE_ROLES: tuple[str, ...] = (
allocator_service.ROLE_AUTHOR,
allocator_service.ROLE_REVIEWER,
allocator_service.ROLE_MERGER,
allocator_service.ROLE_RECONCILER,
allocator_service.ROLE_CONTROLLER,
)
# Intent is operator prose echoed back into an audit record. Bounded so a
# pasted transcript cannot bloat the append-only log.
MAX_INTENT_CHARS = 500
# --- Outcomes ---------------------------------------------------------------
OUTCOME_ASSIGNED = allocator_service.OUTCOME_ASSIGNED
OUTCOME_WAIT = allocator_service.OUTCOME_WAIT
OUTCOME_BLOCKED = "blocked"
OUTCOME_DENIED = "denied"
OUTCOME_INVALID = "invalid_request"
OUTCOME_PREVIEW = allocator_service.OUTCOME_PREVIEW
# --- Reason codes -----------------------------------------------------------
REASON_AUTHORIZED = "request_authorized"
REASON_PREVIEW_OK = "preview_authorized"
REASON_UNAUTHORIZED = "unauthorized"
REASON_NOT_NEXT_SAFE = "not_next_safe_work"
REASON_DUPLICATE_ASSIGNMENT = "duplicate_assignment"
REASON_CONFIRMATION_REQUIRED = "confirmation_required"
REASON_EVIDENCE_UNAVAILABLE = "evidence_unavailable"
REASON_ALLOCATOR_OUTCOME = "allocator_declined"
# --- Check names ------------------------------------------------------------
CHECK_AUTHORIZATION = "authorization"
CHECK_CAPABILITY = "capability"
CHECK_LEASE_AVAILABILITY = "lease_availability"
CHECK_NEXT_SAFE_ACTION = "next_safe_action"
CHECK_HEAD_PIN = "head_pin"
# --- Request model ----------------------------------------------------------
@dataclass(frozen=True)
class WorkRequest:
"""One operator request: a role, a work unit, and a stated intent."""
desired_role: str
work_kind: str
work_number: int
intent_summary: str
remote: str
org: str
repo: str
expected_head_sha: str | None = None
@property
def work_key(self) -> tuple[str, int]:
return (self.work_kind, self.work_number)
@property
def display_ref(self) -> str:
return f"#{self.work_number}"
def to_dict(self) -> dict[str, Any]:
return {
"desired_role": self.desired_role,
"work_kind": self.work_kind,
"work_number": self.work_number,
"intent_summary": self.intent_summary,
"remote": self.remote,
"org": self.org,
"repo": self.repo,
"expected_head_sha": self.expected_head_sha,
}
@dataclass(frozen=True)
class RequestError:
"""A rejected request, with the field that caused the rejection."""
reason_code: str
detail: str
field_name: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"ok": False,
"outcome": OUTCOME_INVALID,
"reason_code": self.reason_code,
"detail": self.detail,
"field": self.field_name,
}
def _clean(value: Any) -> str:
return str(value or "").strip()
def parse_request(
payload: Mapping[str, Any] | None,
*,
default_scope: Mapping[str, str] | None = None,
) -> tuple[WorkRequest | None, RequestError | None]:
"""Validate an operator payload into a :class:`WorkRequest`.
Returns ``(request, None)`` or ``(None, error)``. Never raises and never
guesses: an unknown role, an unknown work kind, or a non-positive number is
an error rather than a silently corrected value.
"""
body = dict(payload or {})
scope = dict(default_scope or {})
role = _clean(body.get("desired_role") or body.get("role")).lower()
if role not in REQUESTABLE_ROLES:
return None, RequestError(
reason_code="unknown_role",
detail=(
f"desired_role must be one of {', '.join(REQUESTABLE_ROLES)}; "
f"got {role or '(empty)'!r}."
),
field_name="desired_role",
)
kind = _clean(body.get("work_kind") or body.get("kind")).lower()
if kind not in WORK_KINDS:
return None, RequestError(
reason_code="unknown_work_kind",
detail=(
f"work_kind must be 'issue' or 'pr'; got {kind or '(empty)'!r}."
),
field_name="work_kind",
)
raw_number = body.get("work_number")
if raw_number is None:
raw_number = (
body.get("pr_number") if kind == KIND_PR else body.get("issue_number")
)
if raw_number is None:
raw_number = body.get("number")
try:
number = int(str(raw_number).strip())
except (TypeError, ValueError):
return None, RequestError(
reason_code="invalid_work_number",
detail=f"work_number must be an integer; got {raw_number!r}.",
field_name="work_number",
)
if number <= 0:
return None, RequestError(
reason_code="invalid_work_number",
detail="work_number must be a positive issue or PR number.",
field_name="work_number",
)
intent = _clean(body.get("intent_summary") or body.get("intent"))
if not intent:
return None, RequestError(
reason_code="missing_intent",
detail="intent_summary is required so the audit record states why.",
field_name="intent_summary",
)
intent = intent[:MAX_INTENT_CHARS]
remote = _clean(body.get("remote")) or _clean(scope.get("remote"))
org = _clean(body.get("org")) or _clean(scope.get("org"))
repo = _clean(body.get("repo")) or _clean(scope.get("repo"))
if not (remote and org and repo):
return None, RequestError(
reason_code="scope_unresolved",
detail=(
"remote, org, and repo could not be resolved from the request "
"or the project registry."
),
field_name="repo",
)
head = _clean(body.get("expected_head_sha")) or None
return (
WorkRequest(
desired_role=role,
work_kind=kind,
work_number=number,
intent_summary=intent,
remote=remote,
org=org,
repo=repo,
expected_head_sha=head,
),
None,
)
# --- Preview ----------------------------------------------------------------
@dataclass(frozen=True)
class RequestCheck:
"""One named precondition and its verdict."""
name: str
ok: bool
reason_code: str
detail: str
evidence: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"ok": self.ok,
"reason_code": self.reason_code,
"detail": self.detail,
"evidence": dict(self.evidence),
}
@dataclass(frozen=True)
class RequestPreview:
"""The full intent preview for one request. Read-only in every field."""
request: WorkRequest
authorized: bool
reason_code: str
detail: str
authorization: dict[str, Any]
checks: tuple[RequestCheck, ...]
prohibited_actions: tuple[str, ...]
allowed_actions: tuple[str, ...]
next_safe_action: str
required_profile: str
required_namespace: str
required_permission: str
correlation_id: str
allocator_evidence: dict[str, Any] = field(default_factory=dict)
@property
def failed_checks(self) -> tuple[RequestCheck, ...]:
return tuple(c for c in self.checks if not c.ok)
def to_dict(self) -> dict[str, Any]:
return {
"ok": self.authorized,
"outcome": OUTCOME_PREVIEW,
"dry_run": True,
"mutation_performed": False,
"authorized": self.authorized,
"reason_code": self.reason_code,
"detail": self.detail,
"request": self.request.to_dict(),
"authorization": dict(self.authorization),
"checks": [c.to_dict() for c in self.checks],
"failed_checks": [c.name for c in self.failed_checks],
"prohibited_actions": list(self.prohibited_actions),
"allowed_actions": list(self.allowed_actions),
"next_safe_action": self.next_safe_action,
"required_profile": self.required_profile,
"required_namespace": self.required_namespace,
"required_permission": self.required_permission,
"correlation_id": self.correlation_id,
"allocator_evidence": dict(self.allocator_evidence),
}
AllocatorFn = Callable[..., dict[str, Any] | None]
ClaimsFn = Callable[["WorkRequest"], Mapping[tuple[str, int], dict[str, Any]]]
def _correlation_id() -> str:
return f"req-{uuid.uuid4().hex}"
def _selection_matches(
selection: Mapping[str, Any] | None, request: WorkRequest
) -> bool:
if not selection:
return False
kind = _clean(selection.get("kind")).lower()
try:
number_int = int(selection.get("number"))
except (TypeError, ValueError):
return False
return (kind, number_int) == request.work_key
def _authorization_check(
decision: console_authz.AuthorizationDecision,
) -> RequestCheck:
return RequestCheck(
name=CHECK_AUTHORIZATION,
ok=bool(decision.allowed),
reason_code=decision.reason_code,
detail=decision.detail,
evidence={
"subject": decision.principal.subject,
"role": decision.principal.role,
"required_role": decision.required_role,
"identity_source": decision.principal.identity_source,
},
)
def _capability_check(request: WorkRequest) -> RequestCheck:
"""Whether the requested role maps to a declared MCP capability.
The console never invents an authority: the permission and role come from
``task_capability_map`` via the same ``allocate_next_work`` task the MCP
allocator gates on.
"""
# The remote-prefixed hint keeps a dadeschools request from being told to
# run under a prgs profile; ``required_profile_for_role`` preserves the
# prefix when one is present and falls back to its own default otherwise.
profile_hint = f"{request.remote}-{request.desired_role}"
try:
profile = allocator_service.required_profile_for_role(
request.desired_role, profile_name=profile_hint
)
namespace = allocator_service.required_namespace_for_role(
request.desired_role, profile_name=profile_hint
)
except Exception as exc: # noqa: BLE001 — an unresolved role is a denial
return RequestCheck(
name=CHECK_CAPABILITY,
ok=False,
reason_code="capability_unresolved",
detail=(
f"no profile/namespace maps to role {request.desired_role!r}: "
f"{exc}"
),
)
resolved = bool(profile and namespace)
return RequestCheck(
name=CHECK_CAPABILITY,
ok=resolved,
reason_code="capability_resolved" if resolved else "capability_unresolved",
detail=(
f"role {request.desired_role!r} runs under profile {profile!r} in "
f"MCP namespace {namespace!r}."
),
evidence={
"required_profile": profile,
"required_namespace": namespace,
"required_permission": required_permission("allocate_next_work"),
"capability_role": required_role("allocate_next_work"),
},
)
def _lease_check(
request: WorkRequest,
claims: Mapping[tuple[str, int], dict[str, Any]] | None,
) -> RequestCheck:
"""Whether the work unit is free of an active claim.
``claims is None`` means the control-plane DB could not be read. That is a
failure, not an absence of claims: an unreadable substrate must never read
as "nothing holds this".
"""
if claims is None:
return RequestCheck(
name=CHECK_LEASE_AVAILABILITY,
ok=False,
reason_code=REASON_EVIDENCE_UNAVAILABLE,
detail=(
"active-claim inventory is unavailable; refusing to treat an "
"unreadable control-plane DB as an unclaimed work unit."
),
)
claim = claims.get(request.work_key)
if claim:
return RequestCheck(
name=CHECK_LEASE_AVAILABILITY,
ok=False,
reason_code=REASON_DUPLICATE_ASSIGNMENT,
detail=(
f"{request.work_kind} {request.display_ref} already carries an "
f"active {claim.get('role') or 'unknown'} lease."
),
evidence={
"lease_id": claim.get("lease_id"),
"session_id": claim.get("session_id"),
"role": claim.get("role"),
"expires_at": claim.get("expires_at"),
},
)
return RequestCheck(
name=CHECK_LEASE_AVAILABILITY,
ok=True,
reason_code="lease_available",
detail=f"no active lease holds {request.work_kind} {request.display_ref}.",
)
def _next_safe_action_check(
request: WorkRequest, allocation: Mapping[str, Any] | None
) -> RequestCheck:
"""Whether the allocator would independently select this exact work unit."""
if not allocation:
return RequestCheck(
name=CHECK_NEXT_SAFE_ACTION,
ok=False,
reason_code=REASON_EVIDENCE_UNAVAILABLE,
detail="allocator dry-run produced no result; refusing to proceed.",
)
selection = allocation.get("selected") or {}
outcome = _clean(allocation.get("outcome"))
if not _selection_matches(selection, request):
chosen = (
f"{_clean(selection.get('kind')) or 'unknown'} #{selection.get('number')}"
if selection
else "nothing"
)
return RequestCheck(
name=CHECK_NEXT_SAFE_ACTION,
ok=False,
reason_code=REASON_NOT_NEXT_SAFE,
detail=(
f"the allocator would select {chosen} for role "
f"{request.desired_role!r}, not {request.work_kind} "
f"{request.display_ref}. Requests confirm the allocator's "
"decision; they never override it."
),
evidence={
"allocator_outcome": outcome,
"allocator_selection": dict(selection),
"reasons": list(allocation.get("reasons") or ()),
},
)
return RequestCheck(
name=CHECK_NEXT_SAFE_ACTION,
ok=True,
reason_code="next_safe_work",
detail=(
f"the allocator selects {request.work_kind} {request.display_ref} "
f"for role {request.desired_role!r}."
),
evidence={
"allocator_outcome": outcome,
"selected_action": _clean(selection.get("selected_action")),
"expected_role_next": _clean(selection.get("expected_role_next")),
},
)
def _head_pin_check(
request: WorkRequest, allocation: Mapping[str, Any] | None
) -> RequestCheck:
"""PR work must be pinned to a head SHA; issue work has nothing to pin."""
if request.work_kind != KIND_PR:
return RequestCheck(
name=CHECK_HEAD_PIN,
ok=True,
reason_code="head_pin_not_applicable",
detail="issue work carries no head SHA to pin.",
)
selection = (allocation or {}).get("selected") or {}
allocator_head = _clean(selection.get("head_sha")) or None
if not allocator_head:
return RequestCheck(
name=CHECK_HEAD_PIN,
ok=False,
reason_code=REASON_EVIDENCE_UNAVAILABLE,
detail=(
"the allocator reported no head SHA for this PR; PR work "
"cannot be initiated unpinned."
),
)
if request.expected_head_sha and request.expected_head_sha != allocator_head:
return RequestCheck(
name=CHECK_HEAD_PIN,
ok=False,
reason_code="head_moved",
detail=(
"the requested head SHA does not match the PR's current head; "
"re-preview against the live head before initiating."
),
evidence={
"requested_head_sha": request.expected_head_sha,
"current_head_sha": allocator_head,
},
)
return RequestCheck(
name=CHECK_HEAD_PIN,
ok=True,
reason_code="head_pinned",
detail=f"PR {request.display_ref} is pinned at {allocator_head}.",
evidence={"head_sha": allocator_head},
)
def _next_safe_action_text(
request: WorkRequest, checks: Sequence[RequestCheck], authorized: bool
) -> str:
if authorized:
return (
f"Confirm and initiate {request.desired_role} work on "
f"{request.work_kind} {request.display_ref} via the allocator."
)
for check in checks:
if not check.ok:
return f"Resolve {check.name}: {check.detail}"
return "No safe action; the request is not authorized."
def preview_request(
request: WorkRequest,
*,
principal: console_authz.Principal | None = None,
allocator: AllocatorFn | None = None,
claims_source: ClaimsFn | None = None,
correlation_id: str | None = None,
audit: bool = True,
) -> RequestPreview:
"""Build the read-only intent preview for *request*. Never mutates."""
who = principal or console_authz.ANONYMOUS
corr = correlation_id or _correlation_id()
decision = console_authz.authorize(ACTION_ID, who, for_execution=False)
allocation: dict[str, Any] | None = None
claims: Mapping[tuple[str, int], dict[str, Any]] | None = None
checks: list[RequestCheck] = [_authorization_check(decision)]
if decision.allowed:
# An unauthorized principal never reaches the allocator or the
# control-plane DB: a denial must not double as a queue oracle.
allocation = _run_allocator(request, allocator, apply=False)
claims = _load_claims(request, claims_source)
checks.append(_capability_check(request))
checks.append(_lease_check(request, claims))
checks.append(_next_safe_action_check(request, allocation))
checks.append(_head_pin_check(request, allocation))
authorized = all(c.ok for c in checks)
allowed_actions, prohibited_actions = allocator_service.role_actions(
request.desired_role
)
capability = next((c for c in checks if c.name == CHECK_CAPABILITY), None)
evidence = capability.evidence if capability else {}
if authorized:
reason_code = REASON_PREVIEW_OK
detail = (
"Request is authorized. Preview only — nothing has been assigned."
)
else:
first_failure = next(c for c in checks if not c.ok)
reason_code, detail = first_failure.reason_code, first_failure.detail
preview = RequestPreview(
request=request,
authorized=authorized,
reason_code=reason_code,
detail=detail,
authorization=decision.to_dict(),
checks=tuple(checks),
prohibited_actions=tuple(prohibited_actions),
allowed_actions=tuple(allowed_actions),
next_safe_action=_next_safe_action_text(request, checks, authorized),
required_profile=str(evidence.get("required_profile") or ""),
required_namespace=str(evidence.get("required_namespace") or ""),
required_permission=str(evidence.get("required_permission") or ""),
correlation_id=corr,
allocator_evidence=_allocator_evidence(allocation),
)
if audit:
_audit(
request,
result=console_audit.RESULT_PREVIEWED,
decision=decision,
principal=who,
reason_code=reason_code,
detail=detail,
correlation_id=corr,
metadata={
"intent_summary": request.intent_summary,
"desired_role": request.desired_role,
"authorized": authorized,
"failed_checks": [c.name for c in preview.failed_checks],
"phase": "preview",
},
)
return preview
# --- Initiation -------------------------------------------------------------
def apply_request(
request: WorkRequest,
*,
principal: console_authz.Principal | None = None,
confirm: bool = False,
allocator: AllocatorFn | None = None,
claims_source: ClaimsFn | None = None,
correlation_id: str | None = None,
) -> dict[str, Any]:
"""Initiate *request* as an exclusive assignment, or refuse.
The only path to an assignment is the allocator agreeing, on a dry-run,
that this work unit is what the requested role should take next. Every
refusal returns before any mutation is attempted.
"""
who = principal or console_authz.ANONYMOUS
corr = correlation_id or _correlation_id()
execution_decision = console_authz.authorize(ACTION_ID, who, for_execution=True)
authorization = execution_decision.to_dict()
def _refuse(
outcome: str,
reason_code: str,
detail: str,
*,
status: int,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
_audit(
request,
result=console_audit.RESULT_DENIED,
decision=execution_decision,
principal=who,
reason_code=reason_code,
detail=detail,
correlation_id=corr,
metadata={
"intent_summary": request.intent_summary,
"desired_role": request.desired_role,
"phase": "apply",
"outcome": outcome,
},
)
payload: dict[str, Any] = {
"ok": False,
"outcome": outcome,
"reason_code": reason_code,
"detail": detail,
"request": request.to_dict(),
"authorization": authorization,
"assignment": None,
"correlation_id": corr,
"mutation_performed": False,
"status_code": status,
}
payload.update(extra or {})
return payload
if not (execution_decision.allowed and execution_decision.execution_enabled):
return _refuse(
OUTCOME_DENIED,
REASON_UNAUTHORIZED,
execution_decision.detail,
status=403,
)
# Confirmation is a property of the action in the RBAC model, so it is read
# from there rather than assumed here.
action = console_authz.get_action(ACTION_ID)
if action is not None and action.requires_confirmation and not confirm:
return _refuse(
OUTCOME_DENIED,
REASON_CONFIRMATION_REQUIRED,
(
"This action requires explicit confirmation. Re-submit with "
"confirm=true after reviewing the preview."
),
status=409,
)
preview = preview_request(
request,
principal=who,
allocator=allocator,
claims_source=claims_source,
correlation_id=corr,
audit=False,
)
if not preview.authorized:
outcome = (
OUTCOME_BLOCKED
if preview.reason_code == REASON_DUPLICATE_ASSIGNMENT
else OUTCOME_WAIT
)
return _refuse(
outcome,
preview.reason_code,
preview.detail,
status=409,
extra={"preview": preview.to_dict()},
)
fingerprint = (
_clean(preview.allocator_evidence.get("candidate_set_fingerprint")) or None
)
allocation = _run_allocator(
request,
allocator,
apply=True,
expected_candidate_set_fingerprint=fingerprint,
)
if not allocation:
return _refuse(
OUTCOME_WAIT,
REASON_EVIDENCE_UNAVAILABLE,
"the allocator returned no result; nothing was assigned.",
status=503,
)
assignment = allocation.get("assignment") or None
outcome = _clean(allocation.get("outcome"))
assigned = bool(
outcome == allocator_service.OUTCOME_ASSIGNED
and assignment
and _selection_matches(allocation.get("selected"), request)
)
if not assigned:
blocked = outcome in {
allocator_service.OUTCOME_BLOCKED_LEASE,
allocator_service.OUTCOME_BLOCKED_TERMINAL,
allocator_service.OUTCOME_BLOCKED_EXCLUDED_OWN_LEASE,
}
return _refuse(
OUTCOME_BLOCKED if blocked else OUTCOME_WAIT,
REASON_ALLOCATOR_OUTCOME,
(
f"the allocator returned {outcome or 'no outcome'} rather than "
"an assignment for this work unit; nothing was assigned."
),
status=409,
extra={"allocator_evidence": _allocator_evidence(allocation)},
)
_audit(
request,
result=console_audit.RESULT_SUCCEEDED,
decision=execution_decision,
principal=who,
reason_code=REASON_AUTHORIZED,
detail=(
f"assigned {request.work_kind} {request.display_ref} to role "
f"{request.desired_role}."
),
correlation_id=corr,
metadata={
"intent_summary": request.intent_summary,
"desired_role": request.desired_role,
"phase": "apply",
"outcome": OUTCOME_ASSIGNED,
"assignment_id": assignment.get("assignment_id"),
"lease_id": assignment.get("lease_id"),
},
)
return {
"ok": True,
"outcome": OUTCOME_ASSIGNED,
"reason_code": REASON_AUTHORIZED,
"detail": (
"Exclusive assignment created via the allocator. Continue in the "
f"{preview.required_namespace or 'assigned'} MCP namespace."
),
"request": request.to_dict(),
"authorization": authorization,
"assignment": dict(assignment),
"handoff": {
"assignment_id": assignment.get("assignment_id"),
"lease_id": assignment.get("lease_id"),
"session_id": assignment.get("session_id"),
"required_profile": preview.required_profile,
"required_namespace": preview.required_namespace,
"allowed_actions": list(preview.allowed_actions),
"forbidden_actions": list(preview.prohibited_actions),
"expected_head_sha": assignment.get("expected_head_sha"),
},
"correlation_id": corr,
"mutation_performed": True,
"status_code": 201,
"allocator_evidence": _allocator_evidence(allocation),
}
# --- Adapters ---------------------------------------------------------------
def _allocator_evidence(allocation: Mapping[str, Any] | None) -> dict[str, Any]:
"""Reduce an allocator result to the non-secret fields worth surfacing."""
if not allocation:
return {}
return {
"outcome": allocation.get("outcome"),
"selected": allocation.get("selected"),
"reasons": list(allocation.get("reasons") or ()),
"candidate_set_fingerprint": allocation.get("candidate_set_fingerprint"),
"candidate_count": allocation.get("candidate_count"),
"inventory_complete": allocation.get("inventory_complete"),
"selection_policy": allocation.get("selection_policy"),
"substrate": allocation.get("substrate"),
}
def _run_allocator(
request: WorkRequest,
allocator: AllocatorFn | None,
*,
apply: bool,
expected_candidate_set_fingerprint: str | None = None,
) -> dict[str, Any] | None:
fn = allocator or default_allocator
try:
result = fn(
request=request,
apply=apply,
expected_candidate_set_fingerprint=expected_candidate_set_fingerprint,
)
except Exception: # noqa: BLE001 — an allocator failure denies, never proceeds
return None
return result if isinstance(result, dict) else None
def _load_claims(
request: WorkRequest, claims_source: ClaimsFn | None
) -> Mapping[tuple[str, int], dict[str, Any]] | None:
fn = claims_source or default_claims_source
try:
claims = fn(request)
except Exception: # noqa: BLE001 — an unreadable substrate is a denial
return None
return claims if isinstance(claims, Mapping) else None
def default_claims_source(
request: WorkRequest,
) -> Mapping[tuple[str, int], dict[str, Any]]:
"""Live active-claim inventory from the #613 control-plane DB."""
import control_plane_db
db = control_plane_db.ControlPlaneDB()
return db.list_active_claims(
remote=request.remote, org=request.org, repo=request.repo
)
def default_allocator(
*,
request: WorkRequest,
apply: bool,
expected_candidate_set_fingerprint: str | None = None,
) -> dict[str, Any] | None:
"""Run the real allocator over the live queue for *request*'s scope.
An incomplete candidate inventory returns ``None`` rather than a ranking
over a partial set (#758): selecting from a short list can pick the wrong
work unit, so the request denies instead.
"""
import control_plane_db
from webui.queue_loader import load_queue_snapshot
from webui.traffic_loader import candidates_from_queue_snapshot
snapshot = load_queue_snapshot()
if snapshot.fetch_error:
return None
for pagination in (snapshot.pr_pagination, snapshot.issue_pagination):
if pagination is not None and not pagination.inventory_complete:
return None
candidates = candidates_from_queue_snapshot(snapshot)
db = control_plane_db.ControlPlaneDB()
result = allocator_service.allocate_next_work(
db,
session_id=f"webui-request-{uuid.uuid4().hex[:12]}",
role=request.desired_role,
remote=request.remote,
org=request.org,
repo=request.repo,
candidates=candidates,
apply=bool(apply),
allocation_mode="role_scoped",
expected_candidate_set_fingerprint=expected_candidate_set_fingerprint,
)
if isinstance(result, dict):
result.setdefault("candidate_count", len(candidates))
result.setdefault("inventory_complete", True)
result.setdefault("selection_policy", allocator_service.SELECTION_POLICY)
return result
# --- Audit ------------------------------------------------------------------
def _audit(
request: WorkRequest,
*,
result: str,
decision: console_authz.AuthorizationDecision,
principal: console_authz.Principal,
reason_code: str,
detail: str,
correlation_id: str,
metadata: dict[str, Any],
) -> dict[str, Any]:
return console_audit.record_event(
action_id=ACTION_ID,
result=result,
decision=decision,
principal=principal,
target={
"kind": request.work_kind,
"ref": request.display_ref,
"remote": request.remote,
"org": request.org,
"repo": request.repo,
},
reason_code=reason_code,
request_id=correlation_id,
detail=detail,
metadata=metadata,
)