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]>
116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
"""Navigation IA for the Phase 1 operator console shell (#638).
|
|
|
|
Single source of truth for the console navigation so ``webui/layout.py`` and
|
|
the ``webui/app.py`` route table stay aligned with epic #631. Read-only: every
|
|
destination is a GET view or a Phase 1 placeholder. No mutation links.
|
|
|
|
Nav groups follow the #631 Phase 1 information architecture: Health, Traffic,
|
|
Runtime/Sessions, Projects, Inventory, Timeline, Policy (placeholder), and
|
|
Insights (placeholder). Later-phase surfaces are declared as ``stub`` items and
|
|
backed by ``STUB_PAGES`` so their nav links resolve to a graceful placeholder
|
|
instead of a 404.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NavItem:
|
|
"""A single navigation destination.
|
|
|
|
``status`` is ``"live"`` for implemented views and ``"stub"`` for Phase 1
|
|
placeholders whose backing view lands in a later child issue.
|
|
"""
|
|
|
|
href: str
|
|
label: str
|
|
status: str = "live"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NavGroup:
|
|
label: str
|
|
items: tuple[NavItem, ...]
|
|
|
|
|
|
NAV_GROUPS: tuple[NavGroup, ...] = (
|
|
NavGroup("Health", (
|
|
NavItem("/health", "Liveness"),
|
|
NavItem("/system-health", "System health"),
|
|
)),
|
|
NavGroup("Traffic", (
|
|
NavItem("/traffic", "Traffic control"),
|
|
NavItem("/queue", "Queue"),
|
|
NavItem("/leases", "Leases"),
|
|
NavItem("/actions", "Actions"),
|
|
NavItem("/requests", "Requests"),
|
|
)),
|
|
NavGroup("Runtime/Sessions", (
|
|
NavItem("/runtime", "Runtime health"),
|
|
NavItem("/sessions", "Sessions", "stub"),
|
|
)),
|
|
NavGroup("Projects", (
|
|
NavItem("/projects", "Projects"),
|
|
)),
|
|
NavGroup("Inventory", (
|
|
NavItem("/inventory", "Inventory", "stub"),
|
|
NavItem("/worktrees", "Worktrees"),
|
|
)),
|
|
NavGroup("Timeline", (
|
|
NavItem("/timeline", "Timeline", "stub"),
|
|
)),
|
|
NavGroup("Policy", (
|
|
NavItem("/policy", "Policy", "stub"),
|
|
NavItem("/prompts", "Prompts"),
|
|
)),
|
|
NavGroup("Insights", (
|
|
NavItem("/insights", "Insights", "stub"),
|
|
NavItem("/analytics", "Analytics"),
|
|
NavItem("/audit", "Audit"),
|
|
)),
|
|
)
|
|
|
|
|
|
# Phase 1 placeholder destinations whose backing views land in later child
|
|
# issues of epic #631. Each maps a path to (title, description). Routes are
|
|
# registered so nav links resolve to a graceful, read-only stub page.
|
|
STUB_PAGES: dict[str, tuple[str, str]] = {
|
|
"/sessions": (
|
|
"Sessions",
|
|
"Active session, capability, and role inventory. Backed by the unified "
|
|
"inventory API (#636) once it lands.",
|
|
),
|
|
"/inventory": (
|
|
"Inventory",
|
|
"Unified sessions, leases, locks, namespaces, and worktree inventory. "
|
|
"Backed by the Phase 1 inventory API (#636).",
|
|
),
|
|
"/timeline": (
|
|
"Timeline",
|
|
"Workflow event timeline across issues and PRs. A later Phase 1 surface.",
|
|
),
|
|
"/policy": (
|
|
"Policy",
|
|
"Capability and role policy surface. Placeholder until a later phase.",
|
|
),
|
|
"/insights": (
|
|
"Insights",
|
|
"Aggregate operational insights and trends. Placeholder until a later "
|
|
"phase.",
|
|
),
|
|
}
|
|
|
|
|
|
def iter_nav_items():
|
|
"""Yield every ``NavItem`` across all groups in declared order."""
|
|
for group in NAV_GROUPS:
|
|
for item in group.items:
|
|
yield item
|
|
|
|
|
|
def nav_hrefs() -> tuple[str, ...]:
|
|
"""Return every navigation href in declared order."""
|
|
return tuple(item.href for item in iter_nav_items())
|