diff --git a/apps/server/openapi.json b/apps/server/openapi.json index b9e4cc5..78f7eed 100644 --- a/apps/server/openapi.json +++ b/apps/server/openapi.json @@ -6,6 +6,18 @@ }, "components": { "schemas": { + "AgentsConfigSnapshot": { + "type": "object", + "properties": { + "AGENTS_PROVIDER": { + "type": "string", + "enum": ["bailian", "qoder", "ark", "claude"] + } + }, + "additionalProperties": { + "type": "string" + } + }, "ErrorResponse": { "type": "object", "properties": { @@ -21,13 +33,97 @@ }, "required": ["error"] }, + "AgentsConfigReady": { + "type": "object", + "properties": { + "ready": { + "type": "boolean" + }, + "provider": { + "type": "string", + "enum": ["bailian", "qoder", "ark", "claude"] + } + }, + "required": ["ready"] + }, + "AgentsConfig": { + "type": "object", + "properties": { + "AGENTS_PROVIDER": { + "type": "string", + "enum": ["bailian", "qoder", "ark", "claude"] + } + }, + "required": ["AGENTS_PROVIDER"], + "additionalProperties": { + "type": "string" + } + }, + "SaveAgentsConfigBody": { + "type": "object", + "properties": { + "AGENTS_PROVIDER": { + "type": "string", + "enum": ["bailian", "qoder", "ark", "claude"] + } + }, + "required": ["AGENTS_PROVIDER"], + "additionalProperties": { + "type": "string" + } + }, "SessionListResponse": { "type": "object", "properties": { "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session" + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "title": { + "type": "string" + }, + "agent": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "additionalProperties": { + "nullable": true + } + }, + "environment_id": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["session_id"] } }, "next_page_token": { @@ -37,174 +133,498 @@ }, "required": ["data"] }, - "Session": { + "SessionDetailResponse": { "type": "object", "properties": { - "session_id": { - "type": "string" - }, - "status": { - "type": "string" - }, - "title": { - "type": "string" - }, - "agent": { + "session": { "type": "object", "properties": { - "agent_id": { + "session_id": { "type": "string" }, - "name": { + "status": { "type": "string" }, - "version": { - "type": "number" + "title": { + "type": "string" + }, + "agent": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "additionalProperties": { + "nullable": true + } + }, + "environment_id": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, - "additionalProperties": { - "nullable": true - } + "required": ["session_id"] }, - "created_at": { - "type": "string" + "events": { + "type": "array", + "items": { + "type": "object", + "properties": { + "event_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "role": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "content": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "text": { + "type": "string" + }, + "data": { + "nullable": true + } + }, + "required": ["type"], + "additionalProperties": { + "nullable": true + } + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "nullable": true + } + }, + "is_error": { + "type": "boolean", + "nullable": true + }, + "code": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + } + }, + "required": ["type"] + } }, - "updated_at": { + "events_next_page_token": { + "type": "string", + "nullable": true + } + }, + "required": ["session", "events"] + }, + "SessionDeleteResponse": { + "type": "object", + "properties": { + "session_id": { "type": "string" }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "deleted": { + "type": "boolean" } }, - "required": ["session_id"] + "required": ["session_id", "deleted"] }, - "SessionDetailResponse": { + "SessionEventsPageResponse": { "type": "object", "properties": { - "session": { - "$ref": "#/components/schemas/Session" - }, "events": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionEvent" + "type": "object", + "properties": { + "event_id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "role": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "content": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "text": { + "type": "string" + }, + "data": { + "nullable": true + } + }, + "required": ["type"], + "additionalProperties": { + "nullable": true + } + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + "nullable": true + } + }, + "is_error": { + "type": "boolean", + "nullable": true + }, + "code": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + } + }, + "required": ["type"] } + }, + "events_next_page_token": { + "type": "string", + "nullable": true } }, - "required": ["session", "events"] + "required": ["events"] }, - "SessionEvent": { + "ProviderFileInfo": { "type": "object", "properties": { - "event_id": { + "id": { "type": "string" }, - "type": { + "filename": { "type": "string" }, - "role": { + "mime_type": { "type": "string" }, + "size_bytes": { + "type": "number" + }, "created_at": { "type": "string" }, - "content": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "text": { - "type": "string" - }, - "data": { - "nullable": true - } - }, - "required": ["type"], - "additionalProperties": { - "nullable": true - } - } + "downloadable": { + "type": "boolean" }, - "metadata": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "status": { + "type": "string" }, - "is_error": { - "type": "boolean", - "nullable": true + "purpose": { + "type": "string" }, - "code": { + "available": { + "type": "boolean" + } + }, + "required": ["id", "filename", "mime_type", "size_bytes", "created_at"] + }, + "ProviderSkillInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "source": { "type": "string", - "nullable": true + "enum": ["custom", "official"] }, - "message": { + "status": { "type": "string", - "nullable": true + "enum": ["checking", "active", "rejected", "deleted"] + }, + "latest_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" } }, - "required": ["type"] + "required": ["id", "name", "source", "status"] } }, "parameters": {} }, "paths": { - "/api/agents": { + "/api/config": { "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": false, - "name": "agentId", - "in": "query" - } - ], "responses": { "200": { - "description": "List agents with readiness", + "description": "Read local OpenAgentPack playground config (~/.agents/config.json)", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "agents": { - "type": "array", - "items": { - "type": "object", - "properties": { - "agent": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "agentName": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "description": { - "type": "string" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": { + "$ref": "#/components/schemas/AgentsConfigSnapshot" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "put": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SaveAgentsConfigBody" + } + } + } + }, + "responses": { + "200": { + "description": "Save local OpenAgentPack playground config (~/.agents/config.json)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentsConfig" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/config/ready": { + "get": { + "responses": { + "200": { + "description": "Whether runtime provider credentials are configured in the server process", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentsConfigReady" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/agents": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": false, + "name": "agentId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List agents with readiness", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "agent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "agentName": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "description": { + "type": "string" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { "nullable": true } } @@ -299,7 +719,8 @@ "memory_store", "skill", "agent", - "deployment" + "deployment", + "file" ] }, "name": { @@ -322,7 +743,15 @@ "properties": { "type": { "type": "string", - "enum": ["environment", "vault", "memory_store", "skill", "agent", "deployment"] + "enum": [ + "environment", + "vault", + "memory_store", + "skill", + "agent", + "deployment", + "file" + ] }, "name": { "type": "string" @@ -354,7 +783,8 @@ "memory_store", "skill", "agent", - "deployment" + "deployment", + "file" ] }, "name": { @@ -398,7 +828,8 @@ "memory_store", "skill", "agent", - "deployment" + "deployment", + "file" ] }, "name": { @@ -471,34 +902,2002 @@ } } }, - "/api/sessions": { + "/api/cloud-agents": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": false, + "name": "prefix", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List raw cloud agents (the resource center's source of truth)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "model": { + "nullable": true + }, + "system": { + "type": "string" + }, + "tools": { + "nullable": true + }, + "skills": { + "nullable": true + }, + "mcp_servers": { + "nullable": true + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "version": { + "type": "number" + }, + "type": { + "type": "string" + }, + "workspace_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "archived_at": { + "type": "string", + "nullable": true + } + }, + "required": ["id"] + } + } + }, + "required": ["agents"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/cloud-agents/{agentId}/archive": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "agentId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Archive a cloud agent (soft delete → status=archived)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["ok"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/cloud-agents/{agentId}": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "agentId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "model": { + "type": "string", + "minLength": 1 + } + }, + "required": ["model"] + } + } + } + }, + "responses": { + "200": { + "description": "Update a playbook agent's config (model switch → sync-override)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["ok"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/environments": { + "get": { + "responses": { + "200": { + "description": "List raw cloud environments (the shared base sandbox resource)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "environments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "config": { + "nullable": true + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "scope": { + "type": "string" + }, + "version": { + "type": "number" + }, + "type": { + "type": "string" + }, + "workspace_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "archived_at": { + "type": "string", + "nullable": true + } + }, + "required": ["id"] + } + } + }, + "required": ["environments"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a base cloud environment (cloud + unrestricted networking)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "environment": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": ["id", "type"] + } + }, + "required": ["environment"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/environments/{environmentId}": { + "delete": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "environmentId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Delete a cloud environment by remote id", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": ["id", "type"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/vaults": { + "get": { + "responses": { + "200": { + "description": "List raw cloud vaults (the shared credential store resource)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vaults": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "created_at": { + "type": "string", + "nullable": true + }, + "updated_at": { + "type": "string", + "nullable": true + }, + "archived_at": { + "type": "string", + "nullable": true + } + }, + "required": ["id"] + } + } + }, + "required": ["vaults"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "key": { + "type": "string", + "minLength": 1 + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "Create a base cloud vault holding the user-supplied DASHSCOPE_API_KEY", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": ["id", "type"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/vaults/{vaultId}": { + "delete": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "vaultId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Delete a cloud vault by remote id", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": ["id", "type"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions": { + "get": { + "parameters": [ + { + "schema": { + "type": "integer", + "nullable": true + }, + "required": false, + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "agentId", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "pageToken", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List sessions for an agent", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionListResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentId": { + "type": "string" + }, + "prompt": { + "type": "string", + "minLength": 1 + }, + "environmentId": { + "type": "string", + "minLength": 1 + }, + "vaultIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fileId": { + "type": "string" + }, + "mountPath": { + "type": "string" + } + }, + "required": ["fileId", "mountPath"] + } + }, + "model": { + "type": "string" + } + }, + "required": ["agentId", "prompt", "environmentId"] + } + } + } + }, + "responses": { + "201": { + "description": "Session created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDetailResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/{sessionId}": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "sessionId", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "agentId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Session detail with events", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDetailResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "sessionId", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "agentId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Session deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDeleteResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/{sessionId}/events": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "sessionId", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "agentId", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "pageToken", + "in": "query" + }, + { + "schema": { + "type": "integer", + "nullable": true + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Paginated session events (newest page first; pass pageToken for older pages)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionEventsPageResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/{sessionId}/messages": { + "post": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "sessionId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentId": { + "type": "string" + }, + "message": { + "type": "string", + "minLength": 1 + } + }, + "required": ["message"] + } + } + } + }, + "responses": { + "200": { + "description": "Message sent; updated session with events", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDetailResponse" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/{sessionId}/stream": { + "get": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "sessionId", + "in": "path" + }, + { + "schema": { + "type": "integer", + "nullable": true + }, + "required": false, + "name": "after", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Stream session events as Server-Sent Events", + "content": { + "text/event-stream": { + "schema": { + "type": "string", + "description": "SSE frames with event types \"event\", \"done\", and \"ping\"." + } + } + } + }, + "204": { + "description": "No active event buffer; caller should fetch the session detail once" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/files": { + "post": { + "operationId": "uploadFile", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + }, + "required": ["file"] + } + } + } + }, + "responses": { + "201": { + "description": "Upload a workspace file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderFileInfo" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "File exceeds the upload size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "get": { + "operationId": "listFiles", + "responses": { + "200": { + "description": "List workspace files", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderFileInfo" + } + } + }, + "required": ["files"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/files/status": { + "post": { + "operationId": "getFileStatuses", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fileIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["fileIds"] + } + } + } + }, + "responses": { + "200": { + "description": "Get file scan statuses", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "available": { + "type": "boolean" + } + }, + "required": ["id"] + } + } + }, + "required": ["files"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/files/{id}/download": { + "get": { + "operationId": "downloadFile", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Resolve a short-lived file download URL", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "expires_at": { + "type": "string" + } + }, + "required": ["url"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/files/{id}": { + "delete": { + "operationId": "deleteFile", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1 + }, + "required": true, + "name": "id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "File deleted" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/skills/upload-file": { + "post": { + "operationId": "uploadSkillFile", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + }, + "required": ["file"] + } + } + } + }, + "responses": { + "201": { + "description": "Upload a skill archive as a file pending audit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderFileInfo" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Skill archive exceeds the upload size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/skills": { + "post": { + "operationId": "createSkill", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fileId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["fileId"] + } + } + } + }, + "responses": { + "201": { + "description": "Create a skill from an audited file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderSkillInfo" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, "get": { + "operationId": "listSkills", "parameters": [ { "schema": { - "type": "integer", - "nullable": true - }, - "required": false, - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string" + "type": "string", + "enum": ["custom", "official"] }, "required": false, - "name": "agentId", + "name": "source", "in": "query" } ], "responses": { "200": { - "description": "List sessions for an agent", + "description": "List custom or official skills", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionListResponse" + "type": "object", + "properties": { + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderSkillInfo" + } + } + }, + "required": ["skills"] } } } @@ -544,37 +2943,46 @@ } } } - }, + } + }, + "/api/skills/warm": { "post": { + "operationId": "warmSkill", "requestBody": { + "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { - "agentId": { - "type": "string" - }, - "prompt": { + "name": { "type": "string", "minLength": 1 }, - "title": { - "type": "string" + "url": { + "type": "string", + "minLength": 1 } }, - "required": ["agentId", "prompt"] + "required": ["name", "url"] } } } }, "responses": { - "201": { - "description": "Session created", + "200": { + "description": "Warm a custom skill until it is active", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionDetailResponse" + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["ok"] } } } @@ -622,33 +3030,54 @@ } } }, - "/api/sessions/{sessionId}": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "sessionId", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "agentId", - "in": "query" + "/api/skills/status": { + "post": { + "operationId": "getSkillStatuses", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "skillIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["skillIds"] + } + } } - ], + }, "responses": { "200": { - "description": "Session detail with events", + "description": "Get skill scan statuses", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionDetailResponse" + "type": "object", + "properties": { + "skills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["checking", "active", "rejected", "deleted"] + } + }, + "required": ["id"] + } + } + }, + "required": ["skills"] } } } @@ -696,47 +3125,23 @@ } } }, - "/api/sessions/{sessionId}/messages": { - "post": { + "/api/skills/{id}": { + "delete": { + "operationId": "deleteSkill", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "minLength": 1 }, "required": true, - "name": "sessionId", + "name": "id", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agentId": { - "type": "string" - }, - "message": { - "type": "string", - "minLength": 1 - } - }, - "required": ["message"] - } - } - } - }, "responses": { - "200": { - "description": "Message sent; updated session with events", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionDetailResponse" - } - } - } + "204": { + "description": "Skill deleted" }, "400": { "description": "Bad request", @@ -781,42 +3186,43 @@ } } }, - "/api/sessions/{sessionId}/stream": { + "/api/models": { "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "sessionId", - "in": "path" - }, - { - "schema": { - "type": "integer", - "nullable": true - }, - "required": false, - "name": "after", - "in": "query" - } - ], "responses": { "200": { - "description": "Stream session events as Server-Sent Events", + "description": "List the active provider's available models", "content": { - "text/event-stream": { + "application/json": { "schema": { - "type": "string", - "description": "SSE frames with event types \"event\", \"done\", and \"ping\"." + "type": "object", + "properties": { + "models": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "is_enabled": { + "type": "boolean" + }, + "is_new": { + "type": "boolean" + } + }, + "required": ["id", "display_name"] + } + } + }, + "required": ["models"] } } } }, - "204": { - "description": "No active event buffer; caller should fetch the session detail once" - }, "400": { "description": "Bad request", "content": { diff --git a/apps/server/package.json b/apps/server/package.json index 172beaa..8a823a0 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -12,7 +12,7 @@ "start": "bun run src/index.ts", "test": "bun test", "typecheck": "tsc --noEmit", - "gen:openapi": "bun run scripts/emit-openapi.ts" + "gen:openapi": "bun run scripts/emit-openapi.ts && biome format --write openapi.json" }, "dependencies": { "@hono/zod-openapi": "^1.4.0", diff --git a/apps/server/scripts/emit-openapi.ts b/apps/server/scripts/emit-openapi.ts index 129ee44..bf9b19b 100644 --- a/apps/server/scripts/emit-openapi.ts +++ b/apps/server/scripts/emit-openapi.ts @@ -6,7 +6,7 @@ if (!res.ok) { } const doc = await res.json(); -const output = `${JSON.stringify(doc, null, 2)}\n`; +const output = `${JSON.stringify(doc, null, "\t")}\n`; await Bun.write(new URL("../openapi.json", import.meta.url), output); console.log("Wrote apps/server/openapi.json"); diff --git a/apps/server/src/routes/files.ts b/apps/server/src/routes/files.ts index cd94e85..1fe9d13 100644 --- a/apps/server/src/routes/files.ts +++ b/apps/server/src/routes/files.ts @@ -1,5 +1,14 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; -import { jsonError } from "@/lib/http-error"; +import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; +import { ErrorResponseSchema, errorResponses } from "@/schemas/common"; +import { + FileDownloadResponseSchema, + FileParamsSchema, + FileStatusesBodySchema, + FileStatusesResponseSchema, + FilesResponseSchema, + ProviderFileInfoSchema, + UploadFileFormSchema, +} from "@/schemas/files"; import { deleteUserFile, getUserFileDownloadUrl, @@ -13,27 +22,40 @@ export const filesRoute = new OpenAPIHono(); // Max upload size enforced at the edge (the provider also caps via nacos cmaMaxFileSize). const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; -/** - * POST /api/files — multipart/form-data with field `file`. Streams the bytes to the provider's - * Files API via the SDK and returns the file metadata ({ id, filename, mime_type, size_bytes, created_at }). - * Plain handler (not zod-openapi) so multipart parsing stays under our control. - */ -filesRoute.post("/files", async (c) => { - let body: Record; - try { - body = await c.req.parseBody(); - } catch (error) { - return jsonError(error, 400); - } - const file = body.file; - if (!(file instanceof File) || file.size === 0) { - return c.json({ error: { message: "file is required" } }, 400); - } - if (file.size > MAX_UPLOAD_BYTES) { - return c.json({ error: { message: "file too large" } }, 413); - } +const uploadFileRoute = createRoute({ + method: "post", + path: "/files", + operationId: "uploadFile", + request: { + body: { + required: true, + content: { "multipart/form-data": { schema: UploadFileFormSchema } }, + }, + }, + responses: { + 201: { + description: "Upload a workspace file", + content: { "application/json": { schema: ProviderFileInfoSchema } }, + }, + 413: { + description: "File exceeds the upload size limit", + content: { "application/json": { schema: ErrorResponseSchema } }, + }, + ...errorResponses, + }, +}); + +filesRoute.openapi( + uploadFileRoute, + async (c) => { + const { file } = c.req.valid("form"); + if (file.size === 0) { + return c.json({ error: { message: "file is required" } }, 400); + } + if (file.size > MAX_UPLOAD_BYTES) { + return c.json({ error: { message: "file too large" } }, 413); + } - try { const content = new Uint8Array(await file.arrayBuffer()); const info = await uploadUserFile({ content, @@ -41,81 +63,94 @@ filesRoute.post("/files", async (c) => { mimeType: file.type || undefined, }); return c.json(info, 201); - } catch (error) { - return jsonError(error); - } + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "file is required" } }, 400); + }, +); + +const listFilesRoute = createRoute({ + method: "get", + path: "/files", + operationId: "listFiles", + responses: { + 200: { + description: "List workspace files", + content: { "application/json": { schema: FilesResponseSchema } }, + }, + ...errorResponses, + }, }); -/** - * GET /api/files — list workspace user-uploaded files (newest first), as - * `{ files: [{ id, filename, mime_type, size_bytes, created_at, downloadable, status?, available }] }`. - * The OpenAPI list omits `status` (@JsonIgnore) but carries `downloadable`; the server enriches - * `status` + `available` via @openagentpack/sdk/file-lifecycle before responding. - */ -filesRoute.get("/files", async (c) => { - try { - const files = await listUserFiles(); - return c.json({ files }); - } catch (error) { - return jsonError(error); - } +filesRoute.openapi(listFilesRoute, async (c) => { + const files = await listUserFiles(); + return c.json({ files }, 200); +}); + +const getFileStatusesRoute = createRoute({ + method: "post", + path: "/files/status", + operationId: "getFileStatuses", + request: { + body: { + required: true, + content: { "application/json": { schema: FileStatusesBodySchema } }, + }, + }, + responses: { + 200: { + description: "Get file scan statuses", + content: { "application/json": { schema: FileStatusesResponseSchema } }, + }, + ...errorResponses, + }, +}); + +filesRoute.openapi( + getFileStatusesRoute, + async (c) => { + const { fileIds } = c.req.valid("json"); + const files = await getUserFileStatuses(fileIds); + return c.json({ files }, 200); + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "fileIds (string[]) is required" } }, 400); + }, +); + +const downloadFileRoute = createRoute({ + method: "get", + path: "/files/{id}/download", + operationId: "downloadFile", + request: { params: FileParamsSchema }, + responses: { + 200: { + description: "Resolve a short-lived file download URL", + content: { "application/json": { schema: FileDownloadResponseSchema } }, + }, + ...errorResponses, + }, }); -/** - * POST /api/files/status — body `{ fileIds: string[] }`. Returns each file's normalized scan - * `status` and bindability (`{ files: [{ id, status, available }] }`). - */ -filesRoute.post("/files/status", async (c) => { - let body: { fileIds?: unknown }; - try { - body = await c.req.json(); - } catch (error) { - return jsonError(error, 400); - } - const fileIds = body.fileIds; - if (!Array.isArray(fileIds) || fileIds.some((id) => typeof id !== "string")) { - return c.json({ error: { message: "fileIds (string[]) is required" } }, 400); - } - try { - const files = await getUserFileStatuses(fileIds as string[]); - return c.json({ files }); - } catch (error) { - return jsonError(error); - } +filesRoute.openapi(downloadFileRoute, async (c) => { + const { id } = c.req.valid("param"); + const result = await getUserFileDownloadUrl(id); + return c.json(result, 200); }); -/** - * GET /api/files/:id/download — resolves a short-lived presigned download URL for a file - * (typically an agent-delivered artifact), as `{ url, expires_at }`. The webui fetches this on - * demand (each click), so the URL never goes stale in the UI. Errors if the provider has no - * download endpoint (bailian/claude). - */ -filesRoute.get("/files/:id/download", async (c) => { - const id = c.req.param("id"); - if (!id) { - return c.json({ error: { message: "file id is required" } }, 400); - } - try { - const result = await getUserFileDownloadUrl(id); - return c.json(result); - } catch (error) { - return jsonError(error); - } +const deleteFileRoute = createRoute({ + method: "delete", + path: "/files/{id}", + operationId: "deleteFile", + request: { params: FileParamsSchema }, + responses: { + 204: { description: "File deleted" }, + ...errorResponses, + }, }); -/** - * DELETE /api/files/:id — removes a previously uploaded file from the provider's Files API. - * Used when the composer's upload chip is dismissed so the backend doesn't accumulate orphans. - */ -filesRoute.delete("/files/:id", async (c) => { - const id = c.req.param("id"); - if (!id) { - return c.json({ error: { message: "file id is required" } }, 400); - } - try { - await deleteUserFile(id); - return c.body(null, 204); - } catch (error) { - return jsonError(error); - } +filesRoute.openapi(deleteFileRoute, async (c) => { + const { id } = c.req.valid("param"); + await deleteUserFile(id); + return c.body(null, 204); }); diff --git a/apps/server/src/routes/skills.ts b/apps/server/src/routes/skills.ts index 15118be..f2f7f82 100644 --- a/apps/server/src/routes/skills.ts +++ b/apps/server/src/routes/skills.ts @@ -1,5 +1,18 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; -import { jsonError } from "@/lib/http-error"; +import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; +import { ErrorResponseSchema, errorResponses } from "@/schemas/common"; +import { ProviderFileInfoSchema } from "@/schemas/files"; +import { + CreateSkillBodySchema, + ProviderSkillInfoSchema, + SkillParamsSchema, + SkillStatusesBodySchema, + SkillStatusesResponseSchema, + SkillsQuerySchema, + SkillsResponseSchema, + UploadSkillFileFormSchema, + WarmSkillBodySchema, + WarmSkillResponseSchema, +} from "@/schemas/skills"; import { createUserSkillFromFile, deleteUserSkill, @@ -13,125 +26,182 @@ export const skillsRoute = new OpenAPIHono(); const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; -// POST /api/skills/upload-file — multipart/form-data field `file` (.zip). Phase 1: upload the zip to -// the Files API and return the file record (still `checking`). The webui polls file status, then calls -// POST /api/skills to create the skill once available. Plain handler so multipart stays manual. -skillsRoute.post("/skills/upload-file", async (c) => { - let body: Record; - try { - body = await c.req.parseBody(); - } catch (error) { - return jsonError(error, 400); - } - const file = body.file; - if (!(file instanceof File) || file.size === 0) { - return c.json({ error: { message: "file is required" } }, 400); - } - if (!file.name.toLowerCase().endsWith(".zip")) { - return c.json({ error: { message: "skill file must be a .zip" } }, 400); - } - if (file.size > MAX_UPLOAD_BYTES) { - return c.json({ error: { message: "file too large" } }, 413); - } - try { +const uploadSkillFileRoute = createRoute({ + method: "post", + path: "/skills/upload-file", + operationId: "uploadSkillFile", + request: { + body: { + required: true, + content: { "multipart/form-data": { schema: UploadSkillFileFormSchema } }, + }, + }, + responses: { + 201: { + description: "Upload a skill archive as a file pending audit", + content: { "application/json": { schema: ProviderFileInfoSchema } }, + }, + 413: { + description: "Skill archive exceeds the upload size limit", + content: { "application/json": { schema: ErrorResponseSchema } }, + }, + ...errorResponses, + }, +}); + +skillsRoute.openapi( + uploadSkillFileRoute, + async (c) => { + const { file } = c.req.valid("form"); + if (file.size === 0) return c.json({ error: { message: "file is required" } }, 400); + if (!file.name.toLowerCase().endsWith(".zip")) { + return c.json({ error: { message: "skill file must be a .zip" } }, 400); + } + if (file.size > MAX_UPLOAD_BYTES) return c.json({ error: { message: "file too large" } }, 413); + const content = new Uint8Array(await file.arrayBuffer()); const info = await uploadUserSkillFile({ content, filename: file.name }); return c.json(info, 201); - } catch (error) { - return jsonError(error); - } + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "file is required" } }, 400); + }, +); + +const createSkillRoute = createRoute({ + method: "post", + path: "/skills", + operationId: "createSkill", + request: { + body: { + required: true, + content: { "application/json": { schema: CreateSkillBodySchema } }, + }, + }, + responses: { + 201: { + description: "Create a skill from an audited file", + content: { "application/json": { schema: ProviderSkillInfoSchema } }, + }, + ...errorResponses, + }, }); -// POST /api/skills — JSON { fileId } from an already-uploaded, available file. Phase 2: create the -// skill (non-blocking; returns the initial scan status). The webui gates this on file availability. -skillsRoute.post("/skills", async (c) => { - let body: { fileId?: unknown }; - try { - body = await c.req.json(); - } catch (error) { - return jsonError(error, 400); - } - const fileId = body.fileId; - if (typeof fileId !== "string" || fileId.length === 0) { - return c.json({ error: { message: "fileId is required" } }, 400); - } - try { +skillsRoute.openapi( + createSkillRoute, + async (c) => { + const { fileId } = c.req.valid("json"); const info = await createUserSkillFromFile(fileId); return c.json(info, 201); - } catch (error) { - return jsonError(error); - } + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "fileId is required" } }, 400); + }, +); + +const warmSkillRoute = createRoute({ + method: "post", + path: "/skills/warm", + operationId: "warmSkill", + request: { + body: { + required: true, + content: { "application/json": { schema: WarmSkillBodySchema } }, + }, + }, + responses: { + 200: { + description: "Warm a custom skill until it is active", + content: { "application/json": { schema: WarmSkillResponseSchema } }, + }, + ...errorResponses, + }, }); -// POST /api/skills/warm — JSON { name, url }. Pre-provision a seed custom skill (download → upload → -// scan to active) ahead of first click, deduped by the provider-unique name. The slow 3–5 min scan -// happens here, off the user's critical path. Best-effort: the lazy first-click path still covers a -// failure. Returns 200 once the skill is active. -skillsRoute.post("/skills/warm", async (c) => { - let body: { name?: unknown; url?: unknown }; - try { - body = await c.req.json(); - } catch (error) { - return jsonError(error, 400); - } - const { name, url } = body; - if (typeof name !== "string" || !name || typeof url !== "string" || !url) { - return c.json({ error: { message: "name and url are required" } }, 400); - } - try { +skillsRoute.openapi( + warmSkillRoute, + async (c) => { + const { name, url } = c.req.valid("json"); await warmSkillByUrl(name, url); return c.json({ ok: true } as const, 200); - } catch (error) { - return jsonError(error); - } + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "name and url are required" } }, 400); + }, +); + +const listSkillsRoute = createRoute({ + method: "get", + path: "/skills", + operationId: "listSkills", + request: { query: SkillsQuerySchema }, + responses: { + 200: { + description: "List custom or official skills", + content: { "application/json": { schema: SkillsResponseSchema } }, + }, + ...errorResponses, + }, }); -// GET /api/skills?source=custom|official — list skills from the chosen catalog (custom = -// workspace-uploaded, default; official = the provider's built-in catalog). Project isolation -// (name prefix) applied by webui. -skillsRoute.get("/skills", async (c) => { - const source = c.req.query("source"); - if (source !== undefined && source !== "custom" && source !== "official") { - return c.json({ error: { message: "source must be 'custom' or 'official'" } }, 400); - } - try { +skillsRoute.openapi( + listSkillsRoute, + async (c) => { + const { source } = c.req.valid("query"); const skills = await listUserSkills(source); - return c.json({ skills }); - } catch (error) { - return jsonError(error); - } + return c.json({ skills }, 200); + }, + (result, c) => { + if (!result.success) { + return c.json({ error: { message: "source must be 'custom' or 'official'" } }, 400); + } + }, +); + +const getSkillStatusesRoute = createRoute({ + method: "post", + path: "/skills/status", + operationId: "getSkillStatuses", + request: { + body: { + required: true, + content: { "application/json": { schema: SkillStatusesBodySchema } }, + }, + }, + responses: { + 200: { + description: "Get skill scan statuses", + content: { "application/json": { schema: SkillStatusesResponseSchema } }, + }, + ...errorResponses, + }, }); -// POST /api/skills/status — body { skillIds: string[] } → per-skill scan status for polling. -skillsRoute.post("/skills/status", async (c) => { - let body: { skillIds?: unknown }; - try { - body = await c.req.json(); - } catch (error) { - return jsonError(error, 400); - } - const skillIds = body.skillIds; - if (!Array.isArray(skillIds) || skillIds.some((id) => typeof id !== "string")) { - return c.json({ error: { message: "skillIds (string[]) is required" } }, 400); - } - try { - const skills = await getUserSkillStatuses(skillIds as string[]); - return c.json({ skills }); - } catch (error) { - return jsonError(error); - } +skillsRoute.openapi( + getSkillStatusesRoute, + async (c) => { + const { skillIds } = c.req.valid("json"); + const skills = await getUserSkillStatuses(skillIds); + return c.json({ skills }, 200); + }, + (result, c) => { + if (!result.success) return c.json({ error: { message: "skillIds (string[]) is required" } }, 400); + }, +); + +const deleteSkillRoute = createRoute({ + method: "delete", + path: "/skills/{id}", + operationId: "deleteSkill", + request: { params: SkillParamsSchema }, + responses: { + 204: { description: "Skill deleted" }, + ...errorResponses, + }, }); -// DELETE /api/skills/:id — remove a custom skill from the provider's Skills API. -skillsRoute.delete("/skills/:id", async (c) => { - const id = c.req.param("id"); - if (!id) { - return c.json({ error: { message: "skill id is required" } }, 400); - } - try { - await deleteUserSkill(id); - return c.body(null, 204); - } catch (error) { - return jsonError(error); - } +skillsRoute.openapi(deleteSkillRoute, async (c) => { + const { id } = c.req.valid("param"); + await deleteUserSkill(id); + return c.body(null, 204); }); diff --git a/apps/server/src/schemas/config.ts b/apps/server/src/schemas/config.ts index 037538a..a6547f2 100644 --- a/apps/server/src/schemas/config.ts +++ b/apps/server/src/schemas/config.ts @@ -3,7 +3,7 @@ import { AGENTS_CONFIG_PROVIDERS } from "@/lib/agents-config"; export const AgentsConfigSchema = z .object({ - AGENTS_PROVIDER: z.enum(AGENTS_CONFIG_PROVIDERS).optional(), + AGENTS_PROVIDER: z.enum(AGENTS_CONFIG_PROVIDERS), }) .catchall(z.string()) .openapi("AgentsConfig"); diff --git a/apps/server/src/schemas/files.ts b/apps/server/src/schemas/files.ts new file mode 100644 index 0000000..1211448 --- /dev/null +++ b/apps/server/src/schemas/files.ts @@ -0,0 +1,46 @@ +import { z } from "@hono/zod-openapi"; + +export const ProviderFileInfoSchema = z + .object({ + id: z.string(), + filename: z.string(), + mime_type: z.string(), + size_bytes: z.number(), + created_at: z.string(), + downloadable: z.boolean().optional(), + status: z.string().optional(), + purpose: z.string().optional(), + available: z.boolean().optional(), + }) + .openapi("ProviderFileInfo"); + +export const UploadFileFormSchema = z.object({ + file: z.instanceof(File).openapi({ type: "string", format: "binary" }), +}); + +export const FilesResponseSchema = z.object({ + files: z.array(ProviderFileInfoSchema), +}); + +export const FileStatusesBodySchema = z.object({ + fileIds: z.array(z.string()), +}); + +export const FileStatusInfoSchema = z.object({ + id: z.string(), + status: z.string().optional(), + available: z.boolean().optional(), +}); + +export const FileStatusesResponseSchema = z.object({ + files: z.array(FileStatusInfoSchema), +}); + +export const FileParamsSchema = z.object({ + id: z.string().min(1), +}); + +export const FileDownloadResponseSchema = z.object({ + url: z.string(), + expires_at: z.string().optional(), +}); diff --git a/apps/server/src/schemas/skills.ts b/apps/server/src/schemas/skills.ts new file mode 100644 index 0000000..f6e6c77 --- /dev/null +++ b/apps/server/src/schemas/skills.ts @@ -0,0 +1,56 @@ +import { z } from "@hono/zod-openapi"; + +export const ProviderSkillInfoSchema = z + .object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + source: z.enum(["custom", "official"]), + status: z.enum(["checking", "active", "rejected", "deleted"]), + latest_version: z.string().optional(), + created_at: z.string().optional(), + updated_at: z.string().optional(), + }) + .openapi("ProviderSkillInfo"); + +export const UploadSkillFileFormSchema = z.object({ + file: z.instanceof(File).openapi({ type: "string", format: "binary" }), +}); + +export const CreateSkillBodySchema = z.object({ + fileId: z.string().min(1), +}); + +export const WarmSkillBodySchema = z.object({ + name: z.string().min(1), + url: z.string().min(1), +}); + +export const WarmSkillResponseSchema = z.object({ + ok: z.literal(true), +}); + +export const SkillsQuerySchema = z.object({ + source: z.enum(["custom", "official"]).optional(), +}); + +export const SkillsResponseSchema = z.object({ + skills: z.array(ProviderSkillInfoSchema), +}); + +export const SkillStatusesBodySchema = z.object({ + skillIds: z.array(z.string()), +}); + +export const SkillStatusInfoSchema = z.object({ + id: z.string(), + status: z.enum(["checking", "active", "rejected", "deleted"]).optional(), +}); + +export const SkillStatusesResponseSchema = z.object({ + skills: z.array(SkillStatusInfoSchema), +}); + +export const SkillParamsSchema = z.object({ + id: z.string().min(1), +}); diff --git a/apps/server/tests/files-route.test.ts b/apps/server/tests/files-route.test.ts new file mode 100644 index 0000000..b5b5311 --- /dev/null +++ b/apps/server/tests/files-route.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test"; +import { filesRoute } from "@/routes/files"; + +describe("files route validation", () => { + test("rejects an upload without a file using the public error envelope", async () => { + const response = await filesRoute.request("/files", { method: "POST", body: new FormData() }); + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ error: { message: "file is required" } }); + }); + + test("rejects a status request whose fileIds is not an array", async () => { + const response = await filesRoute.request("/files/status", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ fileIds: "nope" }), + }); + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ error: { message: "fileIds (string[]) is required" } }); + }); +}); diff --git a/apps/server/tests/openapi-contract.test.ts b/apps/server/tests/openapi-contract.test.ts new file mode 100644 index 0000000..bcb3222 --- /dev/null +++ b/apps/server/tests/openapi-contract.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test"; +import { app } from "@/app"; + +const HTTP_METHODS = new Set(["delete", "get", "head", "options", "patch", "post", "put", "trace"]); + +function normalizePath(path: string): string { + return path.replace(/:([^/]+)/g, "{$1}"); +} + +function runtimeOperations(): string[] { + return [ + ...new Set( + app.routes + .filter((route) => route.path.startsWith("/api/") && route.method !== "ALL") + .map((route) => `${route.method.toUpperCase()} ${normalizePath(route.path)}`), + ), + ].sort(); +} + +async function documentedOperations(): Promise { + const response = await app.request("/openapi.json"); + expect(response.status).toBe(200); + const document = (await response.json()) as { + paths: Record>; + }; + + return Object.entries(document.paths) + .flatMap(([path, pathItem]) => + Object.keys(pathItem) + .filter((method) => HTTP_METHODS.has(method)) + .map((method) => `${method.toUpperCase()} ${path}`), + ) + .sort(); +} + +describe("HTTP contract", () => { + test("every runtime API operation is represented in OpenAPI", async () => { + expect(await documentedOperations()).toEqual(runtimeOperations()); + }); + + test("the committed OpenAPI snapshot matches the runtime document", async () => { + const response = await app.request("/openapi.json"); + expect(response.status).toBe(200); + const runtimeDocument = await response.json(); + const committedDocument = await Bun.file(new URL("../openapi.json", import.meta.url)).json(); + expect(committedDocument).toEqual(runtimeDocument); + }); +}); diff --git a/apps/webui/package.json b/apps/webui/package.json index f080f74..0be0e1c 100644 --- a/apps/webui/package.json +++ b/apps/webui/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite", "build": "vite build && bun ../../scripts/check-web-bundle.ts", + "gen:api-types": "openapi-typescript ../server/openapi.json -o src/lib/api/generated/schema.d.ts && biome format --write src/lib/api/generated/schema.d.ts", "preview": "vite preview", "test": "bun test", "typecheck": "tsc --noEmit" @@ -36,6 +37,7 @@ "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", + "openapi-typescript": "^7.13.0", "typescript": "^6.0.3", "vite": "^8.1.4" } diff --git a/apps/webui/src/lib/api/contract.ts b/apps/webui/src/lib/api/contract.ts index e48a56a..a14a81d 100644 --- a/apps/webui/src/lib/api/contract.ts +++ b/apps/webui/src/lib/api/contract.ts @@ -59,7 +59,7 @@ export type CreateSkillFromFileOptions = { body: { fileId: string } }; // `source` selects the catalog: "custom" (workspace-uploaded, the default) or "official" (the // provider's built-in catalog). The provider requires this be explicit to return official skills. // Omitted → custom. -export type ListSkillsOptions = { query?: { limit?: number; source?: "custom" | "official" } }; +export type ListSkillsOptions = { query?: { source?: "custom" | "official" } }; export type DeleteSkillOptions = { path: { skillId: string } }; export type GetSkillStatusesOptions = { body: { skillIds: string[] } }; @@ -169,7 +169,7 @@ export type UploadFileOptions = { // `Agents__`. The list filters by this prefix; display names and mount paths strip it, // so the sandbox/user only ever see the clean original name. `limit` caps the single page fetched. export type ListFilesOptions = { - query?: { prefix?: string; limit?: number }; + query?: never; }; export type DeleteFileOptions = { diff --git a/apps/webui/src/lib/api/generated/schema.d.ts b/apps/webui/src/lib/api/generated/schema.d.ts new file mode 100644 index 0000000..ba8799f --- /dev/null +++ b/apps/webui/src/lib/api/generated/schema.d.ts @@ -0,0 +1,2669 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Read local OpenAgentPack playground config (~/.agents/config.json) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentsConfigSnapshot"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SaveAgentsConfigBody"]; + }; + }; + responses: { + /** @description Save local OpenAgentPack playground config (~/.agents/config.json) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentsConfig"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/config/ready": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Whether runtime provider credentials are configured in the server process */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentsConfigReady"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + agentId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List agents with readiness */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + agents: { + agent: { + id: string; + agentName: string; + provider: string; + description?: string; + model?: + | string + | { + [key: string]: unknown; + }; + environment?: string; + tools?: unknown; + skills: { + /** @enum {string} */ + type: "custom" | "official"; + id: string; + version?: string; + }[]; + mcpServers: string[]; + metadata?: { + [key: string]: unknown; + }; + }; + readiness: { + /** @enum {string} */ + status: + | "ready" + | "missing" + | "creating" + | "updating" + | "invalid" + | "drifted" + | "unavailable" + | "error"; + agentId: string; + /** @enum {string} */ + driftSeverity?: "blocking" | "non_blocking"; + diagnostics: { + /** @enum {string} */ + severity: "error" | "warning" | "info"; + code: string; + message: string; + resource?: { + /** @enum {string} */ + type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file"; + name: string; + provider: string; + }; + }[]; + missing: { + /** @enum {string} */ + type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file"; + name: string; + provider: string; + }[]; + plannedActions: { + /** @enum {string} */ + action: "create" | "update" | "delete" | "no-op"; + address: { + /** @enum {string} */ + type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file"; + name: string; + provider: string; + }; + reason: string; + /** @enum {string} */ + driftKind?: "none" | "local" | "remote" | "both"; + before?: { + [key: string]: unknown; + }; + after?: { + [key: string]: unknown; + }; + dependencies: { + /** @enum {string} */ + type: "environment" | "vault" | "memory_store" | "skill" | "agent" | "deployment" | "file"; + name: string; + provider: string; + }[]; + }[]; + }; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/cloud-agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + prefix?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List raw cloud agents (the resource center's source of truth) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + agents: { + id: string; + name?: string; + description?: string | null; + model?: unknown; + system?: string; + tools?: unknown; + skills?: unknown; + mcp_servers?: unknown; + metadata?: { + [key: string]: string; + }; + version?: number; + type?: string; + workspace_id?: string; + created_at?: string; + updated_at?: string; + archived_at?: string | null; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/cloud-agents/{agentId}/archive": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path: { + agentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Archive a cloud agent (soft delete → status=archived) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {boolean} */ + ok: true; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/cloud-agents/{agentId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path: { + agentId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + model: string; + }; + }; + }; + responses: { + /** @description Update a playbook agent's config (model switch → sync-override) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {boolean} */ + ok: true; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/environments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List raw cloud environments (the shared base sandbox resource) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + environments: { + id: string; + name?: string; + description?: string | null; + config?: unknown; + metadata?: { + [key: string]: string; + }; + scope?: string; + version?: number; + type?: string; + workspace_id?: string; + created_at?: string; + updated_at?: string; + archived_at?: string | null; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + name: string; + description?: string; + metadata?: { + [key: string]: string; + }; + }; + }; + }; + responses: { + /** @description Create a base cloud environment (cloud + unrestricted networking) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + environment: { + id: string | null; + type: string; + version?: number; + }; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/environments/{environmentId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + environmentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delete a cloud environment by remote id */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + type: string; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/vaults": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List raw cloud vaults (the shared credential store resource) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + vaults: { + id: string; + display_name?: string; + metadata?: { + [key: string]: string; + }; + type?: string; + created_at?: string | null; + updated_at?: string | null; + archived_at?: string | null; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + name: string; + metadata?: { + [key: string]: string; + }; + key?: string; + }; + }; + }; + responses: { + /** @description Create a base cloud vault holding the user-supplied DASHSCOPE_API_KEY */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string | null; + type: string; + version?: number; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/vaults/{vaultId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + vaultId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delete a cloud vault by remote id */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + id: string; + type: string; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + limit?: number | null; + agentId?: string; + pageToken?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List sessions for an agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionListResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + agentId: string; + prompt: string; + environmentId: string; + vaultIds?: string[]; + title?: string; + files?: { + fileId: string; + mountPath: string; + }[]; + model?: string; + }; + }; + }; + responses: { + /** @description Session created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDetailResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/{sessionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + agentId?: string; + }; + header?: never; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session detail with events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDetailResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete: { + parameters: { + query?: { + agentId?: string; + }; + header?: never; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDeleteResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/{sessionId}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + agentId?: string; + pageToken?: string; + limit?: number | null; + }; + header?: never; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated session events (newest page first; pass pageToken for older pages) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionEventsPageResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/{sessionId}/messages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + agentId?: string; + message: string; + }; + }; + }; + responses: { + /** @description Message sent; updated session with events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDetailResponse"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/{sessionId}/stream": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: { + after?: number | null; + }; + header?: never; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Stream session events as Server-Sent Events */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": string; + }; + }; + /** @description No active event buffer; caller should fetch the session detail once */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/files": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listFiles"]; + put?: never; + post: operations["uploadFile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/files/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["getFileStatuses"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/files/{id}/download": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["downloadFile"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/files/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteFile"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/skills/upload-file": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["uploadSkillFile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/skills": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listSkills"]; + put?: never; + post: operations["createSkill"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/skills/warm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["warmSkill"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/skills/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["getSkillStatuses"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/skills/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteSkill"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List the active provider's available models */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + models: { + id: string; + display_name: string; + is_enabled?: boolean; + is_new?: boolean; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + AgentsConfigSnapshot: { + /** @enum {string} */ + AGENTS_PROVIDER?: "bailian" | "qoder" | "ark" | "claude"; + } & { + [key: string]: string; + }; + ErrorResponse: { + error: { + message: string; + }; + }; + AgentsConfigReady: { + ready: boolean; + /** @enum {string} */ + provider?: "bailian" | "qoder" | "ark" | "claude"; + }; + AgentsConfig: { + /** @enum {string} */ + AGENTS_PROVIDER: "bailian" | "qoder" | "ark" | "claude"; + } & { + [key: string]: string; + }; + SaveAgentsConfigBody: { + /** @enum {string} */ + AGENTS_PROVIDER: "bailian" | "qoder" | "ark" | "claude"; + } & { + [key: string]: string; + }; + SessionListResponse: { + data: { + session_id: string; + status?: string; + title?: string; + agent?: { + agent_id?: string; + name?: string; + version?: number; + } & { + [key: string]: unknown; + }; + environment_id?: string | null; + created_at?: string; + updated_at?: string; + metadata?: { + [key: string]: string; + }; + }[]; + next_page_token?: string | null; + }; + SessionDetailResponse: { + session: { + session_id: string; + status?: string; + title?: string; + agent?: { + agent_id?: string; + name?: string; + version?: number; + } & { + [key: string]: unknown; + }; + environment_id?: string | null; + created_at?: string; + updated_at?: string; + metadata?: { + [key: string]: string; + }; + }; + events: { + event_id?: string; + type: string; + role?: string; + created_at?: string; + content?: ({ + type: string; + text?: string; + data?: unknown; + } & { + [key: string]: unknown; + })[]; + metadata?: { + [key: string]: unknown; + }; + is_error?: boolean | null; + code?: string | null; + message?: string | null; + }[]; + events_next_page_token?: string | null; + }; + SessionDeleteResponse: { + session_id: string; + deleted: boolean; + }; + SessionEventsPageResponse: { + events: { + event_id?: string; + type: string; + role?: string; + created_at?: string; + content?: ({ + type: string; + text?: string; + data?: unknown; + } & { + [key: string]: unknown; + })[]; + metadata?: { + [key: string]: unknown; + }; + is_error?: boolean | null; + code?: string | null; + message?: string | null; + }[]; + events_next_page_token?: string | null; + }; + ProviderFileInfo: { + id: string; + filename: string; + mime_type: string; + size_bytes: number; + created_at: string; + downloadable?: boolean; + status?: string; + purpose?: string; + available?: boolean; + }; + ProviderSkillInfo: { + id: string; + name: string; + description?: string; + /** @enum {string} */ + source: "custom" | "official"; + /** @enum {string} */ + status: "checking" | "active" | "rejected" | "deleted"; + latest_version?: string; + created_at?: string; + updated_at?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listFiles: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List workspace files */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + files: components["schemas"]["ProviderFileInfo"][]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + uploadFile: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description Upload a workspace file */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProviderFileInfo"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description File exceeds the upload size limit */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getFileStatuses: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + fileIds: string[]; + }; + }; + }; + responses: { + /** @description Get file scan statuses */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + files: { + id: string; + status?: string; + available?: boolean; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + downloadFile: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Resolve a short-lived file download URL */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + url: string; + expires_at?: string; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + deleteFile: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description File deleted */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + uploadSkillFile: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description Upload a skill archive as a file pending audit */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProviderFileInfo"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Skill archive exceeds the upload size limit */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + listSkills: { + parameters: { + query?: { + source?: "custom" | "official"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List custom or official skills */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + skills: components["schemas"]["ProviderSkillInfo"][]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + createSkill: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + fileId: string; + }; + }; + }; + responses: { + /** @description Create a skill from an audited file */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProviderSkillInfo"]; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + warmSkill: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + url: string; + }; + }; + }; + responses: { + /** @description Warm a custom skill until it is active */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {boolean} */ + ok: true; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getSkillStatuses: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + skillIds: string[]; + }; + }; + }; + responses: { + /** @description Get skill scan statuses */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + skills: { + id: string; + /** @enum {string} */ + status?: "checking" | "active" | "rejected" | "deleted"; + }[]; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + deleteSkill: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Skill deleted */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; +} diff --git a/apps/webui/src/lib/api/generated/wire.ts b/apps/webui/src/lib/api/generated/wire.ts new file mode 100644 index 0000000..f103702 --- /dev/null +++ b/apps/webui/src/lib/api/generated/wire.ts @@ -0,0 +1,48 @@ +import type { paths } from "./schema"; + +export type HttpMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT"; +type LowerMethod = Lowercase; + +type PathForMethod = { + [P in keyof paths]: LowerMethod extends keyof paths[P] + ? paths[P][LowerMethod] extends never + ? never + : P + : never; +}[keyof paths]; + +type Operation> = NonNullable< + paths[P][LowerMethod & keyof paths[P]] +>; + +type Parameter = O extends { parameters: infer Parameters } + ? K extends keyof Parameters + ? Exclude + : never + : never; + +type JsonRequestBody = O extends { requestBody?: infer Body } + ? Exclude extends { content: { "application/json": infer Json } } + ? Json + : never + : never; + +type JsonContent = Response extends { content: { "application/json": infer Json } } ? Json : undefined; + +type SuccessResponse = O extends { responses: infer Responses } + ? { + [Status in keyof Responses]: `${Status & (string | number)}` extends `2${string}` + ? JsonContent + : never; + }[keyof Responses] + : never; + +export type WireRequestOptions> = { + path?: Parameter, "path">; + query?: Parameter, "query">; + body?: JsonRequestBody>; +}; + +export type WireResponse> = SuccessResponse>; + +export type WirePath = PathForMethod; diff --git a/apps/webui/src/lib/api/transports/rest.ts b/apps/webui/src/lib/api/transports/rest.ts index c370579..30f8a0a 100644 --- a/apps/webui/src/lib/api/transports/rest.ts +++ b/apps/webui/src/lib/api/transports/rest.ts @@ -2,24 +2,38 @@ import type { ApiResult, ApiTransport, CreateSkillFromFileOptions, + FileStatusInfo, SkillSummary, SkillUploadResult, UploadedFile, } from "../contract"; import { formatApiErrorMessage } from "../error-message"; +import type { HttpMethod, WirePath, WireRequestOptions, WireResponse } from "../generated/wire"; // The server enriches `status` + `available` via @openagentpack/sdk/file-lifecycle before JSON serialization. -// Strip the wire-only `downloadable` field so the public contract stays clean. -function stripDownloadable(file: UploadedFile & { downloadable?: boolean }): UploadedFile { - const { downloadable: _downloadable, ...rest } = file; - return rest; +type WireFile = WireResponse<"GET", "/api/files">["files"][number]; + +function isFileStatus(value: string | undefined): value is NonNullable { + return value === "available" || value === "checking" || value === "rejected" || value === "type_rejected"; } -type RequestOptions = { - path?: Record; - query?: Record; - body?: unknown; -}; +// Map the generated wire shape onto the UI's narrower file lifecycle interface. +function toUploadedFile(file: WireFile): UploadedFile { + return { + id: file.id, + filename: file.filename, + mime_type: file.mime_type, + size_bytes: file.size_bytes, + created_at: file.created_at, + status: isFileStatus(file.status) ? file.status : undefined, + available: file.available, + }; +} + +async function discardResponse(promise: Promise>): Promise> { + const result = await promise; + return result.error ? { error: result.error } : {}; +} function safeJson(text: string): unknown { try { @@ -29,12 +43,12 @@ function safeJson(text: string): unknown { } } -export async function request( - method: string, - template: string, - options: RequestOptions = {}, -): Promise> { - let path = template; +export async function request>( + method: M, + template: P, + options: WireRequestOptions = {}, +): Promise>> { + let path: string = template; if (options.path) { for (const [key, value] of Object.entries(options.path)) { path = path.replace(`{${key}}`, encodeURIComponent(value)); @@ -76,7 +90,7 @@ export async function request( const message = formatApiErrorMessage(parsed, `HTTP ${response.status}`); return { error: { error: { message } } }; } - return { data: parsed as T }; + return { data: parsed as WireResponse }; } async function uploadFileRest(file: File): Promise> { @@ -94,7 +108,7 @@ async function uploadFileRest(file: File): Promise> { if (!response.ok) { return { error: { error: { message: formatApiErrorMessage(parsed, `HTTP ${response.status}`) } } }; } - return { data: stripDownloadable(parsed as UploadedFile & { downloadable?: boolean }) }; + return { data: toUploadedFile(parsed as WireResponse<"POST", "/api/files">) }; } // Phase 1: upload the zip and return a pending handle. The file must clear content audit @@ -135,34 +149,50 @@ export const restTransport: ApiTransport = { body: options.body, }), deleteSession: (options) => - request("DELETE", "/api/sessions/{sessionId}", { path: options.path, query: options.query }), + discardResponse(request("DELETE", "/api/sessions/{sessionId}", { path: options.path, query: options.query })), listAgents: (options = {}) => request("GET", "/api/cloud-agents", { query: options.query }), listModels: () => request("GET", "/api/models"), - archiveAgent: (options) => request("POST", "/api/cloud-agents/{agentId}/archive", { path: options.path }), - updateAgent: (options) => request("POST", "/api/cloud-agents/{agentId}", { path: options.path, body: options.body }), - warmSkill: (options) => request("POST", "/api/skills/warm", { body: options.body }), + archiveAgent: (options) => + discardResponse(request("POST", "/api/cloud-agents/{agentId}/archive", { path: options.path })), + updateAgent: (options) => + discardResponse(request("POST", "/api/cloud-agents/{agentId}", { path: options.path, body: options.body })), + warmSkill: (options) => discardResponse(request("POST", "/api/skills/warm", { body: options.body })), listEnvironments: () => request("GET", "/api/environments"), - createEnvironment: (options) => request("POST", "/api/environments", { body: options.body }), - deleteEnvironment: (options) => request("DELETE", "/api/environments/{environmentId}", { path: options.path }), + async createEnvironment(options) { + const result = await request("POST", "/api/environments", { body: options.body }); + return result.error ? { error: result.error } : { data: result.data?.environment }; + }, + deleteEnvironment: (options) => + discardResponse(request("DELETE", "/api/environments/{environmentId}", { path: options.path })), listVaults: () => request("GET", "/api/vaults"), createVault: (options) => request("POST", "/api/vaults", { body: options.body }), - deleteVault: (options) => request("DELETE", "/api/vaults/{vaultId}", { path: options.path }), + deleteVault: (options) => discardResponse(request("DELETE", "/api/vaults/{vaultId}", { path: options.path })), uploadFile: (options) => uploadFileRest(options.body.file), - async listFiles(options = {}) { - const res = await request<{ files: (UploadedFile & { downloadable?: boolean })[] }>("GET", "/api/files", { - query: options.query, - }); + async listFiles() { + const res = await request("GET", "/api/files"); if (res.error || !res.data) return res as ApiResult<{ files: UploadedFile[] }>; - return { data: { files: res.data.files.map(stripDownloadable) } }; + return { data: { files: res.data.files.map(toUploadedFile) } }; + }, + async getFileStatuses(options) { + const result = await request("POST", "/api/files/status", { body: options.body }); + if (result.error || !result.data) return result as ApiResult<{ files: FileStatusInfo[] }>; + return { + data: { + files: result.data.files.map((file) => ({ + ...file, + status: isFileStatus(file.status) ? file.status : undefined, + })), + }, + }; }, - getFileStatuses: (options) => request("POST", "/api/files/status", { body: options.body }), - getFileDownload: (options) => request("GET", "/api/files/{fileId}/download", { path: options.path }), - deleteFile: (options) => request("DELETE", "/api/files/{fileId}", { path: options.path }), + getFileDownload: (options) => request("GET", "/api/files/{id}/download", { path: { id: options.path.fileId } }), + deleteFile: (options) => discardResponse(request("DELETE", "/api/files/{id}", { path: { id: options.path.fileId } })), uploadSkill: (options) => uploadSkillRest(options.body.file), createSkillFromFile: (options) => createSkillFromFileRest(options), listSkills: (options = {}) => request("GET", "/api/skills", { query: options.query }), getSkillStatuses: (options) => request("POST", "/api/skills/status", { body: options.body }), - deleteSkill: (options) => request("DELETE", "/api/skills/{skillId}", { path: options.path }), + deleteSkill: (options) => + discardResponse(request("DELETE", "/api/skills/{id}", { path: { id: options.path.skillId } })), getConfig: () => request("GET", "/api/config"), getConfigReady: () => request("GET", "/api/config/ready"), saveConfig: (options) => request("PUT", "/api/config", { body: options.body }), diff --git a/apps/webui/src/lib/domain/file-api.ts b/apps/webui/src/lib/domain/file-api.ts index b093f00..d12b9ab 100644 --- a/apps/webui/src/lib/domain/file-api.ts +++ b/apps/webui/src/lib/domain/file-api.ts @@ -47,8 +47,8 @@ export async function uploadFile(file: File): Promise { * (the OpenAPI list has no project-keyword filter). Returns files with the prefix intact — * callers strip it for display via stripPrefix. */ -export async function listFiles(limit?: number): Promise { - const { data, error } = await listApiFiles({ query: { limit } }); +export async function listFiles(): Promise { + const { data, error } = await listApiFiles(); if (error) throw new Error(formatApiErrorMessage(error, "获取文件列表失败")); return (data?.files ?? []).filter((f) => hasPrefix(f.filename)); } diff --git a/apps/webui/src/lib/domain/skill-api.ts b/apps/webui/src/lib/domain/skill-api.ts index 2f188aa..3016cdc 100644 --- a/apps/webui/src/lib/domain/skill-api.ts +++ b/apps/webui/src/lib/domain/skill-api.ts @@ -32,15 +32,15 @@ export async function createSkillFromFile(fileId: string): Promise } /** List the project's custom skills (workspace-level, newest first). */ -export async function listSkills(limit?: number): Promise { - const { data, error } = await listApiSkills({ query: { limit, source: "custom" } }); +export async function listSkills(): Promise { + const { data, error } = await listApiSkills({ query: { source: "custom" } }); if (error) throw new Error(formatApiErrorMessage(error, "获取 Skill 列表失败")); return (data?.skills ?? []).filter((s) => s.source === "custom"); } /** List the provider's built-in (official) skill catalog. Read-only — these can't be uploaded or deleted. */ -export async function listOfficialSkills(limit?: number): Promise { - const { data, error } = await listApiSkills({ query: { limit, source: "official" } }); +export async function listOfficialSkills(): Promise { + const { data, error } = await listApiSkills({ query: { source: "official" } }); if (error) throw new Error(formatApiErrorMessage(error, "获取内置 Skill 列表失败")); return (data?.skills ?? []).filter((s) => s.source === "official"); } diff --git a/apps/webui/tests/api-types-freshness.test.ts b/apps/webui/tests/api-types-freshness.test.ts new file mode 100644 index 0000000..435c0b2 --- /dev/null +++ b/apps/webui/tests/api-types-freshness.test.ts @@ -0,0 +1,48 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const webuiDir = resolve(import.meta.dir, ".."); +let generatedDir: string | undefined; + +afterAll(async () => { + if (generatedDir) await rm(generatedDir, { recursive: true, force: true }); +}); + +describe("generated HTTP wire contract", () => { + test("matches the committed OpenAPI snapshot", async () => { + generatedDir = await mkdtemp(join(tmpdir(), "openagentpack-api-types-")); + const generatedPath = join(generatedDir, "schema.d.ts"); + const process = Bun.spawn( + [resolve(webuiDir, "node_modules/.bin/openapi-typescript"), "../server/openapi.json", "-o", generatedPath], + { + cwd: webuiDir, + stdout: "pipe", + stderr: "pipe", + }, + ); + const exitCode = await process.exited; + if (exitCode !== 0) { + throw new Error(await new Response(process.stderr).text()); + } + const formatProcess = Bun.spawn( + [resolve(webuiDir, "../../node_modules/.bin/biome"), "format", "--write", generatedPath], + { + cwd: webuiDir, + stdout: "pipe", + stderr: "pipe", + }, + ); + const formatExitCode = await formatProcess.exited; + if (formatExitCode !== 0) { + throw new Error(await new Response(formatProcess.stderr).text()); + } + + const [expected, actual] = await Promise.all([ + readFile(resolve(webuiDir, "src/lib/api/generated/schema.d.ts"), "utf8"), + readFile(generatedPath, "utf8"), + ]); + expect(actual).toBe(expected); + }, 15_000); +}); diff --git a/apps/webui/tests/environment-api.test.ts b/apps/webui/tests/environment-api.test.ts new file mode 100644 index 0000000..5bda26d --- /dev/null +++ b/apps/webui/tests/environment-api.test.ts @@ -0,0 +1,19 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { createApiEnvironment } from "../src/lib/api/client"; + +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +describe("environment API adapter", () => { + test("unwraps the generated wire response for domain callers", async () => { + globalThis.fetch = (async () => + Response.json({ environment: { id: "env_1", type: "cloud", version: 2 } })) as typeof fetch; + + const result = await createApiEnvironment({ body: { name: "Agents/base" } }); + + expect(result).toEqual({ data: { id: "env_1", type: "cloud", version: 2 } }); + }); +}); diff --git a/bun.lock b/bun.lock index 890d773..efe5542 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "apps/server": { "name": "@openagentpack/server", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@hono/zod-openapi": "^1.4.0", "@openagentpack/playbooks": "workspace:*", @@ -56,13 +56,14 @@ "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", + "openapi-typescript": "^7.13.0", "typescript": "^6.0.3", "vite": "^8.1.4", }, }, "packages/cli": { "name": "@openagentpack/cli", - "version": "0.0.1", + "version": "0.0.2", "bin": { "agents": "dist/bin/agents.js", }, @@ -89,7 +90,7 @@ }, "packages/playground": { "name": "@openagentpack/playground", - "version": "0.0.1", + "version": "0.0.2", "bin": { "agents-playground": "dist/bin/playground.js", }, @@ -111,7 +112,7 @@ }, "packages/sdk": { "name": "@openagentpack/sdk", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "jszip": "^3.10.1", "yaml": "^2.9.0", @@ -149,6 +150,10 @@ "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@8.5.0", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="], @@ -311,6 +316,12 @@ "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="], + + "@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="], + + "@redocly/openapi-core": ["@redocly/openapi-core@1.34.17", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.2.0", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-wsV2keCt6B806XpSdezbWZ9aFJYf14YVh+XQf0ESt7M90yqVuxH9//PxvtC70sgj9OCkRM3nRaLfu4MsGQZRig=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], @@ -453,6 +464,8 @@ "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -467,8 +480,12 @@ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -481,6 +498,8 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -497,6 +516,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], @@ -545,6 +566,8 @@ "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-equals": ["fast-equals@5.3.3", "", {}, "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -593,6 +616,8 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-id": ["human-id@4.2.0", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA=="], "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], @@ -601,6 +626,8 @@ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], @@ -641,8 +668,14 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], @@ -785,6 +818,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], @@ -799,6 +834,8 @@ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="], + "openapi3-ts": ["openapi3-ts@4.6.0", "", { "dependencies": { "yaml": "^2.9.0" } }, "sha512-a4sfn6L2sIShhtzJqmjGrARvxAW/3F2BJDdyRVvNF9VhAsZSh5hSyI3a9TNvmzBxXmq66nY5LNT5bQcBxYAZZg=="], "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], @@ -821,6 +858,8 @@ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -841,6 +880,8 @@ "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + "postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="], "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], @@ -909,6 +950,8 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -969,7 +1012,7 @@ "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1003,6 +1046,8 @@ "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], @@ -1023,6 +1068,8 @@ "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -1043,6 +1090,10 @@ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -1057,6 +1108,8 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@redocly/openapi-core/js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "dependency-cruiser/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -1074,5 +1127,9 @@ "vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "@changesets/parse/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@redocly/openapi-core/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "tsconfig-paths-webpack-plugin/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], } } diff --git a/docs/reference/openapi.md b/docs/reference/openapi.md index 784c0d5..2df096a 100644 --- a/docs/reference/openapi.md +++ b/docs/reference/openapi.md @@ -12,7 +12,8 @@ bun run --cwd apps/server gen:openapi # runs scripts/emit-openapi.ts ``` - The snapshot can lag behind the route handlers; regenerate it to capture the latest endpoints. + The snapshot is required to match the running server. `apps/server/tests/openapi-contract.test.ts` + checks both route coverage and snapshot freshness in CI. ## Registered route groups @@ -29,21 +30,16 @@ | Skills | `routes/skills.ts` | | Models | `routes/models.ts` | -Routes are defined with `@hono/zod-openapi`'s `createRoute`, so each route's request and response shapes are validated by Zod schemas and surfaced in the OpenAPI document. +Routes are defined with `@hono/zod-openapi`'s `createRoute`, so request validation, typed responses, +and OpenAPI generation share the same Zod schemas. Multipart files and skill uploads use the same +route-definition seam while retaining their upload-specific validation. -## Documented endpoints (committed snapshot) +## Documented endpoints -The committed `openapi.json` (`openapi: 3.0.0`, title `OpenAgentPack API`) currently documents the agents and sessions surface: - -| Method | Path | Description | -|--------|------|-------------| -| GET | `/api/agents` | List agents with readiness. Optional `agentId` query param. | -| GET | `/api/sessions` | List sessions for an agent. | -| POST | `/api/sessions` | Create a session. Returns `session_id`. | -| GET | `/api/sessions/{sessionId}` | Session detail with events. | -| DELETE | `/api/sessions/{sessionId}` | Delete a session. | -| POST | `/api/sessions/{sessionId}/messages` | Send a message; returns the updated session with events. | -| GET | `/api/sessions/{sessionId}/stream` | Stream session events as Server-Sent Events. | +The committed `openapi.json` (`openapi: 3.0.0`, title `OpenAgentPack API`) is the complete generated +reference for every `/api/*` operation. It covers config, agents, environments, vaults, sessions, +files, skills, and models. Read that file or `GET /openapi.json` for the exact methods, paths, +parameters, and schemas; this page intentionally does not maintain a second handwritten endpoint list. The SSE stream emits frames with event types `event`, `done`, and `ping`. A `410` response means the event buffer is no longer active — the caller should fetch the session detail once.