Files
turnstone/turnstone/api/console_spec.py
T
Patrick Buckley 480a1426b3 Fail-closed history-commit handoff (#1005)
* fix(session): fail-closed history-commit handoff (#981)

The deleted-workstream discovery is now a terminal, ws_id-keyed latch:
keyed conversation commits refuse admission once the durable parent is
gone (convergence finalizers and force-abandon are exempt), history
handoff refuses to mint a proof token so /history fails closed with a
503 instead of silently wiping the pane, and the SSE stream carries a
workstream_gone resync reason. Discarded commits leave a forensic log
of commit keys and roles, never content.

Conversation rows gain a commit_key (migration 071): keyed saves are
idempotent under retry, validated against the full commit identity, and
refused when they would cross a workstream deletion. The prune orphan
category now requires a NULL alias plus a two-hour updated grace, with
cutoffs computed at discovery time and carried into both dialects'
rechecks.

The mid-turn interjection queue is owner-partitioned with no per-site
mode flags: pops take the acting principal's and unowned rows, other
participants' rows are structurally retained, and enforcement lives at
queue admission plus the shared before_spawn gates. The retraction
ledger is bounded by open pop windows: pops open a window atomically
with the queue delete, restores close their ids atomically with the
ledger consume, every other exit closes through one helper, and misses
for unheld ids record nothing. The workstream-gone latch refuses
unattended wakes at all three gates (watcher spawn, claim, delivery
pre-pop), and the retry dispatcher regained its pre-envelope
cancel/error convergence net.

Persistence-state reporting derives through the session bound to each
UI instead of a registry lookup by id that failed open to healthy
during tombstone retention. The dashboard roster no longer re-inserts
ghost entries from trailing activity events, the history tool-outcome
scan tolerates interleaved non-turn rows, and the shared
handoff-deadline handle owns its own retirement.

Single-sourced across call sites: keyed-commit row values, attachment
save wrappers, tail-truncation and conflict-resolution bodies for both
storage dialects; worker-slot lifecycle field sets; the direct-commit
admission frame; queued-row layout accessors; the string-aware comment
stripper shared by every JS harness suite.

Refs #981 #964

* fix(session): sweep handoff fixes to their sibling surfaces

The interactive replay loop treated a system row as a tool-batch
boundary, so every tool result after an interleaved row vanished from
that pane while the coordinator rendered the same history correctly.
Only a conversational turn ends the batch window now, matching the
shared outcome index.

Accepted user turns clear the composer's attachment chips on the same
viewer policy that settles optimistic bubbles rather than on having
matched a local bubble, so a workstream created with an upload no
longer keeps a chip for an attachment the create dispatch already
consumed. The coordinator's raced-Stop arm emits the stream-end hook it
inherits alongside the idle state, leaving no unfinalized bubble or
unflushed tool output. Ending a session surfaces a failure toast when
the request never lands or answers with a non-JSON body.

The per-second persistence reconcile now probes each session without
blocking: a workstream whose generation and handoff locks are held is
skipped until the next pass instead of contending the locks every
commit needs. The one-shot repair that gates workstream creation at
capacity keeps a definite probe — it has no next pass, and the sessions
likeliest to be contended are the ones whose unresolved journals
emptied its candidate list.

Single-sourced: the attachment lane builds its conversation row through
the shared commit-identity builder; the ordinary worker exit releases
its slot through the lifecycle owner; both operator surfaces snapshot
their counters through one non-consuming helper; the replay preamble
loses its per-kind wrappers and its config hook; the browser harness
suites share one brace walker; and each in-flight history attempt is
one record carrying both its abort controller and its deadline.

Refs #981 #964
2026-08-11 04:18:36 -07:00

1902 lines
62 KiB
Python

"""Console endpoint catalog for OpenAPI spec generation."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pydantic import BaseModel
from turnstone.api.console_schemas import (
AdminMemoryInfo,
AssignRoleRequest,
AuditEventInfo,
AvailableModelInfo,
BulkSetNodeMetadataRequest,
CalibrateModelResponse,
ChannelUserInfo,
ClusterNodesResponse,
ClusterOverviewResponse,
ClusterSnapshotResponse,
ClusterWorkstreamsResponse,
ClusterWsDetailResponse,
ConsoleCreateWsRequest,
ConsoleCreateWsResponse,
ConsoleHealthResponse,
CoordinatorApproveRequest,
CoordinatorChildInfo,
CoordinatorChildrenResponse,
CoordinatorCloseAllChildrenRequest,
CoordinatorCloseAllChildrenResponse,
CoordinatorCreateRequest,
CoordinatorCreateResponse,
CoordinatorOpenResponse,
CoordinatorRestrictRequest,
CoordinatorRestrictResponse,
CoordinatorSendRequest,
CoordinatorSendResponse,
CoordinatorTaskInfo,
CoordinatorTasksResponse,
CoordinatorTrustRequest,
CoordinatorTrustResponse,
CreateChannelUserRequest,
CreateMcpServerRequest,
CreateModelDefinitionRequest,
CreatePersonaRequest,
CreateRoleRequest,
CreateSkillRequest,
CreateSkillResourceRequest,
CreateToolPolicyRequest,
DeleteModelDefinitionResponse,
DetectModelRequest,
DetectModelResponse,
ImportMcpConfigRequest,
ImportMcpConfigResponse,
KnownModelsResponse,
ListAdminMemoriesResponse,
ListAuditEventsResponse,
ListAvailableModelsResponse,
ListChannelUsersResponse,
ListMcpServersResponse,
ListModelDefinitionsResponse,
ListOrgsResponse,
ListOutputAssessmentsResponse,
ListPersonasResponse,
ListRolesResponse,
ListSettingSchemaResponse,
ListSettingsResponse,
ListSkillResourcesResponse,
ListSkillsResponse,
ListSkillVersionsResponse,
ListToolPoliciesResponse,
ListUserRolesResponse,
ListVerdictsResponse,
McpReloadResponse,
McpServerDetail,
ModelAuthConstraintsResponse,
ModelCapabilitiesResponse,
ModelDefinitionInfo,
ModelDefinitionWriteResponse,
ModelReloadResponse,
NodeDetailResponse,
NodeMetadataResponse,
OrgInfo,
OutputAssessmentInfo,
ParseSkillRequest,
ParseSkillResponse,
PersonaInfo,
RegistryInstallRequest,
RegistrySearchResponse,
RoleEffectiveResponse,
RoleInfo,
RoleOverridesRequest,
RouteCreateRequest,
RouteCreateResponse,
RouteLiveResponse,
RouteResponse,
SetNodeMetadataValueRequest,
SettingInfo,
SettingSchemaInfo,
SkillDiscoverResponse,
SkillInfo,
SkillInstallRequest,
SkillInstallResponse,
SkillResourceInfo,
SkillVersionInfo,
ToolPolicyInfo,
UpdateMcpServerRequest,
UpdateModelDefinitionRequest,
UpdateOrgRequest,
UpdatePersonaRequest,
UpdateRoleRequest,
UpdateSettingRequest,
UpdateSkillRequest,
UpdateToolPolicyRequest,
UsageBreakdownItem,
UsageResponse,
UserRoleInfo,
VerdictInfo,
)
from turnstone.api.openapi import EndpointSpec, QueryParam, build_openapi
from turnstone.api.schemas import (
AuthLoginRequest,
AuthLoginResponse,
AuthSetupRequest,
AuthSetupResponse,
AuthStatusResponse,
AuthWhoamiResponse,
CreateScheduleRequest,
CreateTokenRequest,
CreateTokenResponse,
CreateUserRequest,
DeleteSettingResponse,
ErrorResponse,
ListScheduleRunsResponse,
ListSchedulesResponse,
ListTokensResponse,
ListUsersResponse,
ScheduleInfo,
StatusResponse,
UpdateScheduleRequest,
UserInfo,
)
from turnstone.api.server_schemas import (
ApproveRequest,
ApproveResponse,
CancelRequest,
CancelResponse,
CloseWorkstreamRequest,
CommandRequest,
DequeueRequest,
ListAttachmentsResponse,
ListSkillSummaryResponse,
ListWorkstreamsResponse,
RewindRequest,
SendRequest,
SendResponse,
SkillSummary,
UploadAttachmentResponse,
WorkstreamDetailResponse,
WorkstreamHistoryResponse,
)
CONSOLE_ENDPOINTS: list[EndpointSpec] = [
# --- Cluster ---
EndpointSpec(
"/v1/api/cluster/overview",
"GET",
"Cluster state summary",
response_model=ClusterOverviewResponse,
tags=["Cluster"],
),
EndpointSpec(
"/v1/api/cluster/nodes",
"GET",
"Paginated node list",
response_model=ClusterNodesResponse,
query_params=[
QueryParam(
"sort", "Sort field", default="activity", enum=["activity", "tokens", "name"]
),
QueryParam("limit", "Page size", schema_type="integer", default=100),
QueryParam("offset", "Pagination offset", schema_type="integer", default=0),
],
tags=["Cluster"],
),
EndpointSpec(
"/v1/api/cluster/workstreams",
"GET",
"Filtered workstream list",
response_model=ClusterWorkstreamsResponse,
query_params=[
QueryParam(
"state",
"Filter by state",
enum=["running", "thinking", "attention", "idle", "error"],
),
QueryParam("node", "Filter by node_id"),
QueryParam("search", "Search in name/title/node"),
QueryParam("sort", "Sort field", default="state", enum=["state", "tokens", "name"]),
QueryParam("page", "Page number", schema_type="integer", default=1),
QueryParam("per_page", "Items per page (max 200)", schema_type="integer", default=50),
],
tags=["Cluster"],
),
EndpointSpec(
"/v1/api/cluster/node/{node_id}",
"GET",
"Single node detail",
response_model=NodeDetailResponse,
error_codes=[404],
tags=["Cluster"],
),
EndpointSpec(
"/v1/api/cluster/workstreams/new",
"POST",
"Create workstream via HTTP dispatch",
request_model=ConsoleCreateWsRequest,
response_model=ConsoleCreateWsResponse,
error_codes=[400, 404, 503],
tags=["Cluster"],
),
EndpointSpec(
"/v1/api/cluster/snapshot",
"GET",
"Full cluster state snapshot",
description="Returns the complete cluster state: all nodes with their workstreams "
"and overview aggregates. Used for initial load and reconnection.",
response_model=ClusterSnapshotResponse,
tags=["Cluster"],
),
# --- Streaming ---
EndpointSpec(
"/v1/api/cluster/events",
"GET",
"Cluster SSE event stream",
description="Server-Sent Events stream for real-time cluster updates. "
"First event is a 'snapshot' with full cluster state, followed by "
"node_joined, node_lost, cluster_state, ws_created, ws_closed, ws_rename events.",
tags=["Streaming"],
),
# --- Auth ---
EndpointSpec(
"/v1/api/auth/login",
"POST",
"Authenticate with a token",
request_model=AuthLoginRequest,
response_model=AuthLoginResponse,
error_codes=[401],
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/setup",
"POST",
"Create first admin user",
request_model=AuthSetupRequest,
response_model=AuthSetupResponse,
error_codes=[400, 409, 503],
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/status",
"GET",
"Return auth state",
response_model=AuthStatusResponse,
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/logout",
"POST",
"Clear auth cookie",
response_model=StatusResponse,
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/oidc/authorize",
"GET",
"Redirect to OIDC provider for SSO login",
response_code=302,
error_codes=[404, 503],
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/oidc/callback",
"GET",
"OIDC callback — validates code, provisions user, sets JWT cookie, redirects to app",
response_code=302,
tags=["Auth"],
),
EndpointSpec(
"/v1/api/auth/whoami",
"GET",
"Return authenticated user info and permissions",
response_model=AuthWhoamiResponse,
error_codes=[401],
tags=["Auth"],
),
# --- Admin ---
EndpointSpec(
"/v1/api/admin/users",
"GET",
"List all users",
response_model=ListUsersResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users",
"POST",
"Create a user",
request_model=CreateUserRequest,
response_model=UserInfo,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}",
"DELETE",
"Delete a user and their tokens",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/tokens",
"GET",
"List tokens for a user",
response_model=ListTokensResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/tokens",
"POST",
"Create an API token (raw token shown once)",
request_model=CreateTokenRequest,
response_model=CreateTokenResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/tokens/{token_id}",
"DELETE",
"Revoke an API token",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
# --- Channels ---
EndpointSpec(
"/v1/api/admin/users/{user_id}/channels",
"GET",
"List channel links for a user",
response_model=ListChannelUsersResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/channels",
"POST",
"Link a channel account to a user",
request_model=CreateChannelUserRequest,
response_model=ChannelUserInfo,
error_codes=[400, 404, 409],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/channels/{channel_type}/{channel_user_id}",
"DELETE",
"Unlink a channel account",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
# --- OIDC Identities ---
EndpointSpec(
"/v1/api/admin/users/{user_id}/oidc-identities",
"GET",
"List OIDC identities linked to a user",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/oidc-identities",
"DELETE",
"Unlink an OIDC identity (issuer + subject as query params)",
error_codes=[400, 404],
tags=["Admin"],
),
# --- Schedules ---
EndpointSpec(
"/v1/api/admin/schedules",
"GET",
"List all scheduled tasks",
response_model=ListSchedulesResponse,
tags=["Schedules"],
),
EndpointSpec(
"/v1/api/admin/schedules",
"POST",
"Create a scheduled task",
request_model=CreateScheduleRequest,
response_model=ScheduleInfo,
error_codes=[400],
tags=["Schedules"],
),
EndpointSpec(
"/v1/api/admin/schedules/{task_id}",
"GET",
"Get a scheduled task",
response_model=ScheduleInfo,
error_codes=[404],
tags=["Schedules"],
),
EndpointSpec(
"/v1/api/admin/schedules/{task_id}",
"PUT",
"Update a scheduled task",
request_model=UpdateScheduleRequest,
response_model=ScheduleInfo,
error_codes=[400, 404],
tags=["Schedules"],
),
EndpointSpec(
"/v1/api/admin/schedules/{task_id}",
"DELETE",
"Delete a scheduled task",
response_model=StatusResponse,
error_codes=[404],
tags=["Schedules"],
),
EndpointSpec(
"/v1/api/admin/schedules/{task_id}/runs",
"GET",
"List run history for a scheduled task",
response_model=ListScheduleRunsResponse,
query_params=[
QueryParam(
"limit", "Max results (default 50, max 200)", schema_type="integer", default=50
),
],
error_codes=[404],
tags=["Schedules"],
),
# --- Governance: Roles ---
EndpointSpec(
"/v1/api/admin/roles",
"GET",
"List all roles",
response_model=ListRolesResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/roles",
"POST",
"Create a custom role",
request_model=CreateRoleRequest,
response_model=RoleInfo,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/roles/{role_id}",
"PUT",
"Update a role",
request_model=UpdateRoleRequest,
response_model=RoleInfo,
error_codes=[400, 404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/roles/{role_id}",
"DELETE",
"Delete a custom role",
response_model=StatusResponse,
error_codes=[400, 404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/roles/{role_id}/effective",
"GET",
"Get effective permissions for a role (baseline + overrides)",
response_model=RoleEffectiveResponse,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/roles/{role_id}/overrides",
"PUT",
"Replace the grant/revoke override set for a builtin role",
request_model=RoleOverridesRequest,
response_model=RoleEffectiveResponse,
error_codes=[400, 404, 409],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/roles",
"GET",
"List roles assigned to a user",
response_model=ListUserRolesResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/roles",
"POST",
"Assign a role to a user",
request_model=AssignRoleRequest,
response_model=StatusResponse,
error_codes=[400, 404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/users/{user_id}/roles/{role_id}",
"DELETE",
"Unassign a role from a user",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
# --- Governance: Orgs ---
EndpointSpec(
"/v1/api/admin/orgs",
"GET",
"List organizations",
response_model=ListOrgsResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/orgs/{org_id}",
"GET",
"Get organization details",
response_model=OrgInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/orgs/{org_id}",
"PUT",
"Update organization settings",
request_model=UpdateOrgRequest,
response_model=OrgInfo,
error_codes=[404],
tags=["Admin"],
),
# --- Governance: Tool Policies ---
EndpointSpec(
"/v1/api/admin/policies",
"GET",
"List tool policies",
response_model=ListToolPoliciesResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/policies",
"POST",
"Create a tool policy",
request_model=CreateToolPolicyRequest,
response_model=ToolPolicyInfo,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/policies/{policy_id}",
"PUT",
"Update a tool policy",
request_model=UpdateToolPolicyRequest,
response_model=ToolPolicyInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/policies/{policy_id}",
"DELETE",
"Delete a tool policy",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
# --- Governance: Skill Discovery ---
EndpointSpec(
"/v1/api/admin/skills/discover",
"GET",
"Search external skill registries for available skills",
response_model=SkillDiscoverResponse,
query_params=[
QueryParam("q", "Search query"),
QueryParam("limit", "Max results (default 20, max 100)", schema_type="integer"),
],
error_codes=[502],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/install",
"POST",
"Install skill(s) from an external source",
request_model=SkillInstallRequest,
response_model=SkillInstallResponse,
error_codes=[400, 404, 409, 502],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/parse",
"POST",
"Parse a SKILL.md document and return its frontmatter fields and body",
request_model=ParseSkillRequest,
response_model=ParseSkillResponse,
error_codes=[400, 413],
tags=["Admin"],
),
# --- Governance: Skills ---
EndpointSpec(
"/v1/api/admin/skills",
"GET",
"List skills",
response_model=ListSkillsResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills",
"POST",
"Create a skill",
request_model=CreateSkillRequest,
response_model=SkillInfo,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}",
"GET",
"Get a skill by ID",
response_model=SkillInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}",
"PUT",
"Update a skill",
request_model=UpdateSkillRequest,
response_model=SkillInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}",
"DELETE",
"Delete a skill",
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/versions",
"GET",
"List version history for a skill",
response_model=ListSkillVersionsResponse,
tags=["Admin"],
),
# --- Models ---
EndpointSpec(
"/v1/api/models",
"GET",
"List enabled model aliases for workstream creation",
response_model=ListAvailableModelsResponse,
tags=["Models"],
),
# --- Skills ---
EndpointSpec(
"/v1/api/skills",
"GET",
"List available skills (summary)",
response_model=ListSkillSummaryResponse,
tags=["Skills"],
),
# --- Governance: Usage & Audit ---
EndpointSpec(
"/v1/api/admin/usage",
"GET",
"Aggregated usage data",
response_model=UsageResponse,
query_params=[
QueryParam("since", "Start timestamp (ISO8601, defaults to last 7 days)"),
QueryParam("until", "End timestamp (ISO8601)"),
QueryParam("user_id", "Filter by user"),
QueryParam("model", "Filter by model"),
QueryParam(
"group_by",
"Group results",
enum=["day", "hour", "model", "user"],
),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/audit",
"GET",
"Paginated audit events",
response_model=ListAuditEventsResponse,
query_params=[
QueryParam("action", "Filter by action type"),
QueryParam("user_id", "Filter by user"),
QueryParam("since", "Start timestamp (ISO8601)"),
QueryParam("until", "End timestamp (ISO8601)"),
QueryParam("limit", "Page size", schema_type="integer", default=50),
QueryParam("offset", "Pagination offset", schema_type="integer", default=0),
],
tags=["Admin"],
),
# --- Governance: Intent Verdicts ---
EndpointSpec(
"/v1/api/admin/verdicts",
"GET",
"Paginated intent verdicts",
response_model=ListVerdictsResponse,
query_params=[
QueryParam("ws_id", "Filter by workstream"),
QueryParam("since", "Start timestamp (ISO8601)"),
QueryParam("until", "End timestamp (ISO8601)"),
QueryParam(
"risk_level",
"Filter by risk level",
enum=["low", "medium", "high", "critical"],
),
QueryParam("limit", "Page size (max 500)", schema_type="integer", default=100),
QueryParam("offset", "Pagination offset", schema_type="integer", default=0),
],
tags=["Admin"],
),
# --- Admin: Output Guard ---
EndpointSpec(
"/v1/api/admin/output-assessments",
"GET",
"Paginated output guard assessments",
response_model=ListOutputAssessmentsResponse,
query_params=[
QueryParam("ws_id", "Filter by workstream"),
QueryParam("risk_level", "Filter by risk level", enum=["low", "medium", "high"]),
QueryParam("since", "Start timestamp (ISO8601)"),
QueryParam("until", "End timestamp (ISO8601)"),
QueryParam("limit", "Page size (max 500)", schema_type="integer", default=100),
QueryParam("offset", "Pagination offset", schema_type="integer", default=0),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/rescan",
"POST",
"Re-scan a skill for security signals",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/unlock",
"POST",
"Unlock a readonly (installed) skill so spec fields and resources can be edited",
error_codes=[400, 404],
tags=["Admin"],
),
# --- Governance: Skill Resources ---
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/resources",
"GET",
"List resource files for a skill",
response_model=ListSkillResourcesResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/resources",
"POST",
"Upload a resource file to a skill",
request_model=CreateSkillResourceRequest,
response_model=SkillResourceInfo,
response_code=201,
error_codes=[400, 404, 409],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/resources/{path}",
"GET",
"Get a single skill resource by path",
response_model=SkillResourceInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/skills/{skill_id}/resources/{path}",
"DELETE",
"Delete a skill resource by path",
error_codes=[404],
tags=["Admin"],
),
# --- Admin: Memories ---
EndpointSpec(
"/v1/api/admin/memories",
"GET",
"List structured memories",
response_model=ListAdminMemoriesResponse,
query_params=[
QueryParam("type", "Filter by memory type"),
QueryParam("scope", "Filter by scope"),
QueryParam("scope_id", "Filter by scope identifier"),
QueryParam("limit", "Page size (max 200)", schema_type="integer", default=100),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/memories/search",
"GET",
"Search memories by query",
response_model=ListAdminMemoriesResponse,
query_params=[
QueryParam("q", "Search query", required=True),
QueryParam("type", "Filter by memory type"),
QueryParam("scope", "Filter by scope"),
QueryParam("scope_id", "Filter by scope identifier"),
QueryParam("limit", "Max results", schema_type="integer", default=20),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/memories/{memory_id}",
"GET",
"Get a single memory by ID",
response_model=AdminMemoryInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/memories/{memory_id}",
"DELETE",
"Delete a memory by ID",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
# --- Admin: System Settings ---
EndpointSpec(
"/v1/api/admin/settings",
"GET",
"List all settings with effective values",
response_model=ListSettingsResponse,
query_params=[
QueryParam("reveal", "Show secret values in plaintext", schema_type="boolean"),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/settings/schema",
"GET",
"Return the full settings registry schema",
response_model=ListSettingSchemaResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/settings/{key}",
"PUT",
"Set a configuration setting value",
request_model=UpdateSettingRequest,
response_model=SettingInfo,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/settings/{key}",
"DELETE",
"Reset a setting to its default value",
response_model=DeleteSettingResponse,
query_params=[
QueryParam("node_id", "Node ID for node-scoped settings"),
],
error_codes=[400, 404],
tags=["Admin"],
),
# --- Admin: MCP Registry ---
EndpointSpec(
"/v1/api/admin/mcp-registry/search",
"GET",
"Search the MCP Registry for available servers",
response_model=RegistrySearchResponse,
query_params=[
QueryParam("search", "Search query"),
QueryParam("limit", "Max results (default 20, max 100)", schema_type="integer"),
QueryParam("cursor", "Pagination cursor for next page"),
],
error_codes=[502],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-registry/install",
"POST",
"Install an MCP server from the registry",
request_model=RegistryInstallRequest,
response_model=McpServerDetail,
error_codes=[400, 404, 409, 502],
tags=["Admin"],
),
# --- Admin: MCP Servers ---
EndpointSpec(
"/v1/api/admin/mcp-servers",
"GET",
"List MCP server definitions with live status",
response_model=ListMcpServersResponse,
query_params=[
QueryParam("reveal", "Show secret env/header values", schema_type="boolean"),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers",
"POST",
"Create an MCP server definition",
request_model=CreateMcpServerRequest,
response_model=McpServerDetail,
error_codes=[400, 409],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers/{server_id}",
"GET",
"Get a single MCP server with status",
response_model=McpServerDetail,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers/{server_id}",
"PUT",
"Update an MCP server definition",
request_model=UpdateMcpServerRequest,
response_model=McpServerDetail,
error_codes=[400, 404, 409],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers/{server_id}",
"DELETE",
"Delete an MCP server definition",
response_model=StatusResponse,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers/reload",
"POST",
"Tell all nodes to re-read MCP server config from DB and reconcile",
response_model=McpReloadResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/mcp-servers/import",
"POST",
"Import MCP servers from a JSON config file",
request_model=ImportMcpConfigRequest,
response_model=ImportMcpConfigResponse,
error_codes=[400],
tags=["Admin"],
),
# --- Admin: Model Definitions ---
EndpointSpec(
"/v1/api/admin/model-definitions",
"GET",
"List model definitions with live status from cluster nodes",
response_model=ListModelDefinitionsResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/auth-constraints",
"GET",
"Dynamic-auth affordance data for the model editor (requires admin.mcp)",
response_model=ModelAuthConstraintsResponse,
error_codes=[403],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions",
"POST",
"Create a model definition",
request_model=CreateModelDefinitionRequest,
response_model=ModelDefinitionWriteResponse,
# 403: dynamic-auth writes escalate to admin.mcp (no service bypass),
# same as the auth-constraints sibling above.
error_codes=[400, 403, 409, 503],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/reload",
"POST",
"Tell all nodes to re-read model definitions from DB and rebuild registry",
response_model=ModelReloadResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/{definition_id}",
"GET",
"Get a single model definition",
response_model=ModelDefinitionInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/{definition_id}",
"PUT",
"Update a model definition",
request_model=UpdateModelDefinitionRequest,
response_model=ModelDefinitionWriteResponse,
# 403: auth-relevant edits escalate to admin.mcp (no service bypass),
# same as the auth-constraints sibling above.
error_codes=[400, 403, 404, 409, 503],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/{definition_id}",
"DELETE",
"Delete a model definition",
response_model=DeleteModelDefinitionResponse,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/detect",
"POST",
"Probe a model endpoint: verify reachability, list models, detect context window and server type",
request_model=DetectModelRequest,
response_model=DetectModelResponse,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-definitions/{definition_id}/calibrate",
"POST",
"Calibrate a reranker model definition and persist its per-model floor",
response_model=CalibrateModelResponse,
# 409: the conditional persist yielded to sustained concurrent
# capabilities writes (bounded retries exhausted).
# 500: the calibration merge is verified against its confinement
# contract before the write; a violation refuses the persist.
error_codes=[404, 409, 500],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-capabilities",
"GET",
"Look up static capabilities for a known model",
response_model=ModelCapabilitiesResponse,
query_params=[
QueryParam(name="provider", description="Provider name", required=True),
QueryParam(name="model", description="Model ID to look up", required=True),
],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/model-capabilities/known",
"GET",
"List known model name prefixes for a provider",
response_model=KnownModelsResponse,
query_params=[
QueryParam(name="provider", description="Provider name", required=True),
],
tags=["Admin"],
),
# --- Admin: Prompt Policies ---
EndpointSpec(
"/v1/api/admin/prompt-policies",
"GET",
"List all prompt policies for system message composition",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/prompt-policies",
"POST",
"Create a prompt policy",
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/prompt-policies/{policy_id}",
"GET",
"Get a single prompt policy",
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/prompt-policies/{policy_id}",
"PUT",
"Update a prompt policy",
error_codes=[400, 404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/prompt-policies/{policy_id}",
"DELETE",
"Delete a prompt policy",
error_codes=[404],
tags=["Admin"],
),
# --- Admin: Personas (no DELETE — archive via PATCH enabled=false) ---
EndpointSpec(
"/v1/api/admin/personas",
"GET",
"List all personas, archived included",
response_model=ListPersonasResponse,
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/personas",
"POST",
"Create a persona",
request_model=CreatePersonaRequest,
response_model=PersonaInfo,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/personas/{persona_id}",
"GET",
"Get a single persona",
response_model=PersonaInfo,
error_codes=[404],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/personas/{persona_id}",
"PATCH",
"Update a persona (edit levers, archive/unarchive, flip default)",
request_model=UpdatePersonaRequest,
response_model=PersonaInfo,
error_codes=[400, 404],
tags=["Admin"],
),
# --- Admin: Node metadata ---
EndpointSpec(
"/v1/api/admin/node-metadata",
"GET",
"Get metadata for all nodes (bulk)",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/nodes/{node_id}/metadata",
"GET",
"Get all metadata for a node",
response_model=NodeMetadataResponse,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/nodes/{node_id}/metadata",
"PUT",
"Bulk set user metadata for a node",
request_model=BulkSetNodeMetadataRequest,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/nodes/{node_id}/metadata/{key}",
"PUT",
"Set a single metadata key for a node",
request_model=SetNodeMetadataValueRequest,
error_codes=[400],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/nodes/{node_id}/metadata/{key}",
"DELETE",
"Delete a single metadata key for a node",
error_codes=[400, 404],
tags=["Admin"],
),
# --- Admin: TLS / ACME ---
EndpointSpec(
"/v1/api/admin/tls/ca",
"GET",
"CA status: initialization state, CN, cert count, cert inventory",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/tls/ca.pem",
"GET",
"Download CA root certificate (PEM format)",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/tls/certs",
"GET",
"List all issued TLS certificates",
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/tls/certs/{domain}/renew",
"POST",
"Force-renew a certificate by domain",
error_codes=[404, 500],
tags=["Admin"],
),
EndpointSpec(
"/v1/api/admin/tls/certs/{domain}",
"DELETE",
"Delete a certificate by domain",
error_codes=[404],
tags=["Admin"],
),
# --- Routing ---
EndpointSpec(
"/v1/api/route/workstreams/new",
"POST",
"Create workstream via rendezvous routing proxy",
description=(
"The documented JSON form accepts RouteCreateRequest. The endpoint also "
"accepts multipart/form-data with a JSON `meta` field and file parts; "
"multipart callers must supply `ws_id` as a query parameter; the console "
"requires the cached `meta.ws_id` to match before forwarding the original "
"body. A JSON body may instead carry "
"an explicit `ws_id`; the console preserves it and uses it as the "
"rendezvous placement key. `resume_ws` accepts an id or saved alias and "
"is resolved to the canonical source id before an atomic fork is routed."
),
request_model=RouteCreateRequest,
response_model=RouteCreateResponse,
error_codes=[400, 403, 404, 409, 413, 429, 500, 502, 503],
query_params=[
QueryParam(
"ws_id",
(
"32-hex rendezvous key required for multipart creates. JSON "
"callers put an optional destination ws_id in the request body."
),
)
],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/live",
"GET",
"Probe whether a routed workstream is loaded without rehydrating it",
description=(
"Routes to the workstream's rendezvous owner and checks its "
"manager-authoritative active list. The response does not expose "
"workstream metadata; missing, unloaded, creating, and "
"caller-invisible rows all report ``live=false``. Routing and "
"upstream uncertainty fail with an error rather than reporting a "
"false miss."
),
response_model=RouteLiveResponse,
error_codes=[400, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/send",
"POST",
"Proxy send to routed node",
request_model=SendRequest,
response_model=SendResponse,
error_codes=[400, 404, 409, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/send",
"DELETE",
"Proxy queued-message cancellation to routed node",
request_model=DequeueRequest,
response_model=StatusResponse,
error_codes=[400, 404, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/approve",
"POST",
"Proxy approve to routed node",
request_model=ApproveRequest,
response_model=ApproveResponse,
error_codes=[400, 403, 404, 409, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/cancel",
"POST",
"Proxy cancel to routed node",
request_model=CancelRequest,
response_model=CancelResponse,
error_codes=[400, 404, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/command",
"POST",
"Proxy command to routed node",
request_model=CommandRequest,
response_model=StatusResponse,
error_codes=[400, 404, 409, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/close",
"POST",
"Proxy workstream close to routed node",
request_model=CloseWorkstreamRequest,
response_model=StatusResponse,
error_codes=[400, 403, 404, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/rewind",
"POST",
"Proxy conversation rewind to routed node",
request_model=RewindRequest,
response_model=StatusResponse,
error_codes=[400, 404, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route/workstreams/{ws_id}/retry",
"POST",
"Proxy last-turn retry to routed node",
response_model=StatusResponse,
error_codes=[400, 404, 502, 503],
tags=["Routing"],
),
EndpointSpec(
"/v1/api/route",
"GET",
"Look up which node owns a workstream",
response_model=RouteResponse,
query_params=[
QueryParam("ws_id", "Workstream ID to look up", required=True),
],
error_codes=[400, 503],
tags=["Routing"],
),
# --- Coordinator workstream API ---
# All require the ``admin.coordinator`` permission. Ownership is
# enforced per-row (callers without ``admin.system`` see only their
# own coordinators); cross-tenant misses 404-mask.
EndpointSpec(
"/v1/api/workstreams/new",
"POST",
"Create a new coordinator workstream",
description=(
'Allocates a console-hosted ``kind="coordinator"`` ChatSession. '
"200 on create; 429 when the ``coordinator.max_active`` cap is "
"reached and no idle coordinator can be evicted. "
"Pre-1.5.0 this returned 201; the lifted ``create`` factory "
"(Stage 2 verb lift) converges on 200 across both kinds."
),
request_model=CoordinatorCreateRequest,
response_model=CoordinatorCreateResponse,
response_code=200,
error_codes=[400, 401, 403, 429, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams",
"GET",
"List coordinator workstreams visible to the caller",
description=(
"Returns coordinators owned by the caller. Callers with "
"``admin.system`` see every coordinator across tenants."
),
response_model=ListWorkstreamsResponse,
error_codes=[403, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}",
"GET",
"Get coordinator detail (rehydrates lazily on miss)",
description=(
"Returns the persisted coordinator's display fields. If the "
"session isn't currently in memory the manager rehydrates it "
"before responding; ``500`` on rehydrate failure carries a "
"correlation id matching the server log line."
),
response_model=WorkstreamDetailResponse,
error_codes=[400, 403, 404, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/open",
"POST",
"Open (rehydrate) a coordinator workstream by ws_id",
description=(
"Parity with ``POST /v1/api/workstreams/{ws_id}/open`` — gives "
"SDK callers and operators a way to warm a coordinator without "
"browsing to it. Idempotent: ``already_loaded=true`` when the "
"session was already in memory."
),
response_model=CoordinatorOpenResponse,
error_codes=[400, 403, 404, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/send",
"DELETE",
"Cancel a queued coordinator message",
description=(
"Removes a previously-queued message identified by ``msg_id`` "
"from the coordinator session's pending queue. Returns "
"``status: removed`` when the queue had the entry, "
"``status: not_found`` otherwise. Reservations attached to "
"the dequeued message are released so the attachments can be "
"reused — parity with the interactive surface."
),
request_model=DequeueRequest,
response_model=StatusResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/send",
"POST",
"Queue a user message onto the coordinator session",
description=(
"Worker thread picks up the message via the session's queue. "
"Optional ``attachment_ids`` select staged uploads to attach to "
"the message (parity with the interactive surface). "
"Response carries ``attached_ids`` / ``dropped_attachment_ids`` "
"so callers can detect partial attaches and ``priority`` / "
"``msg_id`` on the queued path. "
"``status: queue_full`` when the worker queue is full — caller "
"should back off."
),
request_model=CoordinatorSendRequest,
response_model=CoordinatorSendResponse,
error_codes=[400, 403, 404, 409, 500, 503],
tags=["Coordinator"],
),
# --- Coordinator attachments (P1.5: parity with interactive) ---
EndpointSpec(
"/v1/api/workstreams/{ws_id}/attachments",
"POST",
"Upload a file attachment to a coordinator workstream",
description=(
"Multipart upload (field ``file``). Same validation rules as "
"the interactive surface: magic-byte image sniff, UTF-8 text "
"decode, per-kind size cap, per-(ws,user) pending cap. "
"Attachments stay pending until a subsequent ``/send`` "
"attaches them to a message."
),
response_model=UploadAttachmentResponse,
error_codes=[400, 403, 404, 409, 413, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/attachments",
"GET",
"List the caller's pending coordinator attachments",
response_model=ListAttachmentsResponse,
error_codes=[403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/attachments/{attachment_id}/content",
"GET",
"Return raw bytes of a coordinator attachment",
description=(
"Same byte-stream + headers as the interactive surface. Text "
"kinds are forced to ``text/plain`` so an HTML-shaped text "
"upload can't render same-origin."
),
error_codes=[403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/attachments/{attachment_id}",
"DELETE",
"Remove a pending coordinator attachment",
description="Consumed attachments return 404.",
response_model=StatusResponse,
error_codes=[403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/approve",
"POST",
"Resolve a pending tool approval on the coordinator session",
description=(
"Approves or denies the pending tool call(s). Set ``always`` to "
"True to also add the pending tool name(s) to the session's "
"auto-approve set so subsequent calls of the same tool skip the "
"prompt."
),
request_model=CoordinatorApproveRequest,
response_model=ApproveResponse,
error_codes=[400, 403, 404, 409, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/cancel",
"POST",
"Cancel in-flight generation on the coordinator session",
description=(
"Cooperatively stops the active generation and resolves every "
"pending approval cycle. Set ``force=true`` to retire a stuck "
"worker immediately. The response includes a redacted snapshot "
"of work dropped by the cancellation."
),
request_model=CancelRequest,
request_required=False,
response_model=CancelResponse,
error_codes=[403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/rewind",
"POST",
"Drop the last N conversation turns on the coordinator (emits clear_ui)",
description=(
"Claims the coordinator mutation slot, durably truncates N turns, "
"and emits ``clear_ui`` so the dashboard re-fetches the truncated "
"history. Concurrent sends are ordered after the cut; storage failure "
"returns 503 without changing live history. Gated on ``admin.coordinator``."
),
request_model=RewindRequest,
response_model=StatusResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/retry",
"POST",
"Re-send the last user message on the coordinator for a fresh response",
description=(
"Uses one shared worker claim to drop the last response and start the "
"replacement generation, emitting ``clear_ui``. Another send cannot "
"enter between those operations. Gated on ``admin.coordinator``."
),
response_model=StatusResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/close",
"POST",
"Soft-close the coordinator (unload from memory; storage preserved)",
description=(
"Releases the worker thread + UI listeners and marks the row "
"``state=closed`` in storage. The row remains queryable (audit "
"/ history) but cannot be reopened — a closed coordinator is "
"terminal from the manager's perspective. Returns 409 while an "
"accepted live conversation row still requires persistence reconciliation; "
"the coordinator remains loaded and its history journal is retained."
),
response_model=StatusResponse,
error_codes=[403, 404, 409, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/events",
"GET",
"Subscribe to the coordinator's SSE event stream",
description=(
"Server-Sent Events stream carrying ``status``, ``message``, "
"``tool_call``, ``tool_result``, ``approval``, ``error``, and "
"the phase-3 ``child_ws_*`` fan-out events. After rendering REST history, "
"pass its opaque handoff_token once as ?history_token=; it names the exact "
"accepted conversation-row prefix used for that render. history_resync "
"closes this stream and requires a fresh history read; numeric replay is "
"not a substitute. Native Last-Event-ID reconnects take priority. "
"Pass ?user_turn=1 to receive typed accepted-user events; without it, "
"those rows use the backward-compatible strong-repair projection. "
"Pass ?tool_turn=1 to receive final accepted tool rows as typed "
"tool_result events with accepted=true; without it, accepted tool rows "
"use the same pre-row strong-repair projection. "
"Pings every 5s. "
"Body is text/event-stream — the response schema is omitted "
"from the catalog because OpenAPI 3.1 has no first-class SSE "
"type."
),
query_params=[
QueryParam(
"last_event_id",
"Numeric per-workstream event cursor for manual reconnects.",
schema_type="integer",
),
QueryParam(
"history_token",
"Opaque one-shot token naming the accepted prefix rendered from REST history.",
),
QueryParam(
"user_turn",
"Set to 1 to receive typed user_turn events instead of history-repair frames.",
schema_type="integer",
),
QueryParam(
"tool_turn",
"Set to 1 to receive final accepted tool_result projections.",
schema_type="integer",
),
],
error_codes=[403, 404, 409, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/history",
"GET",
"Read the coordinator's reconstructed message history",
description=(
"Returns the tail of the conversation in OpenAI-like message "
"format. Used by the page-load handshake; SSE handles updates "
"after that. Cold coordinators are rehydrated before history is served, "
"so every successful response participates in the REST-to-SSE handoff. "
"Messages are the requested tail "
"of one authoritative total accepted conversation-row prefix: user, "
"assistant, tool, and system rows, including projected compaction "
"checkpoints and "
"cancellation markers. The opaque handoff_token names the exact prefix "
"used for the render and is passed once on initial SSE registration. "
"Admission of a later row changes the token; durable acknowledgement does "
"not. If the durable prefix cannot be loaded, the endpoint returns 503 "
"with `History temporarily unavailable`; that response is not authoritative "
"and supplies no usable handoff token. Bounded by the ``limit`` query "
"parameter."
),
response_model=WorkstreamHistoryResponse,
query_params=[
QueryParam(
"limit",
"Max conversation rows to fetch from storage (default 100, max 500).",
schema_type="integer",
default=100,
),
],
error_codes=[400, 403, 404, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/export",
"GET",
"Export the coordinator's conversation as OpenAI messages JSON",
description=(
"Returns the coordinator's own conversation as an "
'``{"messages": [...]}`` OpenAI Chat Completions envelope, '
"served as a ``<ws_id>.json`` file download. Conversation-only "
"(children are not bundled over HTTP). Gated on "
"``admin.coordinator``."
),
error_codes=[400, 403, 404, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/children",
"GET",
"List the coordinator's spawned child workstreams",
description=(
"Returns interactive child workstreams whose ``parent_ws_id`` "
"is this coordinator. Same row shape as the model-facing "
"``list_workstreams`` tool so the tree UI and the tool agree."
),
response_model=CoordinatorChildrenResponse,
error_codes=[400, 403, 404, 500, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/tasks",
"GET",
"Read the coordinator's task list envelope",
description=(
"Returns the ``{version, tasks}`` envelope persisted via the "
"``tasks`` model tool. Corrupt envelopes return an empty "
"list (the tool itself surfaces corruption errors on mutation)."
),
response_model=CoordinatorTasksResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/trust",
"POST",
"Toggle trusted-session mode for send_to_workstream",
description=(
"When enabled, ``send_to_workstream`` calls that target a "
"ws_id in the coordinator's own subtree skip the approval "
"prompt. Foreign ws_ids continue to require approval — "
"trust only relaxes the guard for work the orchestrator "
"itself spawned. Every auto-approved send still emits a "
"``coordinator.send.auto_approved`` audit row so the trail "
"isn't lost. Gated on both ``admin.coordinator`` AND "
"``coordinator.trust.send`` so the trust feature is an "
"explicit opt-in capability separate from ordinary "
"coordinator administration."
),
request_model=CoordinatorTrustRequest,
response_model=CoordinatorTrustResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/restrict",
"POST",
"Revoke tool access on a live coordinator session",
description=(
"Adds the named tools to the coordinator session's revoked set "
"without closing the session. The model can keep working on "
"whatever is already in flight but cannot invoke the revoked "
"tools again. Idempotent and additive — calling twice with "
"disjoint lists unions them. Revocations do not survive a "
"session close / reopen; operators opt in per session. Writes "
"``coordinator.restricted`` with the revocation delta and the "
"full post-state."
),
request_model=CoordinatorRestrictRequest,
response_model=CoordinatorRestrictResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/workstreams/{ws_id}/close_all_children",
"POST",
"Soft-close every direct child of the coordinator",
description=(
"Reads the in-memory child registry and dispatches "
"``close_workstream`` via the routing proxy for every direct "
"child under a bounded (16-concurrency) semaphore. Soft-close "
"only — it does not touch grandchildren (the model-facing tool "
"asks for a bounded teardown of its own direct fan-out). "
"Returns ``{closed, failed, skipped}`` where "
"``skipped`` distinguishes already-gone (404) from dispatch-"
"broken (``failed``). The optional ``reason`` propagates to "
"every closed child's audit + workstream_config. Writes "
"``coordinator.closed_all_children`` at the coord level."
),
request_model=CoordinatorCloseAllChildrenRequest,
response_model=CoordinatorCloseAllChildrenResponse,
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
EndpointSpec(
"/v1/api/cluster/ws/{ws_id}/detail",
"GET",
"Cluster-wide live workstream detail (storage + live block + tail)",
description=(
"Aggregates the persisted row, a best-effort live block from "
"the owning node (or the in-process coordinator manager for "
'``kind="coordinator"`` rows), and the tail of the message '
"history. Gated on the ``admin.cluster.inspect`` permission "
"(granted to ``builtin-admin`` via migration 040; revoke or "
"reassign to a custom role for tighter control). A workstream "
"attached to a *private* project stays confidential to its "
"members: a permitted caller who isn't its owner / creator / "
"project member gets a 404 (same masking as an unknown id). "
"``live`` is null on node unreachability / 5xx so callers can "
"degrade gracefully."
),
response_model=ClusterWsDetailResponse,
query_params=[
QueryParam(
"message_limit",
"Max conversation rows in the tail (default 20, clamped to 200).",
schema_type="integer",
default=20,
),
],
error_codes=[400, 403, 404, 503],
tags=["Coordinator"],
),
# --- Observability ---
EndpointSpec(
"/health",
"GET",
"Console health check",
response_model=ConsoleHealthResponse,
tags=["Observability"],
),
]
_ALL_MODELS: list[type[BaseModel]] = [
ErrorResponse,
StatusResponse,
DeleteSettingResponse,
AuthLoginRequest,
AuthLoginResponse,
AuthSetupRequest,
AuthSetupResponse,
AuthStatusResponse,
CreateUserRequest,
UserInfo,
ListUsersResponse,
CreateTokenRequest,
CreateTokenResponse,
ListTokensResponse,
ChannelUserInfo,
CreateChannelUserRequest,
ListChannelUsersResponse,
ClusterOverviewResponse,
ClusterNodesResponse,
ClusterWorkstreamsResponse,
NodeDetailResponse,
ClusterSnapshotResponse,
ClusterWsDetailResponse,
ConsoleCreateWsRequest,
ConsoleCreateWsResponse,
ConsoleHealthResponse,
CoordinatorApproveRequest,
ApproveRequest,
ApproveResponse,
CancelRequest,
CancelResponse,
CloseWorkstreamRequest,
CommandRequest,
DequeueRequest,
SendRequest,
SendResponse,
CoordinatorChildInfo,
CoordinatorChildrenResponse,
CoordinatorCloseAllChildrenRequest,
CoordinatorCloseAllChildrenResponse,
CoordinatorCreateRequest,
CoordinatorCreateResponse,
CoordinatorOpenResponse,
CoordinatorRestrictRequest,
CoordinatorRestrictResponse,
CoordinatorSendRequest,
CoordinatorSendResponse,
CoordinatorTaskInfo,
CoordinatorTasksResponse,
CoordinatorTrustRequest,
CoordinatorTrustResponse,
CreateScheduleRequest,
UpdateScheduleRequest,
ScheduleInfo,
ListSchedulesResponse,
ListScheduleRunsResponse,
RoleInfo,
CreateRoleRequest,
UpdateRoleRequest,
ListRolesResponse,
AssignRoleRequest,
UserRoleInfo,
ListUserRolesResponse,
OrgInfo,
UpdateOrgRequest,
ListOrgsResponse,
ToolPolicyInfo,
CreateToolPolicyRequest,
UpdateToolPolicyRequest,
ListToolPoliciesResponse,
UsageBreakdownItem,
UsageResponse,
AuditEventInfo,
ListAuditEventsResponse,
VerdictInfo,
ListVerdictsResponse,
OutputAssessmentInfo,
ListOutputAssessmentsResponse,
AdminMemoryInfo,
ListAdminMemoriesResponse,
SettingInfo,
ListSettingsResponse,
SettingSchemaInfo,
ListSettingSchemaResponse,
UpdateSettingRequest,
McpServerDetail,
CreateMcpServerRequest,
UpdateMcpServerRequest,
ListMcpServersResponse,
ImportMcpConfigRequest,
ImportMcpConfigResponse,
McpReloadResponse,
ModelDefinitionInfo,
ModelDefinitionWriteResponse,
CreateModelDefinitionRequest,
UpdateModelDefinitionRequest,
ListModelDefinitionsResponse,
ModelAuthConstraintsResponse,
PersonaInfo,
CreatePersonaRequest,
UpdatePersonaRequest,
ListPersonasResponse,
ModelReloadResponse,
DeleteModelDefinitionResponse,
DetectModelRequest,
DetectModelResponse,
CalibrateModelResponse,
ModelCapabilitiesResponse,
KnownModelsResponse,
AvailableModelInfo,
ListAvailableModelsResponse,
RegistrySearchResponse,
RegistryInstallRequest,
SkillDiscoverResponse,
SkillInstallRequest,
SkillInstallResponse,
SkillInfo,
SkillVersionInfo,
ParseSkillRequest,
ParseSkillResponse,
CreateSkillRequest,
UpdateSkillRequest,
ListSkillsResponse,
ListSkillVersionsResponse,
SkillResourceInfo,
CreateSkillResourceRequest,
ListSkillResourcesResponse,
RouteResponse,
RouteLiveResponse,
RouteCreateRequest,
RouteCreateResponse,
SkillSummary,
ListSkillSummaryResponse,
WorkstreamDetailResponse,
WorkstreamHistoryResponse,
]
def build_console_spec() -> dict[str, Any]:
"""Build the OpenAPI spec for the turnstone console."""
return build_openapi(
title="turnstone Console API",
description="Cluster-wide visibility and control across all turnstone nodes.",
endpoints=CONSOLE_ENDPOINTS,
models=_ALL_MODELS,
)