diff --git a/.env.example b/.env.example index 9da5f96..23e8941 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ +# === Server Port === +# Port the Next.js server listens on (dev and production) +# PORT=3000 + # === Authentication === # Admin user seeded on first run (only if no users exist in DB) AUTH_USER=admin @@ -65,7 +69,7 @@ NEXT_PUBLIC_GATEWAY_PROTOCOL= NEXT_PUBLIC_GATEWAY_URL= # NEXT_PUBLIC_GATEWAY_TOKEN= # Optional, set if gateway requires auth token # Gateway client id used in websocket handshake (role=operator UI client). -NEXT_PUBLIC_GATEWAY_CLIENT_ID=control-ui +NEXT_PUBLIC_GATEWAY_CLIENT_ID=openclaw-control-ui # === Data Paths (all optional, defaults to .data/ in project root) === # MISSION_CONTROL_DATA_DIR=.data diff --git a/Dockerfile b/Dockerfile index b8915af..792ef4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,9 +32,9 @@ COPY --from=build /app/src/lib/schema.sql ./src/lib/schema.sql RUN mkdir -p .data && chown nextjs:nodejs .data RUN apt-get update && apt-get install -y curl --no-install-recommends && rm -rf /var/lib/apt/lists/* USER nextjs -EXPOSE 3000 ENV PORT=3000 +EXPOSE 3000 ENV HOSTNAME=0.0.0.0 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:3000/login || exit 1 + CMD curl -f http://localhost:${PORT:-3000}/login || exit 1 CMD ["node", "server.js"] diff --git a/README.md b/README.md index 7385b22..34143f2 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,14 @@ Inter-agent communication via the comms API. Agents can send messages to each ot ### Integrations Outbound webhooks with delivery history, configurable alert rules with cooldowns, and multi-gateway connection management. Optional 1Password CLI integration for secret management. +### Workspace Management +Workspaces (tenant instances) are created and managed through the **Super Admin** panel, accessible from the sidebar under **Admin > Super Admin**. From there, admins can: +- **Create** new client instances (slug, display name, Linux user, gateway port, plan tier) +- **Monitor** provisioning jobs and their step-by-step progress +- **Decommission** tenants with optional cleanup of state directories and Linux users + +Each workspace gets its own isolated environment with a dedicated OpenClaw gateway, state directory, and workspace root. See the [Super Admin API](#api-overview) endpoints under `/api/super/*` for programmatic access. + ### Update Checker Automatic GitHub release check notifies you when a new version is available, displayed as a banner in the dashboard. @@ -286,6 +294,20 @@ All endpoints require authentication unless noted. Full reference below. +
+Super Admin (Workspace/Tenant Management) + +| Method | Path | Role | Description | +|--------|------|------|-------------| +| `GET` | `/api/super/tenants` | admin | List all tenants with latest provisioning status | +| `POST` | `/api/super/tenants` | admin | Create tenant and queue bootstrap job | +| `POST` | `/api/super/tenants/[id]/decommission` | admin | Queue tenant decommission job | +| `GET` | `/api/super/provision-jobs` | admin | List provisioning jobs (filter: `?tenant_id=`, `?status=`) | +| `POST` | `/api/super/provision-jobs` | admin | Queue additional job for existing tenant | +| `POST` | `/api/super/provision-jobs/[id]/action` | admin | Approve, reject, or cancel a provisioning job | + +
+
Direct CLI @@ -427,6 +449,24 @@ pnpm test:e2e # Playwright E2E pnpm quality:gate # All checks ``` +## Agent Diagnostics Contract + +`GET /api/agents/{id}/diagnostics` is self-scoped by default. + +- Self access: + - Session user where `username === agent.name`, or + - API-key request with `x-agent-name` matching `{id}` agent name +- Cross-agent access: + - Allowed only with explicit `?privileged=1` and admin auth +- Query validation: + - `hours` must be an integer between `1` and `720` + - `section` must be a comma-separated subset of `summary,tasks,errors,activity,trends,tokens` + +Trend alerts in the `trends.alerts` response are derived from current-vs-previous window comparisons: + +- `warning`: error spikes or severe activity drop +- `info`: throughput drops or potential stall patterns + ## Roadmap See [open issues](https://github.com/builderz-labs/mission-control/issues) for planned work and the [v1.0.0 release notes](https://github.com/builderz-labs/mission-control/releases/tag/v1.0.0) for what shipped. diff --git a/docker-compose.yml b/docker-compose.yml index 9a16156..7ae529a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,9 @@ services: build: . container_name: mission-control ports: - - "${MC_PORT:-3000}:3000" + - "${MC_PORT:-3000}:${PORT:-3000}" + environment: + - PORT=${PORT:-3000} env_file: - path: .env required: false diff --git a/openapi.json b/openapi.json index 50105d5..a041c78 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Mission Control API", - "version": "1.2.0", + "version": "1.3.0", "description": "AI Agent Orchestration Platform API" }, "servers": [ @@ -75,9 +75,1656 @@ { "name": "Connections", "description": "Direct CLI tool connections" + }, + { + "name": "Projects", + "description": "Project management and task grouping" + }, + { + "name": "Mentions", + "description": "User and agent mention autocomplete" + }, + { + "name": "Quality", + "description": "Quality review gate for tasks" + }, + { + "name": "Releases", + "description": "Release and version checking" + }, + { + "name": "Docs", + "description": "API documentation" } ], "paths": { + "/api/activities": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "List activities", + "operationId": "listActivities", + "parameters": [ + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "actor", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "entity_type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Activity list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "activities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "entity_id": { + "type": "integer" + }, + "actor": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "created_at": { + "type": "integer" + } + } + } + }, + "total": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/agents": { + "get": { + "tags": [ + "Agents" + ], + "summary": "List agents", + "operationId": "listAgents", + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "online", + "offline", + "busy", + "idle", + "error" + ] + } + }, + { + "name": "role", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "maximum": 200 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Paginated agent list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agent" + } + }, + "total": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Agents" + ], + "summary": "Create agent", + "operationId": "createAgent", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "role" + ], + "properties": { + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "session_key": { + "type": "string" + }, + "soul_content": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "online", + "offline", + "busy", + "idle", + "error" + ], + "default": "offline" + }, + "config": { + "type": "object" + }, + "template": { + "type": "string" + }, + "gateway_config": { + "type": "object" + }, + "write_to_gateway": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Agent created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "$ref": "#/components/schemas/Agent" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "description": "Agent name already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + }, + "put": { + "tags": [ + "Agents" + ], + "summary": "Update agent by name", + "operationId": "updateAgentByName", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "online", + "offline", + "busy", + "idle", + "error" + ] + }, + "last_activity": { + "type": "string" + }, + "config": { + "type": "object" + }, + "session_key": { + "type": "string" + }, + "soul_content": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Agent updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + } + }, + "/api/agents/comms": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Get agent communications", + "operationId": "getAgentComms", + "parameters": [ + { + "name": "agent", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "Agent communication log", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "from_agent": { + "type": "string" + }, + "to_agent": { + "type": "string" + }, + "content": { + "type": "string" + }, + "type": { + "type": "string" + }, + "created_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/agents/message": { + "post": { + "tags": [ + "Agents" + ], + "summary": "Send message between agents", + "operationId": "sendAgentMessage", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "from", + "to", + "content" + ], + "properties": { + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "content": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Message sent", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/agents/sync": { + "post": { + "tags": [ + "Agents" + ], + "summary": "Sync agents from gateway config", + "operationId": "syncAgents", + "responses": { + "200": { + "description": "Sync results", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "synced": { + "type": "integer" + }, + "created": { + "type": "integer" + }, + "updated": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/agents/{id}": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Get agent by ID", + "operationId": "getAgent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Agent details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "$ref": "#/components/schemas/Agent" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "tags": [ + "Agents" + ], + "summary": "Update agent by ID", + "operationId": "updateAgent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "config": { + "type": "object" + }, + "session_key": { + "type": "string" + }, + "soul_content": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Agent updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "$ref": "#/components/schemas/Agent" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + }, + "delete": { + "tags": [ + "Agents" + ], + "summary": "Delete agent", + "operationId": "deleteAgent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Agent deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/agents/{id}/heartbeat": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Check agent work items", + "operationId": "getAgentHeartbeat", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Heartbeat data with pending work items", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "pending_tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Task" + } + }, + "messages": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "post": { + "tags": [ + "Agents" + ], + "summary": "Trigger manual heartbeat", + "operationId": "triggerAgentHeartbeat", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Heartbeat triggered", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/agents/{id}/memory": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Get agent memory", + "operationId": "getAgentMemory", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Agent memory data", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "tags": [ + "Agents" + ], + "summary": "Update agent memory", + "operationId": "updateAgentMemory", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Memory updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/agents/{id}/soul": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Get agent soul config", + "operationId": "getAgentSoul", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Soul configuration", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "soul_content": { + "type": "string" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "put": { + "tags": [ + "Agents" + ], + "summary": "Update agent soul config", + "operationId": "updateAgentSoul", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "soul_content" + ], + "properties": { + "soul_content": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Soul updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/agents/{id}/wake": { + "post": { + "tags": [ + "Agents" + ], + "summary": "Wake an agent", + "operationId": "wakeAgent", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Agent woken", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/alerts": { + "get": { + "tags": [ + "Alerts" + ], + "summary": "List alert rules", + "operationId": "listAlertRules", + "responses": { + "200": { + "description": "Alert rule list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertRule" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Alerts" + ], + "summary": "Create alert rule or evaluate rules", + "operationId": "createAlertRule", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "title": "CreateRule", + "required": [ + "name", + "entity_type", + "condition_field", + "condition_operator", + "condition_value" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "entity_type": { + "type": "string", + "enum": [ + "agent", + "task", + "session", + "activity" + ] + }, + "condition_field": { + "type": "string" + }, + "condition_operator": { + "type": "string", + "enum": [ + "equals", + "not_equals", + "greater_than", + "less_than", + "contains", + "count_above", + "count_below", + "age_minutes_above" + ] + }, + "condition_value": { + "type": "string" + }, + "action_type": { + "type": "string", + "default": "notification" + }, + "action_config": { + "type": "object" + }, + "cooldown_minutes": { + "type": "integer", + "default": 60 + } + } + }, + { + "type": "object", + "title": "EvaluateRules", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "const": "evaluate" + } + } + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Rule created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rule": { + "$ref": "#/components/schemas/AlertRule" + } + } + } + } + } + }, + "200": { + "description": "Rules evaluated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "evaluated": { + "type": "integer" + }, + "triggered": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rule_id": { + "type": "integer" + }, + "rule_name": { + "type": "string" + }, + "triggered": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + }, + "put": { + "tags": [ + "Alerts" + ], + "summary": "Update alert rule", + "operationId": "updateAlertRule", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "entity_type": { + "type": "string" + }, + "condition_field": { + "type": "string" + }, + "condition_operator": { + "type": "string" + }, + "condition_value": { + "type": "string" + }, + "action_type": { + "type": "string" + }, + "action_config": { + "type": "object" + }, + "cooldown_minutes": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Rule updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rule": { + "$ref": "#/components/schemas/AlertRule" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + }, + "delete": { + "tags": [ + "Alerts" + ], + "summary": "Delete alert rule", + "operationId": "deleteAlertRule", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Rule deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + } + }, + "/api/audit": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "Get audit log", + "operationId": "getAuditLog", + "parameters": [ + { + "name": "action", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "actor", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Audit log entries", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "action": { + "type": "string" + }, + "actor": { + "type": "string" + }, + "target_type": { + "type": "string" + }, + "target_id": { + "type": "integer" + }, + "detail": { + "type": "object" + }, + "ip_address": { + "type": "string" + }, + "created_at": { + "type": "integer" + } + } + } + }, + "total": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/auth/access-requests": { + "get": { + "tags": [ + "Auth" + ], + "summary": "List access requests", + "operationId": "listAccessRequests", + "responses": { + "200": { + "description": "Access request list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "requests": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "approved", + "rejected" + ] + }, + "created_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "tags": [ + "Auth" + ], + "summary": "Approve or reject access request", + "operationId": "handleAccessRequest", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id", + "action" + ], + "properties": { + "id": { + "type": "integer" + }, + "action": { + "type": "string", + "enum": [ + "approve", + "reject" + ] + }, + "role": { + "type": "string", + "enum": [ + "admin", + "operator", + "viewer" + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Request processed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/auth/google": { + "get": { + "tags": [ + "Auth" + ], + "summary": "Google OAuth callback", + "operationId": "googleOAuthCallback", + "security": [], + "parameters": [ + { + "name": "code", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Redirects to dashboard after successful auth" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + } + }, "/api/auth/login": { "post": { "tags": [ @@ -434,22 +2081,60 @@ } } }, - "/api/auth/access-requests": { - "get": { + "/api/backup": { + "post": { "tags": [ - "Auth" + "Admin" ], - "summary": "List access requests", - "operationId": "listAccessRequests", + "summary": "Trigger backup", + "operationId": "triggerBackup", "responses": { "200": { - "description": "Access request list", + "description": "Backup completed", "content": { "application/json": { "schema": { "type": "object", "properties": { - "requests": { + "success": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "size_bytes": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/chat/conversations": { + "get": { + "tags": [ + "Chat" + ], + "summary": "List conversations", + "operationId": "listConversations", + "responses": { + "200": { + "description": "Conversation list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "conversations": { "type": "array", "items": { "type": "object", @@ -457,23 +2142,473 @@ "id": { "type": "integer" }, - "username": { + "participants": { + "type": "array", + "items": { + "type": "string" + } + }, + "last_message": { "type": "string" }, - "email": { + "updated_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/chat/messages": { + "get": { + "tags": [ + "Chat" + ], + "summary": "List messages", + "operationId": "listMessages", + "parameters": [ + { + "name": "conversation_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Message list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "conversation_id": { + "type": "integer" + }, + "sender": { "type": "string" }, - "reason": { + "content": { "type": "string" }, + "created_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Chat" + ], + "summary": "Send message", + "operationId": "sendMessage", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "conversation_id": { + "type": "integer" + }, + "content": { + "type": "string" + }, + "recipient": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "object" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/chat/messages/{id}": { + "get": { + "tags": [ + "Chat" + ], + "summary": "Get message by ID", + "operationId": "getMessage", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Message details", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "delete": { + "tags": [ + "Chat" + ], + "summary": "Delete message", + "operationId": "deleteMessage", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Message deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/claude/sessions": { + "get": { + "tags": [ + "Sessions" + ], + "summary": "List Claude CLI sessions", + "operationId": "listClaudeSessions", + "responses": { + "200": { + "description": "Session list" + } + } + }, + "post": { + "tags": [ + "Sessions" + ], + "summary": "Register a Claude CLI session", + "operationId": "registerClaudeSession", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_name": { + "type": "string" + }, + "model": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Session registered" + } + } + } + }, + "/api/cleanup": { + "post": { + "tags": [ + "Admin" + ], + "summary": "Trigger cleanup of old data", + "operationId": "triggerCleanup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "older_than_days": { + "type": "integer", + "default": 30 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Cleanup results", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "deleted": { + "type": "object" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/connect": { + "post": { + "tags": [ + "Connections" + ], + "summary": "Register a direct CLI connection", + "description": "Registers a CLI tool directly without a gateway. Auto-creates agent if name doesn't exist.", + "operationId": "registerConnection", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "tool_name", + "agent_name" + ], + "properties": { + "tool_name": { + "type": "string" + }, + "tool_version": { + "type": "string" + }, + "agent_name": { + "type": "string" + }, + "agent_role": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Connection registered", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connection_id": { + "type": "string", + "format": "uuid" + }, + "agent_id": { + "type": "integer" + }, + "agent_name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "connected" + ] + }, + "sse_url": { + "type": "string" + }, + "heartbeat_url": { + "type": "string" + }, + "token_report_url": { + "type": "string" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "get": { + "tags": [ + "Connections" + ], + "summary": "List all direct connections", + "operationId": "listConnections", + "responses": { + "200": { + "description": "List of connections", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "agent_id": { + "type": "integer" + }, + "agent_name": { + "type": "string" + }, + "tool_name": { + "type": "string" + }, + "tool_version": { + "type": "string" + }, + "connection_id": { + "type": "string", + "format": "uuid" + }, "status": { "type": "string", "enum": [ - "pending", - "approved", - "rejected" + "connected", + "disconnected" ] }, + "last_heartbeat": { + "type": "integer" + }, "created_at": { "type": "integer" } @@ -485,6 +2620,127 @@ } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "delete": { + "tags": [ + "Connections" + ], + "summary": "Disconnect a CLI connection", + "operationId": "disconnectConnection", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "connection_id" + ], + "properties": { + "connection_id": { + "type": "string", + "format": "uuid" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Disconnected", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "disconnected" + ] + }, + "connection_id": { + "type": "string" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/cron": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get cron jobs", + "operationId": "getCronJobs", + "parameters": [ + { + "name": "action", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "list", + "logs" + ], + "default": "list" + } + } + ], + "responses": { + "200": { + "description": "Cron job list or logs", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "jobs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "schedule": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "last_run": { + "type": "integer" + }, + "last_status": { + "type": "string" + } + } + } + } + } + } + } + } + }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -495,10 +2751,10 @@ }, "post": { "tags": [ - "Auth" + "Admin" ], - "summary": "Approve or reject access request", - "operationId": "handleAccessRequest", + "summary": "Manage cron jobs", + "operationId": "manageCronJobs", "requestBody": { "required": true, "content": { @@ -506,225 +2762,28 @@ "schema": { "type": "object", "required": [ - "id", "action" ], "properties": { - "id": { - "type": "integer" - }, "action": { "type": "string", "enum": [ - "approve", - "reject" + "toggle", + "trigger", + "add", + "remove" ] }, - "role": { - "type": "string", - "enum": [ - "admin", - "operator", - "viewer" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Request processed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/auth/google": { - "get": { - "tags": [ - "Auth" - ], - "summary": "Google OAuth callback", - "operationId": "googleOAuthCallback", - "security": [], - "parameters": [ - { - "name": "code", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "state", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "302": { - "description": "Redirects to dashboard after successful auth" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - } - } - } - }, - "/api/agents": { - "get": { - "tags": [ - "Agents" - ], - "summary": "List agents", - "operationId": "listAgents", - "parameters": [ - { - "name": "status", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "online", - "offline", - "busy", - "idle", - "error" - ] - } - }, - { - "name": "role", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "maximum": 200 - } - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0 - } - } - ], - "responses": { - "200": { - "description": "Paginated agent list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Agent" - } - }, - "total": { - "type": "integer" - }, - "page": { - "type": "integer" - }, - "limit": { - "type": "integer" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Agents" - ], - "summary": "Create agent", - "operationId": "createAgent", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "role" - ], - "properties": { "name": { "type": "string" }, - "role": { + "schedule": { "type": "string" }, - "session_key": { + "command": { "type": "string" }, - "soul_content": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "online", - "offline", - "busy", - "idle", - "error" - ], - "default": "offline" - }, - "config": { - "type": "object" - }, - "template": { - "type": "string" - }, - "gateway_config": { - "type": "object" - }, - "write_to_gateway": { + "enabled": { "type": "boolean" } } @@ -732,98 +2791,9 @@ } } }, - "responses": { - "201": { - "description": "Agent created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agent": { - "$ref": "#/components/schemas/Agent" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "409": { - "description": "Agent name already exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - }, - "429": { - "$ref": "#/components/responses/RateLimited" - } - } - }, - "put": { - "tags": [ - "Agents" - ], - "summary": "Update agent by name", - "operationId": "updateAgentByName", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "online", - "offline", - "busy", - "idle", - "error" - ] - }, - "last_activity": { - "type": "string" - }, - "config": { - "type": "object" - }, - "session_key": { - "type": "string" - }, - "soul_content": { - "type": "string" - }, - "role": { - "type": "string" - } - } - } - } - } - }, "responses": { "200": { - "description": "Agent updated", + "description": "Action applied", "content": { "application/json": { "schema": { @@ -845,115 +2815,95 @@ }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/RateLimited" } } } }, - "/api/agents/{id}": { + "/api/docs": { "get": { "tags": [ - "Agents" - ], - "summary": "Get agent by ID", - "operationId": "getAgent", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } + "Docs" ], + "summary": "Get OpenAPI specification", + "operationId": "getOpenApiSpec", + "security": [], "responses": { "200": { - "description": "Agent details", + "description": "OpenAPI 3.1 JSON spec" + } + } + } + }, + "/api/events": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "SSE stream for real-time events", + "operationId": "getEventStream", + "responses": { + "200": { + "description": "Server-Sent Events stream", "content": { - "application/json": { + "text/event-stream": { "schema": { - "type": "object", - "properties": { - "agent": { - "$ref": "#/components/schemas/Agent" - } - } + "type": "string" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" } } - }, - "put": { + } + }, + "/api/export": { + "get": { "tags": [ - "Agents" + "Admin" ], - "summary": "Update agent by ID", - "operationId": "updateAgent", + "summary": "Export data", + "operationId": "exportData", "parameters": [ { - "name": "id", - "in": "path", - "required": true, + "name": "type", + "in": "query", "schema": { - "type": "integer" + "type": "string", + "enum": [ + "tasks", + "agents", + "activities", + "all" + ] + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "json", + "csv" + ], + "default": "json" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "role": { - "type": "string" - }, - "status": { - "type": "string" - }, - "config": { - "type": "object" - }, - "session_key": { - "type": "string" - }, - "soul_content": { - "type": "string" - } - } - } - } - } - }, "responses": { "200": { - "description": "Agent updated", + "description": "Exported data", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "agent": { - "$ref": "#/components/schemas/Agent" - } - } + "type": "object" + } + }, + "text/csv": { + "schema": { + "type": "string" } } } @@ -963,55 +2913,6 @@ }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/RateLimited" - } - } - }, - "delete": { - "tags": [ - "Agents" - ], - "summary": "Delete agent", - "operationId": "deleteAgent", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Agent deleted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" } } } @@ -1096,47 +2997,20 @@ } } }, - "/api/agents/{id}/heartbeat": { + "/api/gateway-config": { "get": { "tags": [ - "Agents" - ], - "summary": "Check agent work items", - "operationId": "getAgentHeartbeat", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } + "Admin" ], + "summary": "Read gateway config", + "operationId": "getGatewayConfig", "responses": { "200": { - "description": "Heartbeat data with pending work items", + "description": "Gateway configuration", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "agent": { - "type": "string" - }, - "pending_tasks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Task" - } - }, - "messages": { - "type": "array", - "items": { - "type": "object" - } - } - } + "type": "object" } } } @@ -1144,30 +3018,30 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { - "$ref": "#/components/responses/NotFound" + "403": { + "$ref": "#/components/responses/Forbidden" } } }, "post": { "tags": [ - "Agents" + "Admin" ], - "summary": "Trigger manual heartbeat", - "operationId": "triggerAgentHeartbeat", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" + "summary": "Update gateway config", + "operationId": "updateGatewayConfig", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } } } - ], + }, "responses": { "200": { - "description": "Heartbeat triggered", + "description": "Config updated", "content": { "application/json": { "schema": { @@ -1184,39 +3058,49 @@ "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { - "$ref": "#/components/responses/NotFound" + "403": { + "$ref": "#/components/responses/Forbidden" } } } }, - "/api/agents/{id}/soul": { + "/api/gateways": { "get": { "tags": [ - "Agents" - ], - "summary": "Get agent soul config", - "operationId": "getAgentSoul", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } + "Admin" ], + "summary": "List gateways", + "operationId": "listGateways", "responses": { "200": { - "description": "Soul configuration", + "description": "Gateway list", "content": { "application/json": { "schema": { "type": "object", "properties": { - "soul_content": { - "type": "string" + "gateways": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "status": { + "type": "string" + }, + "last_health_check": { + "type": "integer" + } + } + } } } } @@ -1225,28 +3109,15 @@ }, "401": { "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" } } }, - "put": { + "post": { "tags": [ - "Agents" - ], - "summary": "Update agent soul config", - "operationId": "updateAgentSoul", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } + "Admin" ], + "summary": "Add gateway", + "operationId": "addGateway", "requestBody": { "required": true, "content": { @@ -1254,10 +3125,67 @@ "schema": { "type": "object", "required": [ - "soul_content" + "name", + "url" ], "properties": { - "soul_content": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Gateway added", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "put": { + "tags": [ + "Admin" + ], + "summary": "Update gateway", + "operationId": "updateGateway", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "url": { "type": "string" } } @@ -1267,7 +3195,7 @@ }, "responses": { "200": { - "description": "Soul updated", + "description": "Gateway updated", "content": { "application/json": { "schema": { @@ -1291,28 +3219,338 @@ "$ref": "#/components/responses/NotFound" } } + }, + "delete": { + "tags": [ + "Admin" + ], + "summary": "Delete gateway", + "operationId": "deleteGateway", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Gateway deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } } }, - "/api/agents/{id}/memory": { + "/api/gateways/health": { + "post": { + "tags": [ + "Admin" + ], + "summary": "Probe gateway health", + "operationId": "probeGatewayHealth", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gateway_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Health check results", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "latency_ms": { + "type": "number" + }, + "details": { + "type": "object" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/github": { "get": { "tags": [ - "Agents" + "Admin" ], - "summary": "Get agent memory", - "operationId": "getAgentMemory", + "summary": "Get GitHub integration status", + "operationId": "getGithubStatus", + "responses": { + "200": { + "description": "GitHub integration status" + } + } + }, + "post": { + "tags": [ + "Admin" + ], + "summary": "Sync GitHub issues", + "operationId": "syncGithubIssues", + "responses": { + "200": { + "description": "Sync result" + } + } + } + }, + "/api/integrations": { + "get": { + "tags": [ + "Admin" + ], + "summary": "List integrations", + "operationId": "listIntegrations", + "responses": { + "200": { + "description": "Integration list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "integrations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "config": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Admin" + ], + "summary": "Integration actions (enable, disable, test, configure)", + "operationId": "integrationAction", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "action", + "id" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "enable", + "disable", + "test", + "configure" + ] + }, + "id": { + "type": "string" + }, + "config": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Action applied", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/logs": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "Get system logs", + "operationId": "getSystemLogs", "parameters": [ { - "name": "id", - "in": "path", - "required": true, + "name": "level", + "in": "query", "schema": { - "type": "integer" + "type": "string", + "enum": [ + "info", + "warn", + "error", + "debug" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100 } } ], "responses": { "200": { - "description": "Agent memory data", + "description": "Log entries", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "logs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "timestamp": { + "type": "string" + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + }, + "source": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/memory": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get memory files", + "operationId": "getMemoryFiles", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Memory file contents", "content": { "application/json": { "schema": { @@ -1323,34 +3561,33 @@ }, "401": { "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" } } }, - "put": { + "post": { "tags": [ - "Agents" - ], - "summary": "Update agent memory", - "operationId": "updateAgentMemory", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } + "Admin" ], + "summary": "Update memory file", + "operationId": "updateMemoryFile", "requestBody": { "required": true, "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "required": [ + "path", + "content" + ], + "properties": { + "path": { + "type": "string" + }, + "content": { + "type": "string" + } + } } } } @@ -1376,37 +3613,167 @@ }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" } } } }, - "/api/agents/{id}/wake": { - "post": { + "/api/mentions": { + "get": { "tags": [ - "Agents" + "Mentions" ], - "summary": "Wake an agent", - "operationId": "wakeAgent", + "summary": "Get mention autocomplete targets", + "operationId": "getMentionTargets", "parameters": [ { - "name": "id", - "in": "path", - "required": true, + "name": "q", + "in": "query", "schema": { - "type": "integer" + "type": "string" + }, + "description": "Search query" + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "user", + "agent" + ] + }, + "description": "Filter by type" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + }, + "description": "Max results" + } + ], + "responses": { + "200": { + "description": "Mention targets list" + } + } + } + }, + "/api/notifications": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "List notifications", + "operationId": "listNotifications", + "parameters": [ + { + "name": "recipient", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "unread", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50 } } ], + "responses": { + "200": { + "description": "Notification list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "recipient": { + "type": "string" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "read": { + "type": "boolean" + }, + "source_type": { + "type": "string" + }, + "source_id": { + "type": "integer" + }, + "created_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Monitoring" + ], + "summary": "Notification actions (mark read, dismiss)", + "operationId": "notificationAction", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "action" + ], "properties": { - "reason": { + "action": { + "type": "string", + "enum": [ + "mark_read", + "mark_all_read", + "dismiss" + ] + }, + "id": { + "type": "integer" + }, + "recipient": { "type": "string" } } @@ -1416,7 +3783,7 @@ }, "responses": { "200": { - "description": "Agent woken", + "description": "Action applied", "content": { "application/json": { "schema": { @@ -1430,6 +3797,123 @@ } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/agents/{id}/diagnostics": { + "get": { + "tags": [ + "Agents" + ], + "summary": "Get self diagnostics for an agent", + "description": "Self-scoped diagnostics by default. Cross-agent access requires `privileged=1` with admin credentials. Trend alerts are informational signals derived from current-vs-previous window deltas (error spikes, throughput drops, activity stalls).", + "operationId": "getAgentDiagnostics", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent numeric ID or name." + }, + { + "name": "hours", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 720, + "default": 24 + }, + "description": "Diagnostics window in hours." + }, + { + "name": "section", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Comma-separated sections: summary,tasks,errors,activity,trends,tokens." + }, + { + "name": "privileged", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "1" + ] + }, + "description": "Set to `1` to allow explicit admin cross-agent diagnostics access." + } + ], + "responses": { + "200": { + "description": "Diagnostics payload", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "role": { "type": "string" }, + "status": { "type": "string" } + } + }, + "timeframe": { + "type": "object", + "properties": { + "hours": { "type": "integer" }, + "since": { "type": "integer" }, + "until": { "type": "integer" } + } + }, + "summary": { "type": "object" }, + "tasks": { "type": "object" }, + "errors": { "type": "object" }, + "activity": { "type": "object" }, + "trends": { + "type": "object", + "properties": { + "current_period": { "type": "object" }, + "previous_period": { "type": "object" }, + "change": { "type": "object" }, + "alerts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "level": { "type": "string", "enum": ["info", "warning"] }, + "message": { "type": "string" } + } + } + } + } + }, + "tokens": { "type": "object" } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, @@ -1442,13 +3926,13 @@ } } }, - "/api/agents/message": { + "/api/notifications/deliver": { "post": { "tags": [ - "Agents" + "Monitoring" ], - "summary": "Send message between agents", - "operationId": "sendAgentMessage", + "summary": "Deliver notification to agent", + "operationId": "deliverNotification", "requestBody": { "required": true, "content": { @@ -1456,18 +3940,18 @@ "schema": { "type": "object", "required": [ - "from", - "to", - "content" + "agent", + "title", + "message" ], "properties": { - "from": { + "agent": { "type": "string" }, - "to": { + "title": { "type": "string" }, - "content": { + "message": { "type": "string" }, "type": { @@ -1480,7 +3964,7 @@ }, "responses": { "200": { - "description": "Message sent", + "description": "Notification delivered", "content": { "application/json": { "schema": { @@ -1488,9 +3972,6 @@ "properties": { "success": { "type": "boolean" - }, - "id": { - "type": "integer" } } } @@ -1509,13 +3990,793 @@ } } }, - "/api/agents/comms": { + "/api/pipelines": { "get": { "tags": [ - "Agents" + "Pipelines" ], - "summary": "Get agent communications", - "operationId": "getAgentComms", + "summary": "List pipelines", + "operationId": "listPipelines", + "responses": { + "200": { + "description": "Pipeline list with enriched step data and run counts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipelines": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "template_id": { + "type": "integer" + }, + "template_name": { + "type": "string" + }, + "on_failure": { + "type": "string", + "enum": [ + "stop", + "continue" + ] + } + } + } + }, + "created_by": { + "type": "string" + }, + "use_count": { + "type": "integer" + }, + "runs": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "completed": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "running": { + "type": "integer" + } + } + }, + "created_at": { + "type": "integer" + }, + "updated_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + }, + "post": { + "tags": [ + "Pipelines" + ], + "summary": "Create pipeline", + "operationId": "createPipeline", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "steps" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "template_id" + ], + "properties": { + "template_id": { + "type": "integer" + }, + "on_failure": { + "type": "string", + "enum": [ + "stop", + "continue" + ], + "default": "stop" + } + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pipeline created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + }, + "put": { + "tags": [ + "Pipelines" + ], + "summary": "Update pipeline", + "operationId": "updatePipeline", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Pipeline updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "delete": { + "tags": [ + "Pipelines" + ], + "summary": "Delete pipeline", + "operationId": "deletePipeline", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Pipeline deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/pipelines/run": { + "post": { + "tags": [ + "Pipelines" + ], + "summary": "Run a pipeline", + "operationId": "runPipeline", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pipeline_id" + ], + "properties": { + "pipeline_id": { + "type": "integer" + }, + "params": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Pipeline run started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "run_id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/projects": { + "get": { + "tags": [ + "Projects" + ], + "summary": "List all projects", + "operationId": "listProjects", + "responses": { + "200": { + "description": "Project list" + } + } + }, + "post": { + "tags": [ + "Projects" + ], + "summary": "Create a new project", + "operationId": "createProject", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Project created" + } + } + } + }, + "/api/projects/{id}": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Get project by ID", + "operationId": "getProject", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Project details" + } + } + }, + "patch": { + "tags": [ + "Projects" + ], + "summary": "Update a project", + "operationId": "updateProject", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": [ + "Projects" + ], + "summary": "Delete a project", + "operationId": "deleteProject", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Deleted" + } + } + } + }, + "/api/projects/{id}/tasks": { + "get": { + "tags": [ + "Projects" + ], + "summary": "List tasks in a project", + "operationId": "listProjectTasks", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Tasks in project" + } + } + } + }, + "/api/quality-review": { + "get": { + "tags": [ + "Quality" + ], + "summary": "List quality reviews", + "operationId": "listQualityReviews", + "parameters": [ + { + "name": "task_id", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Filter by task" + } + ], + "responses": { + "200": { + "description": "Quality review list" + } + } + }, + "post": { + "tags": [ + "Quality" + ], + "summary": "Submit a quality review", + "operationId": "submitQualityReview", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "task_id", + "status" + ], + "properties": { + "task_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "approved", + "rejected" + ] + }, + "notes": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Review submitted" + } + } + } + }, + "/api/releases/check": { + "get": { + "tags": [ + "Releases" + ], + "summary": "Check for new releases", + "operationId": "checkReleases", + "security": [], + "responses": { + "200": { + "description": "Release info with version comparison" + } + } + } + }, + "/api/scheduler": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get scheduler status", + "operationId": "getSchedulerStatus", + "responses": { + "200": { + "description": "Scheduler status and registered tasks", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "running": { + "type": "boolean" + }, + "tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "interval": { + "type": "string" + }, + "last_run": { + "type": "integer" + }, + "next_run": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "tags": [ + "Admin" + ], + "summary": "Trigger scheduled task", + "operationId": "triggerScheduledTask", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "task" + ], + "properties": { + "task": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Task triggered", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "result": { + "type": "object" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/search": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Full-text search", + "operationId": "search", + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "tasks", + "agents", + "activities", + "all" + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Search results", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "snippet": { + "type": "string" + }, + "score": { + "type": "number" + } + } + } + }, + "total": { + "type": "integer" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/sessions": { + "get": { + "tags": [ + "Sessions" + ], + "summary": "List gateway sessions", + "operationId": "listSessions", "parameters": [ { "name": "agent", @@ -1535,33 +4796,33 @@ ], "responses": { "200": { - "description": "Agent communication log", + "description": "Session list", "content": { "application/json": { "schema": { "type": "object", "properties": { - "messages": { + "sessions": { "type": "array", "items": { "type": "object", "properties": { - "id": { + "key": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "string" + }, + "status": { + "type": "string" + }, + "totalTokens": { "type": "integer" }, - "from_agent": { - "type": "string" - }, - "to_agent": { - "type": "string" - }, - "content": { - "type": "string" - }, - "type": { - "type": "string" - }, - "created_at": { + "updatedAt": { "type": "integer" } } @@ -1578,29 +4839,94 @@ } } }, - "/api/agents/sync": { + "/api/sessions/{id}/control": { "post": { "tags": [ - "Agents" + "Sessions" ], - "summary": "Sync agents from gateway config", - "operationId": "syncAgents", + "summary": "Control session (pause/resume/kill)", + "operationId": "controlSession", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "pause", + "resume", + "kill" + ] + } + } + } + } + } + }, "responses": { "200": { - "description": "Sync results", + "description": "Action applied", "content": { "application/json": { "schema": { "type": "object", "properties": { - "synced": { - "type": "integer" - }, - "created": { - "type": "integer" - }, - "updated": { - "type": "integer" + "success": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/settings": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get application settings", + "operationId": "getSettings", + "responses": { + "200": { + "description": "Current settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "settings": { + "type": "object" } } } @@ -1614,6 +4940,596 @@ "$ref": "#/components/responses/Forbidden" } } + }, + "post": { + "tags": [ + "Admin" + ], + "summary": "Update settings", + "operationId": "updateSettings", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/spawn": { + "post": { + "tags": [ + "Admin" + ], + "summary": "Spawn agent process", + "operationId": "spawnAgent", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "agent" + ], + "properties": { + "agent": { + "type": "string" + }, + "task": { + "type": "string" + }, + "params": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Agent spawned", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "session_id": { + "type": "string" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/standup": { + "get": { + "tags": [ + "Admin" + ], + "summary": "Get standup report", + "operationId": "getStandupReport", + "parameters": [ + { + "name": "date", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "Standup report", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "agents": { + "type": "array", + "items": { + "type": "object" + } + }, + "tasks_completed": { + "type": "integer" + }, + "tasks_in_progress": { + "type": "integer" + }, + "highlights": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, + "/api/status": { + "get": { + "tags": [ + "Monitoring" + ], + "summary": "Get system status", + "operationId": "getSystemStatus", + "security": [], + "responses": { + "200": { + "description": "System health status", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded", + "down" + ] + }, + "version": { + "type": "string" + }, + "uptime": { + "type": "integer" + }, + "agents": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "online": { + "type": "integer" + } + } + }, + "tasks": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "in_progress": { + "type": "integer" + } + } + } + } + } + } + } + } + } + } + }, + "/api/super/provision-jobs": { + "get": { + "tags": [ + "Super Admin" + ], + "summary": "List provision jobs", + "operationId": "listProvisionJobs", + "responses": { + "200": { + "description": "Provision job list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "jobs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "tenant_id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "running", + "completed", + "failed" + ] + }, + "output": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "completed_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "tags": [ + "Super Admin" + ], + "summary": "Create provision job", + "operationId": "createProvisionJob", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "tenant_id", + "type" + ], + "properties": { + "tenant_id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "params": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Job created", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/api/super/provision-jobs/{id}": { + "get": { + "tags": [ + "Super Admin" + ], + "summary": "Get provision job details", + "operationId": "getProvisionJob", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Job details", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/super/provision-jobs/{id}/run": { + "post": { + "tags": [ + "Super Admin" + ], + "summary": "Run provision job", + "operationId": "runProvisionJob", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Job started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, + "/api/super/tenants": { + "get": { + "tags": [ + "Super Admin" + ], + "summary": "List tenants", + "operationId": "listTenants", + "responses": { + "200": { + "description": "Tenant list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tenants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "linux_user": { + "type": "string" + }, + "created_at": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "tags": [ + "Super Admin" + ], + "summary": "Create tenant and bootstrap job", + "operationId": "createTenant", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "slug", + "name" + ], + "properties": { + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "linux_user": { + "type": "string" + }, + "config": { + "type": "object" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Tenant created with bootstrap job", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "409": { + "description": "Tenant slug or linux user already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/super/tenants/{id}/decommission": { + "post": { + "tags": [ + "Super Admin" + ], + "summary": "Decommission tenant", + "operationId": "decommissionTenant", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Tenant decommissioned", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } } }, "/api/tasks": { @@ -2071,6 +5987,77 @@ } } }, + "/api/tasks/{id}/broadcast": { + "post": { + "tags": [ + "Tasks" + ], + "summary": "Broadcast task to agents", + "operationId": "broadcastTask", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Task broadcast sent", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "delivered_to": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } + }, "/api/tasks/{id}/comments": { "get": { "tags": [ @@ -2200,337 +6187,6 @@ } } }, - "/api/tasks/{id}/broadcast": { - "post": { - "tags": [ - "Tasks" - ], - "summary": "Broadcast task to agents", - "operationId": "broadcastTask", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agents": { - "type": "array", - "items": { - "type": "string" - } - }, - "message": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Task broadcast sent", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "delivered_to": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, - "/api/chat/conversations": { - "get": { - "tags": [ - "Chat" - ], - "summary": "List conversations", - "operationId": "listConversations", - "responses": { - "200": { - "description": "Conversation list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "conversations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "participants": { - "type": "array", - "items": { - "type": "string" - } - }, - "last_message": { - "type": "string" - }, - "updated_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/chat/messages": { - "get": { - "tags": [ - "Chat" - ], - "summary": "List messages", - "operationId": "listMessages", - "parameters": [ - { - "name": "conversation_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50 - } - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0 - } - } - ], - "responses": { - "200": { - "description": "Message list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "conversation_id": { - "type": "integer" - }, - "sender": { - "type": "string" - }, - "content": { - "type": "string" - }, - "created_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Chat" - ], - "summary": "Send message", - "operationId": "sendMessage", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "content" - ], - "properties": { - "conversation_id": { - "type": "integer" - }, - "content": { - "type": "string" - }, - "recipient": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Message sent", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "object" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/chat/messages/{id}": { - "get": { - "tags": [ - "Chat" - ], - "summary": "Get message by ID", - "operationId": "getMessage", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Message details", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - }, - "delete": { - "tags": [ - "Chat" - ], - "summary": "Delete message", - "operationId": "deleteMessage", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Message deleted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, "/api/tokens": { "get": { "tags": [ @@ -2757,146 +6413,6 @@ } } }, - "/api/sessions": { - "get": { - "tags": [ - "Sessions" - ], - "summary": "List gateway sessions", - "operationId": "listSessions", - "parameters": [ - { - "name": "agent", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50 - } - } - ], - "responses": { - "200": { - "description": "Session list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "status": { - "type": "string" - }, - "totalTokens": { - "type": "integer" - }, - "updatedAt": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/sessions/{id}/control": { - "post": { - "tags": [ - "Sessions" - ], - "summary": "Control session (pause/resume/kill)", - "operationId": "controlSession", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "pause", - "resume", - "kill" - ] - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Action applied", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, "/api/webhooks": { "get": { "tags": [ @@ -3237,6 +6753,38 @@ } } }, + "/api/webhooks/retry": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Retry a failed webhook delivery", + "operationId": "retryWebhookDelivery", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "delivery_id" + ], + "properties": { + "delivery_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Delivery retried" + } + } + } + }, "/api/webhooks/test": { "post": { "tags": [ @@ -3296,331 +6844,16 @@ } } }, - "/api/alerts": { + "/api/webhooks/verify-docs": { "get": { "tags": [ - "Alerts" + "Webhooks" ], - "summary": "List alert rules", - "operationId": "listAlertRules", + "summary": "Get webhook verification documentation", + "operationId": "getWebhookVerifyDocs", "responses": { "200": { - "description": "Alert rule list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "rules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AlertRule" - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Alerts" - ], - "summary": "Create alert rule or evaluate rules", - "operationId": "createAlertRule", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "title": "CreateRule", - "required": [ - "name", - "entity_type", - "condition_field", - "condition_operator", - "condition_value" - ], - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "entity_type": { - "type": "string", - "enum": [ - "agent", - "task", - "session", - "activity" - ] - }, - "condition_field": { - "type": "string" - }, - "condition_operator": { - "type": "string", - "enum": [ - "equals", - "not_equals", - "greater_than", - "less_than", - "contains", - "count_above", - "count_below", - "age_minutes_above" - ] - }, - "condition_value": { - "type": "string" - }, - "action_type": { - "type": "string", - "default": "notification" - }, - "action_config": { - "type": "object" - }, - "cooldown_minutes": { - "type": "integer", - "default": 60 - } - } - }, - { - "type": "object", - "title": "EvaluateRules", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string", - "const": "evaluate" - } - } - } - ] - } - } - } - }, - "responses": { - "201": { - "description": "Rule created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "rule": { - "$ref": "#/components/schemas/AlertRule" - } - } - } - } - } - }, - "200": { - "description": "Rules evaluated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "evaluated": { - "type": "integer" - }, - "triggered": { - "type": "integer" - }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "rule_id": { - "type": "integer" - }, - "rule_name": { - "type": "string" - }, - "triggered": { - "type": "boolean" - }, - "reason": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "429": { - "$ref": "#/components/responses/RateLimited" - } - } - }, - "put": { - "tags": [ - "Alerts" - ], - "summary": "Update alert rule", - "operationId": "updateAlertRule", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "entity_type": { - "type": "string" - }, - "condition_field": { - "type": "string" - }, - "condition_operator": { - "type": "string" - }, - "condition_value": { - "type": "string" - }, - "action_type": { - "type": "string" - }, - "action_config": { - "type": "object" - }, - "cooldown_minutes": { - "type": "integer" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Rule updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "rule": { - "$ref": "#/components/schemas/AlertRule" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/RateLimited" - } - } - }, - "delete": { - "tags": [ - "Alerts" - ], - "summary": "Delete alert rule", - "operationId": "deleteAlertRule", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Rule deleted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "deleted": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "429": { - "$ref": "#/components/responses/RateLimited" + "description": "Verification guide and examples" } } } @@ -3877,2684 +7110,6 @@ } } } - }, - "/api/pipelines": { - "get": { - "tags": [ - "Pipelines" - ], - "summary": "List pipelines", - "operationId": "listPipelines", - "responses": { - "200": { - "description": "Pipeline list with enriched step data and run counts", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "pipelines": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "template_id": { - "type": "integer" - }, - "template_name": { - "type": "string" - }, - "on_failure": { - "type": "string", - "enum": [ - "stop", - "continue" - ] - } - } - } - }, - "created_by": { - "type": "string" - }, - "use_count": { - "type": "integer" - }, - "runs": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "completed": { - "type": "integer" - }, - "failed": { - "type": "integer" - }, - "running": { - "type": "integer" - } - } - }, - "created_at": { - "type": "integer" - }, - "updated_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Pipelines" - ], - "summary": "Create pipeline", - "operationId": "createPipeline", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "steps" - ], - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "type": "object", - "required": [ - "template_id" - ], - "properties": { - "template_id": { - "type": "integer" - }, - "on_failure": { - "type": "string", - "enum": [ - "stop", - "continue" - ], - "default": "stop" - } - } - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Pipeline created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "pipeline": { - "type": "object" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "429": { - "$ref": "#/components/responses/RateLimited" - } - } - }, - "put": { - "tags": [ - "Pipelines" - ], - "summary": "Update pipeline", - "operationId": "updatePipeline", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "type": "object" - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Pipeline updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "pipeline": { - "type": "object" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - }, - "delete": { - "tags": [ - "Pipelines" - ], - "summary": "Delete pipeline", - "operationId": "deletePipeline", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Pipeline deleted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/pipelines/run": { - "post": { - "tags": [ - "Pipelines" - ], - "summary": "Run a pipeline", - "operationId": "runPipeline", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "pipeline_id" - ], - "properties": { - "pipeline_id": { - "type": "integer" - }, - "params": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Pipeline run started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "run_id": { - "type": "integer" - }, - "status": { - "type": "string" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, - "/api/activities": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "List activities", - "operationId": "listActivities", - "parameters": [ - { - "name": "type", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "actor", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "entity_type", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50 - } - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0 - } - } - ], - "responses": { - "200": { - "description": "Activity list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "activities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "entity_type": { - "type": "string" - }, - "entity_id": { - "type": "integer" - }, - "actor": { - "type": "string" - }, - "description": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "created_at": { - "type": "integer" - } - } - } - }, - "total": { - "type": "integer" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/audit": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "Get audit log", - "operationId": "getAuditLog", - "parameters": [ - { - "name": "action", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "actor", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 100 - } - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0 - } - } - ], - "responses": { - "200": { - "description": "Audit log entries", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "action": { - "type": "string" - }, - "actor": { - "type": "string" - }, - "target_type": { - "type": "string" - }, - "target_id": { - "type": "integer" - }, - "detail": { - "type": "object" - }, - "ip_address": { - "type": "string" - }, - "created_at": { - "type": "integer" - } - } - } - }, - "total": { - "type": "integer" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/logs": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "Get system logs", - "operationId": "getSystemLogs", - "parameters": [ - { - "name": "level", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "info", - "warn", - "error", - "debug" - ] - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 100 - } - } - ], - "responses": { - "200": { - "description": "Log entries", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "logs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "timestamp": { - "type": "string" - }, - "level": { - "type": "string" - }, - "message": { - "type": "string" - }, - "source": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/notifications": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "List notifications", - "operationId": "listNotifications", - "parameters": [ - { - "name": "recipient", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "unread", - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50 - } - } - ], - "responses": { - "200": { - "description": "Notification list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "notifications": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "recipient": { - "type": "string" - }, - "type": { - "type": "string" - }, - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "read": { - "type": "boolean" - }, - "source_type": { - "type": "string" - }, - "source_id": { - "type": "integer" - }, - "created_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Monitoring" - ], - "summary": "Notification actions (mark read, dismiss)", - "operationId": "notificationAction", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "mark_read", - "mark_all_read", - "dismiss" - ] - }, - "id": { - "type": "integer" - }, - "recipient": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Action applied", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/notifications/deliver": { - "post": { - "tags": [ - "Monitoring" - ], - "summary": "Deliver notification to agent", - "operationId": "deliverNotification", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "agent", - "title", - "message" - ], - "properties": { - "agent": { - "type": "string" - }, - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Notification delivered", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/events": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "SSE stream for real-time events", - "operationId": "getEventStream", - "responses": { - "200": { - "description": "Server-Sent Events stream", - "content": { - "text/event-stream": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/status": { - "get": { - "tags": [ - "Monitoring" - ], - "summary": "Get system status", - "operationId": "getSystemStatus", - "security": [], - "responses": { - "200": { - "description": "System health status", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "ok", - "degraded", - "down" - ] - }, - "version": { - "type": "string" - }, - "uptime": { - "type": "integer" - }, - "agents": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "online": { - "type": "integer" - } - } - }, - "tasks": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "in_progress": { - "type": "integer" - } - } - } - } - } - } - } - } - } - } - }, - "/api/settings": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Get application settings", - "operationId": "getSettings", - "responses": { - "200": { - "description": "Current settings", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "settings": { - "type": "object" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Update settings", - "operationId": "updateSettings", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/scheduler": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Get scheduler status", - "operationId": "getSchedulerStatus", - "responses": { - "200": { - "description": "Scheduler status and registered tasks", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "running": { - "type": "boolean" - }, - "tasks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "interval": { - "type": "string" - }, - "last_run": { - "type": "integer" - }, - "next_run": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Trigger scheduled task", - "operationId": "triggerScheduledTask", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "task" - ], - "properties": { - "task": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Task triggered", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "result": { - "type": "object" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/cron": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Get cron jobs", - "operationId": "getCronJobs", - "parameters": [ - { - "name": "action", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "list", - "logs" - ], - "default": "list" - } - } - ], - "responses": { - "200": { - "description": "Cron job list or logs", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "jobs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "schedule": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "last_run": { - "type": "integer" - }, - "last_status": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Manage cron jobs", - "operationId": "manageCronJobs", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "action" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "toggle", - "trigger", - "add", - "remove" - ] - }, - "name": { - "type": "string" - }, - "schedule": { - "type": "string" - }, - "command": { - "type": "string" - }, - "enabled": { - "type": "boolean" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Action applied", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/gateways": { - "get": { - "tags": [ - "Admin" - ], - "summary": "List gateways", - "operationId": "listGateways", - "responses": { - "200": { - "description": "Gateway list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "gateways": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "url": { - "type": "string" - }, - "status": { - "type": "string" - }, - "last_health_check": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Add gateway", - "operationId": "addGateway", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "url" - ], - "properties": { - "name": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Gateway added", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "put": { - "tags": [ - "Admin" - ], - "summary": "Update gateway", - "operationId": "updateGateway", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Gateway updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - }, - "delete": { - "tags": [ - "Admin" - ], - "summary": "Delete gateway", - "operationId": "deleteGateway", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Gateway deleted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/gateways/health": { - "post": { - "tags": [ - "Admin" - ], - "summary": "Probe gateway health", - "operationId": "probeGatewayHealth", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "gateway_id": { - "type": "integer" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Health check results", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean" - }, - "latency_ms": { - "type": "number" - }, - "details": { - "type": "object" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/gateway-config": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Read gateway config", - "operationId": "getGatewayConfig", - "responses": { - "200": { - "description": "Gateway configuration", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Update gateway config", - "operationId": "updateGatewayConfig", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Config updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/integrations": { - "get": { - "tags": [ - "Admin" - ], - "summary": "List integrations", - "operationId": "listIntegrations", - "responses": { - "200": { - "description": "Integration list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "integrations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "config": { - "type": "object" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Integration actions (enable, disable, test, configure)", - "operationId": "integrationAction", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "action", - "id" - ], - "properties": { - "action": { - "type": "string", - "enum": [ - "enable", - "disable", - "test", - "configure" - ] - }, - "id": { - "type": "string" - }, - "config": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Action applied", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/spawn": { - "post": { - "tags": [ - "Admin" - ], - "summary": "Spawn agent process", - "operationId": "spawnAgent", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "agent" - ], - "properties": { - "agent": { - "type": "string" - }, - "task": { - "type": "string" - }, - "params": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Agent spawned", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "session_id": { - "type": "string" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/standup": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Get standup report", - "operationId": "getStandupReport", - "parameters": [ - { - "name": "date", - "in": "query", - "schema": { - "type": "string", - "format": "date" - } - } - ], - "responses": { - "200": { - "description": "Standup report", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "date": { - "type": "string" - }, - "agents": { - "type": "array", - "items": { - "type": "object" - } - }, - "tasks_completed": { - "type": "integer" - }, - "tasks_in_progress": { - "type": "integer" - }, - "highlights": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/memory": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Get memory files", - "operationId": "getMemoryFiles", - "parameters": [ - { - "name": "path", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Memory file contents", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "post": { - "tags": [ - "Admin" - ], - "summary": "Update memory file", - "operationId": "updateMemoryFile", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "path", - "content" - ], - "properties": { - "path": { - "type": "string" - }, - "content": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Memory updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/search": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Full-text search", - "operationId": "search", - "parameters": [ - { - "name": "q", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "tasks", - "agents", - "activities", - "all" - ] - } - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "Search results", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "snippet": { - "type": "string" - }, - "score": { - "type": "number" - } - } - } - }, - "total": { - "type": "integer" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - } - }, - "/api/backup": { - "post": { - "tags": [ - "Admin" - ], - "summary": "Trigger backup", - "operationId": "triggerBackup", - "responses": { - "200": { - "description": "Backup completed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "size_bytes": { - "type": "integer" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/cleanup": { - "post": { - "tags": [ - "Admin" - ], - "summary": "Trigger cleanup of old data", - "operationId": "triggerCleanup", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "older_than_days": { - "type": "integer", - "default": 30 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Cleanup results", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "deleted": { - "type": "object" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/export": { - "get": { - "tags": [ - "Admin" - ], - "summary": "Export data", - "operationId": "exportData", - "parameters": [ - { - "name": "type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "tasks", - "agents", - "activities", - "all" - ] - } - }, - { - "name": "format", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "json", - "csv" - ], - "default": "json" - } - } - ], - "responses": { - "200": { - "description": "Exported data", - "content": { - "application/json": { - "schema": { - "type": "object" - } - }, - "text/csv": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/super/tenants": { - "get": { - "tags": [ - "Super Admin" - ], - "summary": "List tenants", - "operationId": "listTenants", - "responses": { - "200": { - "description": "Tenant list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tenants": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "linux_user": { - "type": "string" - }, - "created_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Super Admin" - ], - "summary": "Create tenant and bootstrap job", - "operationId": "createTenant", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "slug", - "name" - ], - "properties": { - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "linux_user": { - "type": "string" - }, - "config": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Tenant created with bootstrap job", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "409": { - "description": "Tenant slug or linux user already exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/api/super/tenants/{id}/decommission": { - "post": { - "tags": [ - "Super Admin" - ], - "summary": "Decommission tenant", - "operationId": "decommissionTenant", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Tenant decommissioned", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, - "/api/super/provision-jobs": { - "get": { - "tags": [ - "Super Admin" - ], - "summary": "List provision jobs", - "operationId": "listProvisionJobs", - "responses": { - "200": { - "description": "Provision job list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "jobs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "tenant_id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "pending", - "running", - "completed", - "failed" - ] - }, - "output": { - "type": "string" - }, - "created_at": { - "type": "integer" - }, - "completed_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "tags": [ - "Super Admin" - ], - "summary": "Create provision job", - "operationId": "createProvisionJob", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "tenant_id", - "type" - ], - "properties": { - "tenant_id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "params": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Job created", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - } - }, - "/api/super/provision-jobs/{id}": { - "get": { - "tags": [ - "Super Admin" - ], - "summary": "Get provision job details", - "operationId": "getProvisionJob", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Job details", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, - "/api/super/provision-jobs/{id}/run": { - "post": { - "tags": [ - "Super Admin" - ], - "summary": "Run provision job", - "operationId": "runProvisionJob", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Job started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "status": { - "type": "string" - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } - }, - "/api/connect": { - "post": { - "tags": [ - "Connections" - ], - "summary": "Register a direct CLI connection", - "description": "Registers a CLI tool directly without a gateway. Auto-creates agent if name doesn't exist.", - "operationId": "registerConnection", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "tool_name", - "agent_name" - ], - "properties": { - "tool_name": { - "type": "string" - }, - "tool_version": { - "type": "string" - }, - "agent_name": { - "type": "string" - }, - "agent_role": { - "type": "string" - }, - "metadata": { - "type": "object" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Connection registered", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connection_id": { - "type": "string", - "format": "uuid" - }, - "agent_id": { - "type": "integer" - }, - "agent_name": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "connected" - ] - }, - "sse_url": { - "type": "string" - }, - "heartbeat_url": { - "type": "string" - }, - "token_report_url": { - "type": "string" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "get": { - "tags": [ - "Connections" - ], - "summary": "List all direct connections", - "operationId": "listConnections", - "responses": { - "200": { - "description": "List of connections", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connections": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "agent_id": { - "type": "integer" - }, - "agent_name": { - "type": "string" - }, - "tool_name": { - "type": "string" - }, - "tool_version": { - "type": "string" - }, - "connection_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "connected", - "disconnected" - ] - }, - "last_heartbeat": { - "type": "integer" - }, - "created_at": { - "type": "integer" - } - } - } - } - } - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - } - } - }, - "delete": { - "tags": [ - "Connections" - ], - "summary": "Disconnect a CLI connection", - "operationId": "disconnectConnection", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "connection_id" - ], - "properties": { - "connection_id": { - "type": "string", - "format": "uuid" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Disconnected", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "disconnected" - ] - }, - "connection_id": { - "type": "string" - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" - } - } - } } }, "components": { @@ -6902,6 +7457,26 @@ "type": "integer" } } + }, + "Project": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "updated_at": { + "type": "integer" + } + } } }, "responses": { diff --git a/package.json b/package.json index 25e8136..91ec7a0 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "1.3.0", "description": "OpenClaw Mission Control — open-source agent orchestration dashboard", "scripts": { - "dev": "next dev --hostname 127.0.0.1", + "dev": "next dev --hostname 127.0.0.1 --port ${PORT:-3000}", "build": "next build", - "start": "next start --hostname 0.0.0.0 --port 3005", + "start": "next start --hostname 0.0.0.0 --port ${PORT:-3000}", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run", diff --git a/playwright.config.ts b/playwright.config.ts index f50bcea..4a5c30d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,9 +18,18 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] } } ], webServer: { - command: 'pnpm start', + command: 'node .next/standalone/server.js', url: 'http://127.0.0.1:3005', reuseExistingServer: true, - timeout: 30_000, + timeout: 120_000, + env: { + ...process.env, + HOSTNAME: process.env.HOSTNAME || '127.0.0.1', + PORT: process.env.PORT || '3005', + MC_DISABLE_RATE_LIMIT: process.env.MC_DISABLE_RATE_LIMIT || '1', + API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345', + AUTH_USER: process.env.AUTH_USER || 'testadmin', + AUTH_PASS: process.env.AUTH_PASS || 'testpass1234!', + }, } }) diff --git a/src/app/[[...panel]]/page.tsx b/src/app/[[...panel]]/page.tsx index 0b0dcaa..e7d05dd 100644 --- a/src/app/[[...panel]]/page.tsx +++ b/src/app/[[...panel]]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { NavRail } from '@/components/layout/nav-rail' import { HeaderBar } from '@/components/layout/header-bar' import { LiveFeed } from '@/components/layout/live-feed' @@ -42,6 +42,7 @@ import { useServerEvents } from '@/lib/use-server-events' import { useMissionControl } from '@/store' export default function Home() { + const router = useRouter() const { connect } = useWebSocket() const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable, liveFeedOpen, toggleLiveFeed } = useMissionControl() @@ -62,7 +63,13 @@ export default function Home() { // Fetch current user fetch('/api/auth/me') - .then(res => res.ok ? res.json() : null) + .then(async (res) => { + if (res.ok) return res.json() + if (res.status === 401) { + router.replace(`/login?next=${encodeURIComponent(pathname)}`) + } + return null + }) .then(data => { if (data?.user) setCurrentUser(data.user) }) .catch(() => {}) @@ -120,7 +127,7 @@ export default function Home() { const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}` connect(wsUrl, wsToken) }) - }, [connect, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable]) + }, [connect, pathname, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable]) if (!isClient) { return ( diff --git a/src/app/api/agents/[id]/diagnostics/route.ts b/src/app/api/agents/[id]/diagnostics/route.ts new file mode 100644 index 0000000..4810843 --- /dev/null +++ b/src/app/api/agents/[id]/diagnostics/route.ts @@ -0,0 +1,343 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getDatabase } from '@/lib/db'; +import { requireRole } from '@/lib/auth'; +import { logger } from '@/lib/logger'; + +const ALLOWED_SECTIONS = ['summary', 'tasks', 'errors', 'activity', 'trends', 'tokens'] as const; +type DiagnosticsSection = (typeof ALLOWED_SECTIONS)[number]; + +function parseHoursParam(raw: string | null): { value?: number; error?: string } { + if (raw === null) return { value: 24 }; + const parsed = Number(raw); + if (!Number.isInteger(parsed)) { + return { error: 'hours must be an integer between 1 and 720' }; + } + if (parsed < 1 || parsed > 720) { + return { error: 'hours must be between 1 and 720' }; + } + return { value: parsed }; +} + +function parseSectionsParam(raw: string | null): { value?: Set; error?: string } { + if (!raw || raw.trim().length === 0) { + return { value: new Set(ALLOWED_SECTIONS) }; + } + + const requested = raw + .split(',') + .map((section) => section.trim()) + .filter(Boolean); + + if (requested.length === 0) { + return { error: 'section must include at least one valid value' }; + } + + const invalid = requested.filter((section) => !ALLOWED_SECTIONS.includes(section as DiagnosticsSection)); + if (invalid.length > 0) { + return { error: `Invalid section value(s): ${invalid.join(', ')}` }; + } + + return { value: new Set(requested as DiagnosticsSection[]) }; +} + +/** + * GET /api/agents/[id]/diagnostics - Agent Self-Diagnostics API + * + * Provides an agent with its own performance metrics, error analysis, + * and trend data so it can self-optimize. + * + * Query params: + * hours - Time window in hours (default: 24, max: 720 = 30 days) + * section - Comma-separated sections to include (default: all) + * Options: summary, tasks, errors, activity, trends, tokens + * + * Response includes: + * summary - High-level KPIs (throughput, error rate, activity count) + * tasks - Task completion breakdown by status and priority + * errors - Error frequency, types, and recent error details + * activity - Activity breakdown by type with hourly timeline + * trends - Multi-period comparison for trend detection + * tokens - Token usage by model with cost estimates + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const auth = requireRole(request, 'viewer'); + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + + try { + const db = getDatabase(); + const resolvedParams = await params; + const agentId = resolvedParams.id; + const workspaceId = auth.user.workspace_id ?? 1; + + // Resolve agent by ID or name + let agent: any; + if (/^\d+$/.test(agentId)) { + agent = db.prepare('SELECT id, name, role, status, last_seen, created_at FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId); + } else { + agent = db.prepare('SELECT id, name, role, status, last_seen, created_at FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId); + } + + if (!agent) { + return NextResponse.json({ error: 'Agent not found' }, { status: 404 }); + } + + const { searchParams } = new URL(request.url); + const requesterAgentName = (request.headers.get('x-agent-name') || '').trim(); + const privileged = searchParams.get('privileged') === '1'; + const isSelfRequest = (requesterAgentName || auth.user.username) === agent.name; + + // Self-only by default. Cross-agent access requires explicit privileged override. + if (!isSelfRequest && !(privileged && auth.user.role === 'admin')) { + return NextResponse.json( + { error: 'Diagnostics are self-scoped. Use privileged=1 with admin role for cross-agent access.' }, + { status: 403 } + ); + } + + const parsedHours = parseHoursParam(searchParams.get('hours')); + if (parsedHours.error) { + return NextResponse.json({ error: parsedHours.error }, { status: 400 }); + } + + const parsedSections = parseSectionsParam(searchParams.get('section')); + if (parsedSections.error) { + return NextResponse.json({ error: parsedSections.error }, { status: 400 }); + } + + const hours = parsedHours.value as number; + const sections = parsedSections.value as Set; + + const now = Math.floor(Date.now() / 1000); + const since = now - hours * 3600; + + const result: Record = { + agent: { id: agent.id, name: agent.name, role: agent.role, status: agent.status }, + timeframe: { hours, since, until: now }, + }; + + if (sections.has('summary')) { + result.summary = buildSummary(db, agent.name, workspaceId, since); + } + + if (sections.has('tasks')) { + result.tasks = buildTaskMetrics(db, agent.name, workspaceId, since); + } + + if (sections.has('errors')) { + result.errors = buildErrorAnalysis(db, agent.name, workspaceId, since); + } + + if (sections.has('activity')) { + result.activity = buildActivityBreakdown(db, agent.name, workspaceId, since); + } + + if (sections.has('trends')) { + result.trends = buildTrends(db, agent.name, workspaceId, hours); + } + + if (sections.has('tokens')) { + result.tokens = buildTokenMetrics(db, agent.name, workspaceId, since); + } + + return NextResponse.json(result); + } catch (error) { + logger.error({ err: error }, 'GET /api/agents/[id]/diagnostics error'); + return NextResponse.json({ error: 'Failed to fetch diagnostics' }, { status: 500 }); + } +} + +/** High-level KPIs */ +function buildSummary(db: any, agentName: string, workspaceId: number, since: number) { + const tasksDone = (db.prepare( + `SELECT COUNT(*) as c FROM tasks WHERE assigned_to = ? AND workspace_id = ? AND status = 'done' AND updated_at >= ?` + ).get(agentName, workspaceId, since) as any).c; + + const tasksTotal = (db.prepare( + `SELECT COUNT(*) as c FROM tasks WHERE assigned_to = ? AND workspace_id = ?` + ).get(agentName, workspaceId) as any).c; + + const activityCount = (db.prepare( + `SELECT COUNT(*) as c FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ?` + ).get(agentName, workspaceId, since) as any).c; + + const errorCount = (db.prepare( + `SELECT COUNT(*) as c FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? AND type LIKE '%error%'` + ).get(agentName, workspaceId, since) as any).c; + + const errorRate = activityCount > 0 ? Math.round((errorCount / activityCount) * 10000) / 100 : 0; + + return { + tasks_completed: tasksDone, + tasks_total: tasksTotal, + activity_count: activityCount, + error_count: errorCount, + error_rate_percent: errorRate, + }; +} + +/** Task completion breakdown */ +function buildTaskMetrics(db: any, agentName: string, workspaceId: number, since: number) { + const byStatus = db.prepare( + `SELECT status, COUNT(*) as count FROM tasks WHERE assigned_to = ? AND workspace_id = ? GROUP BY status` + ).all(agentName, workspaceId) as Array<{ status: string; count: number }>; + + const byPriority = db.prepare( + `SELECT priority, COUNT(*) as count FROM tasks WHERE assigned_to = ? AND workspace_id = ? GROUP BY priority` + ).all(agentName, workspaceId) as Array<{ priority: string; count: number }>; + + const recentCompleted = db.prepare( + `SELECT id, title, priority, updated_at FROM tasks WHERE assigned_to = ? AND workspace_id = ? AND status = 'done' AND updated_at >= ? ORDER BY updated_at DESC LIMIT 10` + ).all(agentName, workspaceId, since) as any[]; + + // Estimate throughput: tasks completed per day in the window + const windowDays = Math.max((Math.floor(Date.now() / 1000) - since) / 86400, 1); + const completedInWindow = recentCompleted.length; + const throughputPerDay = Math.round((completedInWindow / windowDays) * 100) / 100; + + return { + by_status: Object.fromEntries(byStatus.map(r => [r.status, r.count])), + by_priority: Object.fromEntries(byPriority.map(r => [r.priority, r.count])), + recent_completed: recentCompleted, + throughput_per_day: throughputPerDay, + }; +} + +/** Error frequency and analysis */ +function buildErrorAnalysis(db: any, agentName: string, workspaceId: number, since: number) { + const errorActivities = db.prepare( + `SELECT type, COUNT(*) as count FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? AND (type LIKE '%error%' OR type LIKE '%fail%') GROUP BY type ORDER BY count DESC` + ).all(agentName, workspaceId, since) as Array<{ type: string; count: number }>; + + const recentErrors = db.prepare( + `SELECT id, type, description, data, created_at FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? AND (type LIKE '%error%' OR type LIKE '%fail%') ORDER BY created_at DESC LIMIT 20` + ).all(agentName, workspaceId, since) as any[]; + + return { + by_type: errorActivities, + total: errorActivities.reduce((sum, e) => sum + e.count, 0), + recent: recentErrors.map(e => ({ + ...e, + data: e.data ? JSON.parse(e.data) : null, + })), + }; +} + +/** Activity breakdown with hourly timeline */ +function buildActivityBreakdown(db: any, agentName: string, workspaceId: number, since: number) { + const byType = db.prepare( + `SELECT type, COUNT(*) as count FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? GROUP BY type ORDER BY count DESC` + ).all(agentName, workspaceId, since) as Array<{ type: string; count: number }>; + + const timeline = db.prepare( + `SELECT (created_at / 3600) * 3600 as hour_bucket, COUNT(*) as count FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? GROUP BY hour_bucket ORDER BY hour_bucket ASC` + ).all(agentName, workspaceId, since) as Array<{ hour_bucket: number; count: number }>; + + return { + by_type: byType, + timeline: timeline.map(t => ({ + timestamp: t.hour_bucket, + hour: new Date(t.hour_bucket * 1000).toISOString(), + count: t.count, + })), + }; +} + +/** Multi-period trend comparison for anomaly/trend detection */ +function buildTrends(db: any, agentName: string, workspaceId: number, hours: number) { + const now = Math.floor(Date.now() / 1000); + + // Compare current period vs previous period of same length + const currentSince = now - hours * 3600; + const previousSince = currentSince - hours * 3600; + + const periodMetrics = (since: number, until: number) => { + const activities = (db.prepare( + `SELECT COUNT(*) as c FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? AND created_at < ?` + ).get(agentName, workspaceId, since, until) as any).c; + + const errors = (db.prepare( + `SELECT COUNT(*) as c FROM activities WHERE actor = ? AND workspace_id = ? AND created_at >= ? AND created_at < ? AND (type LIKE '%error%' OR type LIKE '%fail%')` + ).get(agentName, workspaceId, since, until) as any).c; + + const tasksCompleted = (db.prepare( + `SELECT COUNT(*) as c FROM tasks WHERE assigned_to = ? AND workspace_id = ? AND status = 'done' AND updated_at >= ? AND updated_at < ?` + ).get(agentName, workspaceId, since, until) as any).c; + + return { activities, errors, tasks_completed: tasksCompleted }; + }; + + const current = periodMetrics(currentSince, now); + const previous = periodMetrics(previousSince, currentSince); + + const pctChange = (cur: number, prev: number) => { + if (prev === 0) return cur > 0 ? 100 : 0; + return Math.round(((cur - prev) / prev) * 10000) / 100; + }; + + return { + current_period: { since: currentSince, until: now, ...current }, + previous_period: { since: previousSince, until: currentSince, ...previous }, + change: { + activities_pct: pctChange(current.activities, previous.activities), + errors_pct: pctChange(current.errors, previous.errors), + tasks_completed_pct: pctChange(current.tasks_completed, previous.tasks_completed), + }, + alerts: buildTrendAlerts(current, previous), + }; +} + +/** Generate automatic alerts from trend data */ +function buildTrendAlerts(current: { activities: number; errors: number; tasks_completed: number }, previous: { activities: number; errors: number; tasks_completed: number }) { + const alerts: Array<{ level: string; message: string }> = []; + + // Error rate spike + if (current.errors > 0 && previous.errors > 0) { + const errorIncrease = (current.errors - previous.errors) / previous.errors; + if (errorIncrease > 0.5) { + alerts.push({ level: 'warning', message: `Error count increased ${Math.round(errorIncrease * 100)}% vs previous period` }); + } + } else if (current.errors > 3 && previous.errors === 0) { + alerts.push({ level: 'warning', message: `New error pattern: ${current.errors} errors (none in previous period)` }); + } + + // Throughput drop + if (previous.tasks_completed > 0 && current.tasks_completed === 0) { + alerts.push({ level: 'info', message: 'No tasks completed in current period (possible stall)' }); + } else if (previous.tasks_completed > 2 && current.tasks_completed < previous.tasks_completed * 0.5) { + alerts.push({ level: 'info', message: `Task throughput dropped ${Math.round((1 - current.tasks_completed / previous.tasks_completed) * 100)}%` }); + } + + // Activity drop (possible offline) + if (previous.activities > 5 && current.activities < previous.activities * 0.25) { + alerts.push({ level: 'warning', message: `Activity dropped ${Math.round((1 - current.activities / previous.activities) * 100)}% — agent may be stalled` }); + } + + return alerts; +} + +/** Token usage by model */ +function buildTokenMetrics(db: any, agentName: string, workspaceId: number, since: number) { + try { + // session_id on token_usage may store agent name or session key + const byModel = db.prepare( + `SELECT model, SUM(input_tokens) as input_tokens, SUM(output_tokens) as output_tokens, COUNT(*) as request_count FROM token_usage WHERE session_id = ? AND workspace_id = ? AND created_at >= ? GROUP BY model ORDER BY (input_tokens + output_tokens) DESC` + ).all(agentName, workspaceId, since) as Array<{ model: string; input_tokens: number; output_tokens: number; request_count: number }>; + + const total = byModel.reduce((acc, r) => ({ + input_tokens: acc.input_tokens + r.input_tokens, + output_tokens: acc.output_tokens + r.output_tokens, + requests: acc.requests + r.request_count, + }), { input_tokens: 0, output_tokens: 0, requests: 0 }); + + return { + by_model: byModel, + total, + }; + } catch { + // token_usage table may not exist + return { by_model: [], total: { input_tokens: 0, output_tokens: 0, requests: 0 } }; + } +} diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index 3f835f0..1f52638 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -8,6 +8,10 @@ import { requireRole } from '@/lib/auth'; import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, createAgentSchema } from '@/lib/validation'; +import { runOpenClaw } from '@/lib/command'; +import { config as appConfig } from '@/lib/config'; +import { resolveWithin } from '@/lib/paths'; +import path from 'node:path'; /** * GET /api/agents - List all agents with optional filtering @@ -123,6 +127,7 @@ export async function POST(request: NextRequest) { const { name, + openclaw_id, role, session_key, soul_content, @@ -130,9 +135,16 @@ export async function POST(request: NextRequest) { config = {}, template, gateway_config, - write_to_gateway + write_to_gateway, + provision_openclaw_workspace, + openclaw_workspace_path } = body; + const openclawId = (openclaw_id || name || 'agent') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + // Resolve template if specified let finalRole = role; let finalConfig: Record = { ...config }; @@ -158,6 +170,32 @@ export async function POST(request: NextRequest) { if (existingAgent) { return NextResponse.json({ error: 'Agent name already exists' }, { status: 409 }); } + + if (provision_openclaw_workspace) { + if (!appConfig.openclawStateDir) { + return NextResponse.json( + { error: 'OPENCLAW_STATE_DIR is not configured; cannot provision OpenClaw workspace' }, + { status: 500 } + ); + } + + const workspacePath = openclaw_workspace_path + ? path.resolve(openclaw_workspace_path) + : resolveWithin(appConfig.openclawStateDir, path.join('workspaces', openclawId)); + + try { + await runOpenClaw( + ['agents', 'add', openclawId, '--name', name, '--workspace', workspacePath, '--non-interactive'], + { timeoutMs: 20000 } + ); + } catch (provisionError: any) { + logger.error({ err: provisionError, openclawId, workspacePath }, 'OpenClaw workspace provisioning failed'); + return NextResponse.json( + { error: provisionError?.message || 'Failed to provision OpenClaw agent workspace' }, + { status: 502 } + ); + } + } const now = Math.floor(Date.now() / 1000); @@ -215,7 +253,6 @@ export async function POST(request: NextRequest) { // Write to gateway config if requested if (write_to_gateway && finalConfig) { try { - const openclawId = (name || 'agent').toLowerCase().replace(/\s+/g, '-'); await writeAgentToConfig({ id: openclawId, name, diff --git a/src/app/api/cleanup/route.ts b/src/app/api/cleanup/route.ts index c590ea3..3cd13fc 100644 --- a/src/app/api/cleanup/route.ts +++ b/src/app/api/cleanup/route.ts @@ -3,6 +3,7 @@ import { requireRole } from '@/lib/auth' import { getDatabase, logAuditEvent } from '@/lib/db' import { config } from '@/lib/config' import { heavyLimiter } from '@/lib/rate-limit' +import { countStaleGatewaySessions, pruneGatewaySessionsOlderThan } from '@/lib/sessions' interface CleanupResult { table: string @@ -59,6 +60,17 @@ export async function GET(request: NextRequest) { preview.push({ table: 'Token Usage (file)', retention_days: ret.tokenUsage, stale_count: 0, note: 'No token data file' }) } + if (ret.gatewaySessions > 0) { + preview.push({ + table: 'Gateway Session Store', + retention_days: ret.gatewaySessions, + stale_count: countStaleGatewaySessions(ret.gatewaySessions), + note: 'Stored under ~/.openclaw/agents/*/sessions/sessions.json', + }) + } else { + preview.push({ table: 'Gateway Session Store', retention_days: 0, stale_count: 0, note: 'Retention disabled (keep forever)' }) + } + return NextResponse.json({ retention: config.retention, preview }) } @@ -137,6 +149,19 @@ export async function POST(request: NextRequest) { } } + if (ret.gatewaySessions > 0) { + const sessionPrune = dryRun + ? { deleted: countStaleGatewaySessions(ret.gatewaySessions), filesTouched: 0 } + : pruneGatewaySessionsOlderThan(ret.gatewaySessions) + results.push({ + table: 'Gateway Session Store', + deleted: sessionPrune.deleted, + cutoff_date: new Date(Date.now() - ret.gatewaySessions * 86400000).toISOString().split('T')[0], + retention_days: ret.gatewaySessions, + }) + totalDeleted += sessionPrune.deleted + } + if (!dryRun && totalDeleted > 0) { const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown' logAuditEvent({ diff --git a/src/app/api/memory/route.ts b/src/app/api/memory/route.ts index 6615d35..a1aa609 100644 --- a/src/app/api/memory/route.ts +++ b/src/app/api/memory/route.ts @@ -9,6 +9,7 @@ import { readLimiter, mutationLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' const MEMORY_PATH = config.memoryDir +const MEMORY_ALLOWED_PREFIXES = (config.memoryAllowedPrefixes || []).map((p) => p.replace(/\\/g, '/')) // Ensure memory directory exists on startup if (MEMORY_PATH && !existsSync(MEMORY_PATH)) { @@ -24,6 +25,16 @@ interface MemoryFile { children?: MemoryFile[] } +function normalizeRelativePath(value: string): string { + return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '') +} + +function isPathAllowed(relativePath: string): boolean { + if (!MEMORY_ALLOWED_PREFIXES.length) return true + const normalized = normalizeRelativePath(relativePath) + return MEMORY_ALLOWED_PREFIXES.some((prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)) +} + function isWithinBase(base: string, candidate: string): boolean { if (candidate === base) return true return candidate.startsWith(base + sep) @@ -137,12 +148,37 @@ export async function GET(request: NextRequest) { if (!MEMORY_PATH) { return NextResponse.json({ tree: [] }) } + if (MEMORY_ALLOWED_PREFIXES.length) { + const tree: MemoryFile[] = [] + for (const prefix of MEMORY_ALLOWED_PREFIXES) { + const folder = prefix.replace(/\/$/, '') + const fullPath = join(MEMORY_PATH, folder) + if (!existsSync(fullPath)) continue + try { + const stats = await stat(fullPath) + if (!stats.isDirectory()) continue + tree.push({ + path: folder, + name: folder, + type: 'directory', + modified: stats.mtime.getTime(), + children: await buildFileTree(fullPath, folder), + }) + } catch { + // Skip unreadable roots + } + } + return NextResponse.json({ tree }) + } const tree = await buildFileTree(MEMORY_PATH) return NextResponse.json({ tree }) } if (action === 'content' && path) { // Return file content + if (!isPathAllowed(path)) { + return NextResponse.json({ error: 'Path not allowed' }, { status: 403 }) + } if (!MEMORY_PATH) { return NextResponse.json({ error: 'Memory directory not configured' }, { status: 500 }) } @@ -227,7 +263,16 @@ export async function GET(request: NextRequest) { } } - await searchDirectory(MEMORY_PATH) + if (MEMORY_ALLOWED_PREFIXES.length) { + for (const prefix of MEMORY_ALLOWED_PREFIXES) { + const folder = prefix.replace(/\/$/, '') + const fullPath = join(MEMORY_PATH, folder) + if (!existsSync(fullPath)) continue + await searchDirectory(fullPath, folder) + } + } else { + await searchDirectory(MEMORY_PATH) + } return NextResponse.json({ query, @@ -256,6 +301,9 @@ export async function POST(request: NextRequest) { if (!path) { return NextResponse.json({ error: 'Path is required' }, { status: 400 }) } + if (!isPathAllowed(path)) { + return NextResponse.json({ error: 'Path not allowed' }, { status: 403 }) + } if (!MEMORY_PATH) { return NextResponse.json({ error: 'Memory directory not configured' }, { status: 500 }) @@ -316,6 +364,9 @@ export async function DELETE(request: NextRequest) { if (!path) { return NextResponse.json({ error: 'Path is required' }, { status: 400 }) } + if (!isPathAllowed(path)) { + return NextResponse.json({ error: 'Path not allowed' }, { status: 403 }) + } if (!MEMORY_PATH) { return NextResponse.json({ error: 'Memory directory not configured' }, { status: 500 }) diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts index a6be8d4..d88130f 100644 --- a/src/app/api/settings/route.ts +++ b/src/app/api/settings/route.ts @@ -23,6 +23,7 @@ const settingDefinitions: Record line.startsWith('Mem:')) - if (memLine) { - const parts = memLine.split(/\s+/) - status.memory = { - total: parseInt(parts[1]) || 0, - used: parseInt(parts[2]) || 0, - available: parseInt(parts[6]) || 0 + // Memory info (cross-platform) + if (process.platform === 'darwin') { + const totalBytes = os.totalmem() + const freeBytes = os.freemem() + const totalMB = Math.round(totalBytes / (1024 * 1024)) + const usedMB = Math.round((totalBytes - freeBytes) / (1024 * 1024)) + const availableMB = Math.round(freeBytes / (1024 * 1024)) + status.memory = { total: totalMB, used: usedMB, available: availableMB } + } else { + const { stdout: memOutput } = await runCommand('free', ['-m'], { + timeoutMs: 3000 + }) + const memLine = memOutput.split('\n').find(line => line.startsWith('Mem:')) + if (memLine) { + const parts = memLine.split(/\s+/) + status.memory = { + total: parseInt(parts[1]) || 0, + used: parseInt(parts[2]) || 0, + available: parseInt(parts[6]) || 0 + } } } } catch (error) { @@ -414,14 +434,17 @@ async function performHealthCheck() { }) } - // Check disk space + // Check disk space (cross-platform: use df -h / and parse capacity column) try { - const { stdout } = await runCommand('df', ['/', '--output=pcent'], { + const { stdout } = await runCommand('df', ['-h', '/'], { timeoutMs: 3000 }) const lines = stdout.trim().split('\n') const last = lines[lines.length - 1] || '' - const usagePercent = parseInt(last.replace('%', '').trim() || '0') + const parts = last.split(/\s+/) + // On macOS capacity is col 4 ("85%"), on Linux use% is col 4 as well + const pctField = parts.find(p => p.endsWith('%')) || '0%' + const usagePercent = parseInt(pctField.replace('%', '') || '0') health.checks.push({ name: 'Disk Space', @@ -436,15 +459,21 @@ async function performHealthCheck() { }) } - // Check memory usage + // Check memory usage (cross-platform) try { - const { stdout } = await runCommand('free', ['-m'], { timeoutMs: 3000 }) - const lines = stdout.split('\n') - const memLine = lines.find((line) => line.startsWith('Mem:')) - const parts = (memLine || '').split(/\s+/) - const total = parseInt(parts[1] || '0') - const available = parseInt(parts[6] || '0') - const usagePercent = Math.round(((total - available) / total) * 100) + let usagePercent: number + if (process.platform === 'darwin') { + const totalBytes = os.totalmem() + const freeBytes = os.freemem() + usagePercent = Math.round(((totalBytes - freeBytes) / totalBytes) * 100) + } else { + const { stdout } = await runCommand('free', ['-m'], { timeoutMs: 3000 }) + const memLine = stdout.split('\n').find((line) => line.startsWith('Mem:')) + const parts = (memLine || '').split(/\s+/) + const total = parseInt(parts[1] || '0') + const available = parseInt(parts[6] || '0') + usagePercent = Math.round(((total - available) / total) * 100) + } health.checks.push({ name: 'Memory Usage', diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index 70fd2f9..68898e2 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -6,6 +6,7 @@ import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, updateTaskSchema } from '@/lib/validation'; import { resolveMentionRecipients } from '@/lib/mentions'; +import { normalizeTaskUpdateStatus } from '@/lib/task-status'; function formatTicketRef(prefix?: string | null, num?: number | null): string | undefined { if (!prefix || typeof num !== 'number' || !Number.isFinite(num) || num <= 0) return undefined @@ -115,7 +116,7 @@ export async function PUT( const { title, description, - status, + status: requestedStatus, priority, project_id, assigned_to, @@ -125,6 +126,12 @@ export async function PUT( tags, metadata } = body; + const normalizedStatus = normalizeTaskUpdateStatus({ + currentStatus: currentTask.status, + requestedStatus, + assignedTo: assigned_to, + assignedToProvided: assigned_to !== undefined, + }) const now = Math.floor(Date.now() / 1000); const descriptionMentionResolution = description !== undefined @@ -152,15 +159,15 @@ export async function PUT( fieldsToUpdate.push('description = ?'); updateParams.push(description); } - if (status !== undefined) { - if (status === 'done' && !hasAegisApproval(db, taskId, workspaceId)) { + if (normalizedStatus !== undefined) { + if (normalizedStatus === 'done' && !hasAegisApproval(db, taskId, workspaceId)) { return NextResponse.json( { error: 'Aegis approval is required to move task to done.' }, { status: 403 } ) } fieldsToUpdate.push('status = ?'); - updateParams.push(status); + updateParams.push(normalizedStatus); } if (priority !== undefined) { fieldsToUpdate.push('priority = ?'); @@ -240,8 +247,8 @@ export async function PUT( // Track changes and log activities const changes: string[] = []; - if (status && status !== currentTask.status) { - changes.push(`status: ${currentTask.status} → ${status}`); + if (normalizedStatus !== undefined && normalizedStatus !== currentTask.status) { + changes.push(`status: ${currentTask.status} → ${normalizedStatus}`); // Create notification for status change if assigned if (currentTask.assigned_to) { @@ -249,7 +256,7 @@ export async function PUT( currentTask.assigned_to, 'status_change', 'Task Status Updated', - `Task "${currentTask.title}" status changed to ${status}`, + `Task "${currentTask.title}" status changed to ${normalizedStatus}`, 'task', taskId, workspaceId @@ -322,7 +329,7 @@ export async function PUT( priority: currentTask.priority, assigned_to: currentTask.assigned_to }, - newValues: { title, status, priority, assigned_to } + newValues: { title, status: normalizedStatus ?? currentTask.status, priority, assigned_to } }, workspaceId ); diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index bdd1626..09b7883 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -6,6 +6,7 @@ import { mutationLimiter } from '@/lib/rate-limit'; import { logger } from '@/lib/logger'; import { validateBody, createTaskSchema, bulkUpdateTaskStatusSchema } from '@/lib/validation'; import { resolveMentionRecipients } from '@/lib/mentions'; +import { normalizeTaskCreateStatus } from '@/lib/task-status'; function formatTicketRef(prefix?: string | null, num?: number | null): string | undefined { if (!prefix || typeof num !== 'number' || !Number.isFinite(num) || num <= 0) return undefined @@ -163,7 +164,7 @@ export async function POST(request: NextRequest) { const { title, description, - status = 'inbox', + status, priority = 'medium', project_id, assigned_to, @@ -173,6 +174,7 @@ export async function POST(request: NextRequest) { tags = [], metadata = {} } = body; + const normalizedStatus = normalizeTaskCreateStatus(status, assigned_to) // Check for duplicate title const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ? AND workspace_id = ?').get(title, workspaceId); @@ -212,7 +214,7 @@ export async function POST(request: NextRequest) { const dbResult = insertStmt.run( title, description, - status, + normalizedStatus, priority, resolvedProjectId, row.ticket_counter, @@ -234,7 +236,7 @@ export async function POST(request: NextRequest) { // Log activity db_helpers.logActivity('task_created', 'task', taskId, created_by, `Created task: ${title}`, { title, - status, + status: normalizedStatus, priority, assigned_to }, workspaceId); diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index 38ab1a9..eab1399 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -522,7 +522,7 @@ export function Dashboard() { {isLocal ? ( } onNavigate={navigateToPanel} /> ) : ( - } onNavigate={navigateToPanel} /> + } onNavigate={navigateToPanel} /> )} diff --git a/src/components/layout/live-feed.tsx b/src/components/layout/live-feed.tsx index dae3c77..f64420b 100644 --- a/src/components/layout/live-feed.tsx +++ b/src/components/layout/live-feed.tsx @@ -7,6 +7,7 @@ export function LiveFeed() { const { logs, sessions, activities, connection, dashboardMode, toggleLiveFeed } = useMissionControl() const isLocal = dashboardMode === 'local' const [expanded, setExpanded] = useState(true) + const [hasCollapsed, setHasCollapsed] = useState(false) // Combine logs, activities, and (in local mode) session events into a unified feed const sessionItems = isLocal @@ -70,7 +71,7 @@ export function LiveFeed() { } return ( -
+
{/* Header */}
@@ -80,7 +81,7 @@ export function LiveFeed() {
+ {/* Info Banner */} +
+ Agent Memory vs Workspace Memory:{' '} + This tab edits only this agent's private working memory (a scratchpad stored in the database). + To browse or edit all workspace memory files (daily logs, knowledge base, MEMORY.md, etc.), visit the{' '} + Memory Browser page. +
+ {/* Memory Content */}
+ +
)}
diff --git a/src/components/panels/agent-squad-panel-phase3.tsx b/src/components/panels/agent-squad-panel-phase3.tsx index 97b827c..9dad847 100644 --- a/src/components/panels/agent-squad-panel-phase3.tsx +++ b/src/components/panels/agent-squad-panel-phase3.tsx @@ -96,7 +96,14 @@ export function AgentSquadPanelPhase3() { setSyncToast(null) try { const response = await fetch('/api/agents/sync', { method: 'POST' }) + if (response.status === 401) { + window.location.assign('/login?next=%2Fagents') + return + } const data = await response.json() + if (response.status === 403) { + throw new Error('Admin access required for agent sync') + } if (!response.ok) throw new Error(data.error || 'Sync failed') setSyncToast(`Synced ${data.synced} agents (${data.created} new, ${data.updated} updated)`) fetchAgents() @@ -116,7 +123,17 @@ export function AgentSquadPanelPhase3() { if (agents.length === 0) setLoading(true) const response = await fetch('/api/agents') - if (!response.ok) throw new Error('Failed to fetch agents') + if (response.status === 401) { + window.location.assign('/login?next=%2Fagents') + return + } + if (response.status === 403) { + throw new Error('Access denied') + } + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error || 'Failed to fetch agents') + } const data = await response.json() setAgents(data.agents || []) diff --git a/src/components/panels/memory-browser-panel.tsx b/src/components/panels/memory-browser-panel.tsx index 61c9229..48022b5 100644 --- a/src/components/panels/memory-browser-panel.tsx +++ b/src/components/panels/memory-browser-panel.tsx @@ -47,7 +47,7 @@ export function MemoryBrowserPanel() { setMemoryFiles(data.tree || []) // Auto-expand some common directories - setExpandedFolders(new Set(['daily', 'knowledge'])) + setExpandedFolders(new Set(['daily', 'knowledge', 'memory', 'knowledge-base'])) } catch (error) { log.error('Failed to load file tree:', error) } finally { @@ -61,15 +61,14 @@ export function MemoryBrowserPanel() { const getFilteredFiles = () => { if (activeTab === 'all') return memoryFiles - - return memoryFiles.filter(file => { - if (activeTab === 'daily') { - return file.name === 'daily' || file.path.includes('daily/') - } - if (activeTab === 'knowledge') { - return file.name === 'knowledge' || file.path.includes('knowledge/') - } - return true + + const tabPrefixes = activeTab === 'daily' + ? ['daily/', 'memory/'] + : ['knowledge/', 'knowledge-base/'] + + return memoryFiles.filter((file) => { + const normalizedPath = `${file.path.replace(/\\/g, '/')}/` + return tabPrefixes.some((prefix) => normalizedPath.startsWith(prefix)) }) } @@ -731,6 +730,8 @@ function CreateFileModal({ onChange={(e) => setFilePath(e.target.value)} className="w-full px-3 py-2 bg-surface-1 border border-border rounded-md text-foreground focus:outline-none focus:ring-1 focus:ring-primary/50" > + + diff --git a/src/components/panels/office-panel.tsx b/src/components/panels/office-panel.tsx index 521301b..ed0ada5 100644 --- a/src/components/panels/office-panel.tsx +++ b/src/components/panels/office-panel.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useMissionControl, Agent } from '@/store' type ViewMode = 'office' | 'org-chart' +type OrgSegmentMode = 'category' | 'role' | 'status' interface Desk { agent: Agent @@ -75,6 +76,7 @@ export function OfficePanel() { const { agents } = useMissionControl() const [localAgents, setLocalAgents] = useState([]) const [viewMode, setViewMode] = useState('office') + const [orgSegmentMode, setOrgSegmentMode] = useState('category') const [selectedAgent, setSelectedAgent] = useState(null) const [loading, setLoading] = useState(true) @@ -123,6 +125,64 @@ export function OfficePanel() { return groups }, [displayAgents]) + const categoryGroups = useMemo(() => { + const groups = new Map() + const getCategory = (agent: Agent): string => { + const name = (agent.name || '').toLowerCase() + if (name.startsWith('habi-')) return 'Habi Lanes' + if (name.startsWith('ops-')) return 'Ops Automation' + if (name.includes('canary')) return 'Canary' + if (name.startsWith('main')) return 'Core' + if (name.startsWith('remote-')) return 'Remote' + return 'Other' + } + + for (const a of displayAgents) { + const category = getCategory(a) + if (!groups.has(category)) groups.set(category, []) + groups.get(category)!.push(a) + } + + const order = ['Habi Lanes', 'Ops Automation', 'Core', 'Canary', 'Remote', 'Other'] + return new Map( + [...groups.entries()].sort(([a], [b]) => { + const ai = order.indexOf(a) + const bi = order.indexOf(b) + const av = ai === -1 ? Number.MAX_SAFE_INTEGER : ai + const bv = bi === -1 ? Number.MAX_SAFE_INTEGER : bi + if (av !== bv) return av - bv + return a.localeCompare(b) + }) + ) + }, [displayAgents]) + + const statusGroups = useMemo(() => { + const groups = new Map() + for (const a of displayAgents) { + const key = statusLabel[a.status] || a.status + if (!groups.has(key)) groups.set(key, []) + groups.get(key)!.push(a) + } + + const order = ['Working', 'Available', 'Error', 'Away'] + return new Map( + [...groups.entries()].sort(([a], [b]) => { + const ai = order.indexOf(a) + const bi = order.indexOf(b) + const av = ai === -1 ? Number.MAX_SAFE_INTEGER : ai + const bv = bi === -1 ? Number.MAX_SAFE_INTEGER : bi + if (av !== bv) return av - bv + return a.localeCompare(b) + }) + ) + }, [displayAgents]) + + const orgGroups = useMemo(() => { + if (orgSegmentMode === 'role') return roleGroups + if (orgSegmentMode === 'status') return statusGroups + return categoryGroups + }, [categoryGroups, orgSegmentMode, roleGroups, statusGroups]) + if (loading && displayAgents.length === 0) { return (
@@ -237,11 +297,40 @@ export function OfficePanel() {
) : (
- {[...roleGroups.entries()].map(([role, members]) => ( -
+
+
+ Segmented by{' '} + + {orgSegmentMode === 'category' ? 'category' : orgSegmentMode} + +
+
+ + + +
+
+ + {[...orgGroups.entries()].map(([segment, members]) => ( +
-

{role}

+

{segment}

({members.length})
diff --git a/src/components/panels/settings-panel.tsx b/src/components/panels/settings-panel.tsx index 08912c8..eacf25f 100644 --- a/src/components/panels/settings-panel.tsx +++ b/src/components/panels/settings-panel.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { useMissionControl } from '@/store' +import { useNavigateToPanel } from '@/lib/navigation' interface Setting { key: string @@ -24,6 +25,7 @@ const categoryOrder = ['general', 'retention', 'gateway', 'custom'] export function SettingsPanel() { const { currentUser } = useMissionControl() + const navigateToPanel = useNavigateToPanel() const [settings, setSettings] = useState([]) const [grouped, setGrouped] = useState>({}) const [loading, setLoading] = useState(true) @@ -43,12 +45,17 @@ export function SettingsPanel() { const fetchSettings = useCallback(async () => { try { const res = await fetch('/api/settings') + if (res.status === 401) { + window.location.assign('/login?next=%2Fsettings') + return + } if (res.status === 403) { setError('Admin access required') return } if (!res.ok) { - setError('Failed to load settings') + const data = await res.json().catch(() => ({})) + setError(data.error || 'Failed to load settings') return } const data = await res.json() @@ -180,6 +187,21 @@ export function SettingsPanel() {
+ {/* Workspace Info */} + {currentUser?.role === 'admin' && ( +
+ Workspace Management:{' '} + To create or manage workspaces (tenant instances), go to the{' '} + {' '} + panel under Admin > Super Admin in the sidebar. From there you can create new client instances, manage tenants, and monitor provisioning jobs. +
+ )} + {/* Feedback */} {feedback && (
- +
+ + +
@@ -452,17 +460,21 @@ export function SuperAdminPanel() {
)} -
- - {createExpanded && ( + {createExpanded && ( +
+
+

Create New Workspace

+ +
- Add a new workspace/client instance here. Fill the form below and click Create + Queue. + Fill in the workspace details below and click Create + Queue to provision a new client instance.
{gatewayLoadError && (
@@ -540,8 +552,8 @@ export function SuperAdminPanel() {
- )}
+ )}
diff --git a/src/components/panels/task-board-panel.tsx b/src/components/panels/task-board-panel.tsx index e7dc395..95df955 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -217,7 +217,7 @@ function MentionTextarea({ className={className} /> {open && filtered.length > 0 && ( -
+
{filtered.map((option, index) => (