Ported from authority branch agent/g7-security-critical-closure-20260914 (d5f5c9b, base=beabd053).
S1 auth: role/user from server token->identity mapping, ignore client X-AIOS-* headers.
S2 bootstrap credential: PBKDF2 random-salt, fail-closed SECRET, no plaintext fallback, session HMAC+expiry.
S3 path containment + control-plane file-read hardening.
S4 admin UI XSS + real governed approval + dashboard CSRF/branch validation.
S5 runtime delivery: registry.yaml conflict, openapi securityScheme, install.sh fail propagation, do_POST shadowing.
Tests: 5 negative suites (66 tests). Evidence pack in deliverables/g7-evidence/.
66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""Task Center action API model for Web UI buttons."""
|
|
|
|
from typing import Any
|
|
|
|
try:
|
|
from fastapi import APIRouter
|
|
except Exception:
|
|
APIRouter = None
|
|
|
|
from .task_lifecycle import TaskLifecycle
|
|
|
|
|
|
lifecycle = TaskLifecycle()
|
|
|
|
# Role must come from an authenticated principal, never from the client body.
|
|
_ROLE_WHITELIST = {"user", "agent", "executor", "dispatcher", "notifier", "group_admin", "owner"}
|
|
_authenticator = None # callable(payload) -> {"role": ...} | None, injected by host
|
|
|
|
|
|
def set_authenticator(fn):
|
|
"""Install a server-side authenticator used to derive the actor role.
|
|
|
|
`fn` receives the raw request payload and must return either None (or a
|
|
dict without a whitelisted "role") for anonymous, or a dict containing a
|
|
server-verified "role". The client-supplied `payload["role"]` is ignored.
|
|
"""
|
|
global _authenticator
|
|
_authenticator = fn
|
|
|
|
|
|
def effective_role(payload: dict[str, Any]) -> str:
|
|
if _authenticator is not None:
|
|
principal = _authenticator(payload)
|
|
if isinstance(principal, dict) and principal.get("role") in _ROLE_WHITELIST:
|
|
return principal["role"]
|
|
return "user"
|
|
|
|
|
|
def get_task_actions(task: dict[str, Any], role: str) -> dict[str, Any]:
|
|
return {
|
|
"task_id": task.get("task_id"),
|
|
"actions": lifecycle.available_actions(task, role),
|
|
}
|
|
|
|
|
|
def execute_task_action(task: dict[str, Any], action: str, role: str, reason: str | None = None):
|
|
return lifecycle.transition(task, action, role, reason)
|
|
|
|
|
|
def build_router():
|
|
if APIRouter is None:
|
|
return None
|
|
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-actions"])
|
|
|
|
@router.post("/{task_id}/actions/{action}", operation_id="executeTaskAction")
|
|
def action(task_id: str, action: str, payload: dict[str, Any]):
|
|
# Role is derived from the authenticated principal; body role is ignored.
|
|
return execute_task_action(payload.get("task", {}), action, effective_role(payload), payload.get("reason"))
|
|
|
|
return router
|
|
|
|
|
|
router = build_router()
|
|
|
|
__all__ = ["router", "get_task_actions", "execute_task_action"]
|