{"components":{"schemas":{"ApprovalDecisionRequest":{"properties":{"approval_id":{"description":"Unique approval identifier","type":"string"},"approved":{"description":"Whether the action is approved","type":"boolean"},"approved_by":{"description":"User who approved (defaults to authenticated user)","type":"string"},"feedback":{"description":"Optional feedback message","type":"string"},"modified_action":{"description":"Optional modified action if approved with changes","type":"string"},"run_id":{"description":"Optional run ID","type":"string"},"workflow_id":{"description":"Workflow ID requiring approval","type":"string"}},"required":["workflow_id","approval_id","approved"],"type":"object"},"ApprovalDecisionResponse":{"properties":{"approval_id":{"description":"Approval ID","type":"string"},"message":{"description":"Status message","type":"string"},"run_id":{"description":"Run ID","type":"string"},"status":{"description":"Status of the approval submission","type":"string"},"success":{"description":"Whether the approval was successfully submitted","type":"boolean"},"workflow_id":{"description":"Workflow ID","type":"string"}},"type":"object"},"CancelTaskRequest":{"properties":{"reason":{"description":"Optional reason for cancellation","type":"string"}},"type":"object"},"CancelTaskResponse":{"properties":{"message":{"description":"Status message","type":"string"},"status":{"description":"Current task status (included in 409 response)","type":"string"},"success":{"description":"Whether the cancellation was successful","type":"boolean"}},"type":"object"},"HealthResponse":{"properties":{"checks":{"additionalProperties":{"type":"string"},"type":"object"},"time":{"format":"date-time","type":"string"},"version":{"type":"string"}},"type":"object"},"IntegrationConnection":{"properties":{"connected_at":{"format":"date-time","nullable":true,"type":"string"},"created_at":{"format":"date-time","type":"string"},"display_name":{"type":"string"},"id":{"format":"uuid","type":"string"},"provider":{"type":"string"},"provider_account":{"type":"string"},"status":{"enum":["pending","active","declined","error","revoked"],"type":"string"}},"type":"object"},"ListSessionsResponse":{"properties":{"sessions":{"description":"List of sessions","items":{"$ref":"#/components/schemas/SessionSummary"},"type":"array"},"total_count":{"description":"Total number of sessions for the user","type":"integer"}},"required":["sessions","total_count"],"type":"object"},"ListTasksResponse":{"properties":{"tasks":{"items":{"$ref":"#/components/schemas/TaskSummary"},"type":"array"},"total_count":{"type":"integer"}},"type":"object"},"SessionHistoryResponse":{"properties":{"session_id":{"description":"Session UUID","format":"uuid","type":"string"},"tasks":{"description":"List of tasks in chronological order","items":{"$ref":"#/components/schemas/TaskHistory"},"type":"array"},"total":{"description":"Total number of tasks in session","type":"integer"}},"required":["session_id","tasks","total"],"type":"object"},"SessionResponse":{"properties":{"context":{"description":"Session context metadata","type":"object"},"created_at":{"description":"Session creation timestamp","format":"date-time","type":"string"},"expires_at":{"description":"Session expiration timestamp","format":"date-time","type":"string"},"session_id":{"description":"Unique session identifier","format":"uuid","type":"string"},"task_count":{"description":"Number of tasks in this session","type":"integer"},"token_budget":{"description":"Token budget for the session","type":"integer"},"tokens_used":{"description":"Total tokens consumed in this session","type":"integer"},"updated_at":{"description":"Session last update timestamp","format":"date-time","type":"string"},"user_id":{"description":"User who owns this session","format":"uuid","type":"string"}},"required":["session_id","user_id","task_count","tokens_used","created_at"],"type":"object"},"SessionSummary":{"properties":{"average_cost_per_task":{"description":"Average cost per task (total_cost_usd / task_count)","format":"double","type":"number"},"budget_remaining":{"description":"Remaining tokens in the budget (never negative)","type":"integer"},"budget_utilization":{"description":"Fraction of token budget consumed (tokens_used / token_budget)","format":"double","type":"number"},"context":{"description":"Session context metadata (includes title, external_id, and feature flags)","type":"object"},"created_at":{"description":"Session creation timestamp","format":"date-time","type":"string"},"expires_at":{"description":"Session expiration timestamp","format":"date-time","type":"string"},"failed_tasks":{"description":"Number of failed tasks","type":"integer"},"first_task_mode":{"description":"Mode of the first task in the session (used for research detection)","type":"string"},"is_active":{"description":"Whether the session is considered active (recent activity in the last 24 hours)","type":"boolean"},"is_near_budget_limit":{"description":"Whether the session is nearing its token budget (utilization \u003e 90%)","type":"boolean"},"is_research_session":{"description":"Whether this session is inferred to be a research session","type":"boolean"},"last_activity_at":{"description":"Timestamp of the latest completed task in this session","format":"date-time","type":"string"},"latest_task_query":{"description":"Query text of the most recent task in this session","type":"string"},"latest_task_status":{"description":"Status of the most recent task (e.g., COMPLETED, FAILED, RUNNING)","type":"string"},"session_id":{"description":"Unique session identifier","format":"uuid","type":"string"},"success_rate":{"description":"Task success rate in the range 0.0-1.0 (successful_tasks / task_count)","format":"double","type":"number"},"successful_tasks":{"description":"Number of successfully completed tasks","type":"integer"},"task_count":{"description":"Number of tasks in this session","type":"integer"},"title":{"description":"Session title (auto-generated or user-defined, max 60 chars)","maxLength":60,"type":"string"},"token_budget":{"description":"Token budget for the session","type":"integer"},"tokens_used":{"description":"Total tokens consumed in this session (from DB, optionally enriched by Redis)","type":"integer"},"total_cost_usd":{"description":"Total cost of all tasks in this session, in USD","format":"double","type":"number"},"updated_at":{"description":"Session last update timestamp","format":"date-time","type":"string"},"user_id":{"description":"User who owns this session","format":"uuid","type":"string"}},"required":["session_id","user_id","task_count","tokens_used","created_at"],"type":"object"},"SessionTurnsResponse":{"properties":{"count":{"description":"Total number of turns in session","type":"integer"},"session_id":{"description":"Session identifier (UUID or external_id)","type":"string"},"turns":{"description":"Turns grouped by task in chronological order","items":{"$ref":"#/components/schemas/Turn"},"type":"array"}},"required":["session_id","turns","count"],"type":"object"},"TaskEvent":{"properties":{"agent_id":{"type":"string"},"message":{"type":"string"},"seq":{"type":"integer"},"stream_id":{"type":"string"},"timestamp":{"format":"date-time","type":"string"},"type":{"type":"string"},"workflow_id":{"type":"string"}},"type":"object"},"TaskHistory":{"properties":{"agents_used":{"description":"Number of agents used","type":"integer"},"completed_at":{"description":"Task completion timestamp","format":"date-time","type":"string"},"duration_ms":{"description":"Task duration in milliseconds","type":"integer"},"error_message":{"description":"Error message if task failed","type":"string"},"metadata":{"description":"Additional task metadata","type":"object"},"mode":{"description":"Execution mode","type":"string"},"query":{"description":"Task query/command","type":"string"},"result":{"description":"Task result/output","type":"string"},"started_at":{"description":"Task start timestamp","format":"date-time","type":"string"},"status":{"description":"Task execution status","enum":["RUNNING","COMPLETED","FAILED","CANCELLED","TIMEOUT"],"type":"string"},"task_id":{"description":"Task UUID","format":"uuid","type":"string"},"tools_invoked":{"description":"Number of tools invoked","type":"integer"},"total_cost_usd":{"description":"Total cost in USD","format":"double","type":"number"},"total_tokens":{"description":"Total tokens used","type":"integer"},"workflow_id":{"description":"Temporal workflow ID","type":"string"}},"required":["task_id","workflow_id","query","status","started_at"],"type":"object"},"TaskRequest":{"properties":{"context":{"description":"Additional context for the task (recognized keys: role, model_tier, model_override, provider_override, system_prompt, prompt_params, template, template_name (alias), template_version, disable_ai, history_window_size, use_case_preset, primers_count, recents_count, compression_trigger_ratio, compression_target_ratio)","example":{"compression_target_ratio":0.375,"compression_trigger_ratio":0.75,"disable_ai":true,"history_window_size":75,"model_override":"gpt-5-2025-08-07","model_tier":"large","primers_count":3,"prompt_params":{"current_date":"2025-09-01","profile_id":"49598h6e"},"recents_count":20,"role":"analysis","system_prompt":"You are a concise assistant.","template":"research_summary","template_version":"1.0.0"},"type":"object"},"mode":{"default":"simple","description":"Execution mode (advanced): simple|standard|complex|supervisor. Omit to auto-detect.","enum":["simple","standard","complex","supervisor"],"type":"string"},"model_override":{"description":"Specific model override (injects into context.model_override, e.g., 'gpt-5.6-terra', 'claude-sonnet-5')","type":"string"},"model_tier":{"description":"Preferred model tier (injects into context.model_tier)","enum":["small","medium","large"],"type":"string"},"provider_override":{"description":"Provider override (injects into context.provider_override, e.g., 'openai', 'anthropic')","type":"string"},"query":{"description":"The task query or command","type":"string"},"session_id":{"description":"Session ID for continuity (UUID or custom string)","type":"string"}},"required":["query"],"type":"object"},"TaskResponse":{"properties":{"created_at":{"format":"date-time","type":"string"},"message":{"type":"string"},"task_id":{"type":"string"}},"type":"object"},"TaskStatusResponse":{"properties":{"created_at":{"format":"date-time","type":"string"},"error":{"type":"string"},"mode":{"type":"string"},"query":{"type":"string"},"response":{"description":"Parsed JSON response (if result is valid JSON) - for backward compatibility","type":"object"},"result":{"description":"Raw LLM response (plain text or JSON string)","type":"string"},"session_id":{"type":"string"},"status":{"type":"string"},"task_id":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"},"TaskStreamResponse":{"properties":{"stream_url":{"description":"SSE endpoint URL to stream real-time events for this task","example":"/api/v1/stream/sse?workflow_id=task-user123-1234567890","type":"string"},"task_id":{"description":"Task identifier (same as workflow_id)","example":"task-user123-1234567890","type":"string"},"workflow_id":{"description":"Unique workflow identifier for this task","example":"task-user123-1234567890","type":"string"}},"required":["workflow_id","task_id","stream_url"],"type":"object"},"TaskSummary":{"properties":{"completed_at":{"format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"},"mode":{"type":"string"},"query":{"type":"string"},"status":{"type":"string"},"task_id":{"type":"string"},"total_token_usage":{"type":"object"}},"type":"object"},"Turn":{"properties":{"events":{"items":{"$ref":"#/components/schemas/TaskEvent"},"type":"array"},"final_output":{"type":"string"},"metadata":{"$ref":"#/components/schemas/TurnMetadata"},"task_id":{"type":"string"},"timestamp":{"format":"date-time","type":"string"},"turn":{"type":"integer"},"user_query":{"type":"string"}},"required":["turn","task_id","user_query","final_output","timestamp","events","metadata"],"type":"object"},"TurnMetadata":{"properties":{"agents_involved":{"items":{"type":"string"},"type":"array"},"attachments":{"description":"Optional attachment metadata from task context","items":{"properties":{"filename":{"type":"string"},"id":{"type":"string"},"media_type":{"type":"string"},"size_bytes":{"type":"integer"},"thumbnail":{"description":"Thumbnail data URL (\u003c=50KB)","type":"string"}},"type":"object"},"type":"array"},"execution_time_ms":{"type":"integer"},"tokens_used":{"type":"integer"}},"required":["tokens_used","execution_time_ms","agents_involved"],"type":"object"},"UpdateSessionTitleRequest":{"properties":{"title":{"description":"New session title (max 60 characters)","maxLength":60,"minLength":1,"type":"string"}},"required":["title"],"type":"object"},"UpdateSessionTitleResponse":{"properties":{"session_id":{"description":"Session identifier","format":"uuid","type":"string"},"title":{"description":"Updated session title","type":"string"}},"required":["session_id","title"],"type":"object"}},"securitySchemes":{"apiKey":{"description":"API key for authentication","in":"header","name":"X-API-Key","type":"apiKey"}}},"info":{"description":"REST API for Shannon multi-agent AI platform","title":"Shannon Gateway API","version":"0.1.0"},"openapi":"3.0.0","paths":{"/api/v1/agents/avatars/presets":{"get":{"description":"Returns the list of pre-defined avatar presets loaded from the server config (AGENT_AVATAR_PRESETS_PATH). Each entry has a CDN URL ready for display.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"presets":{"items":{"properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Preset list"},"401":{"description":"Unauthorized"}},"security":[{"apiKey":[]}],"summary":"List built-in agent avatar presets","tags":["Agents"]}},"/api/v1/agents/sync":{"get":{"description":"Returns the cloud-side agent mirror for the authenticated tenant, including soft-deleted rows so the daemon can apply tombstones to its local store.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"agents":{"items":{"properties":{"agent_key":{"type":"string"},"config":{"type":"object"},"deleted_at":{"format":"date-time","nullable":true,"type":"string"},"display_name":{"type":"string"},"memory":{"nullable":true,"type":"string"},"profile":{"type":"object"},"prompt":{"type":"string"},"skills":{"items":{},"type":"array"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Agent list (includes soft-deleted tombstones)"},"401":{"description":"Unauthorized"},"500":{"description":"DB query failed"}},"security":[{"apiKey":[]}],"summary":"Pull agent definitions to daemon (sync read)","tags":["Agents"]},"put":{"description":"Daemon upserts its local agent roster to the cloud mirror. Supports incremental and full-sync modes. full_sync=true deletes cloud agents not present in the batch (gated by sync_started_at to avoid racing concurrent creates).","requestBody":{"content":{"application/json":{"schema":{"properties":{"agents":{"items":{"properties":{"agent_key":{"type":"string"},"config":{"type":"object"},"display_name":{"type":"string"},"memory":{"nullable":true,"type":"string"},"profile":{"type":"object"},"prompt":{"type":"string"},"skills":{"items":{},"type":"array"},"updated_at":{"format":"date-time","type":"string"}},"required":["agent_key"],"type":"object"},"type":"array"},"full_sync":{"description":"When true, soft-deletes cloud agents not present in this batch (subject to sync_started_at gate).","type":"boolean"},"sync_started_at":{"description":"Timestamp when the daemon began collecting the batch. Cloud-side agents created after this timestamp are not deleted.","format":"date-time","type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"soft_deleted":{"description":"Number of agent rows soft-deleted (full_sync only).","type":"integer"},"synced":{"description":"Number of agent rows inserted or updated.","type":"integer"}},"type":"object"}}},"description":"Sync result"},"400":{"description":"Invalid request body or missing agent_key"},"401":{"description":"Unauthorized"},"500":{"description":"DB write failed"}},"security":[{"apiKey":[]}],"summary":"Push agent definitions from daemon (sync write)","tags":["Agents"]}},"/api/v1/approvals/decision":{"post":{"description":"Submit a human approval decision for a workflow requiring approval. Replaces the admin-only endpoint with a gateway-managed, authenticated endpoint.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalDecisionRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalDecisionResponse"}}},"description":"Approval decision submitted successfully"},"400":{"description":"Invalid request - Missing required fields"},"401":{"description":"Unauthorized - Invalid or missing API key"},"403":{"description":"Forbidden - User does not have permission to approve this workflow"},"404":{"description":"Workflow or approval not found"},"429":{"description":"Rate limit exceeded"}},"summary":"Submit approval decision"}},"/api/v1/integrations":{"get":{"description":"Returns the authenticated user's non-revoked connections. Pending rows are lazily reconciled against the vendor.","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/IntegrationConnection"},"type":"array"}}},"description":"Connections"},"401":{"description":"Unauthorized"},"404":{"description":"Feature disabled"}},"summary":"List the caller's integration connections","tags":["Integrations"]}},"/api/v1/integrations/tools":{"get":{"description":"Returns the tools of the caller's ACTIVE connections as function-calling schemas (external name, description, JSON Schema parameters, provider). Returns an empty array (not 404) when the feature is disabled. Consumed by the Kocoro Desktop daemon's local agent loop.","responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"description":{"type":"string"},"material_side_effect":{"description":"Explicit only when known; X read tools are false","type":"boolean"},"name":{"type":"string"},"parameters":{"description":"JSON Schema for the tool arguments","type":"object"},"provider":{"type":"string"}},"type":"object"},"type":"array"}}},"description":"Integration tool schemas"},"401":{"description":"Unauthorized"}},"summary":"List the caller's connected integration tools","tags":["Integrations"]}},"/api/v1/integrations/tools/{name}/execute":{"post":{"description":"Resolves the external tool name to its provider and vendor tool, then executes it against the caller's active connection. Identity is taken from the authenticated context only. request_id is ShanClaw's stable execution identity: replay does not repeat a completed vendor read or ledger write. Legacy call_id/X-Call-ID and then Idempotency-Key are fallback inputs.","parameters":[{"description":"External integration tool name (from GET /api/v1/integrations/tools)","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"arguments":{"description":"Tool-specific parameters","type":"object"},"call_id":{"description":"Legacy stable idempotency key (also accepted as X-Call-ID)","type":"string"},"request_id":{"description":"Stable ShanClaw execution identity; preferred idempotency key","type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"error":{"nullable":true,"type":"string"},"metadata":{"type":"object"},"output":{},"success":{"type":"boolean"},"text":{"nullable":true,"type":"string"},"usage":{"properties":{"breakdown":{"additionalProperties":{"type":"integer"},"type":"object"},"cost_model":{"type":"string"},"cost_usd":{"type":"number"},"model":{"type":"string"},"provider":{"type":"string"},"unit_type":{"type":"string"},"units":{"type":"integer"}},"type":"object"}},"type":"object"}}},"description":"Tool execution result (success may be false for a tool-level error)"},"400":{"description":"Invalid request body"},"401":{"description":"Unauthorized"},"403":{"description":"Feature disabled or tool not allowed"},"404":{"description":"Provider not connected or unknown tool"},"409":{"description":"Vendor auth expired (reconnect required), same request_id still in progress, idempotency_conflict when the id is reused for different tool/arguments, or outcome_unknown. Encrypted replay output expires after 24 hours; expired calls remain billing-safe and are never re-dispatched."},"429":{"description":"integration_limit_exceeded"},"502":{"description":"Vendor error"},"503":{"description":"billing_error; replay with the same call_id"}},"summary":"Execute an integration tool","tags":["Integrations"]}},"/api/v1/integrations/x/oauth/callback":{"get":{"description":"Unauthenticated browser callback secured by single-use, expiring, connection-bound OAuth state. The required users/me identity read is recorded against the per-user X cap and billing ledger. Returns a no-store HTML page the user can close.","parameters":[{"in":"query","name":"state","required":true,"schema":{"type":"string"}},{"in":"query","name":"code","required":false,"schema":{"type":"string"}},{"in":"query","name":"error","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Connected or user denied; close-window HTML"},"400":{"description":"Invalid, expired, or replayed state"},"429":{"description":"Valid authorization, but the monthly X integration cap is exhausted"},"503":{"description":"Integrations are disabled, or valid authorization could not finalize token exchange, identity billing, or activation"}},"summary":"Complete native X OAuth authorization","tags":["Integrations"]}},"/api/v1/integrations/{id}":{"delete":{"description":"Revokes the connection at the vendor then soft-deletes it. The local revoke succeeds even if the vendor call fails.","parameters":[{"description":"Connection ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Disconnected"},"401":{"description":"Unauthorized"},"404":{"description":"Not found or feature disabled"}},"summary":"Disconnect an integration connection","tags":["Integrations"]},"get":{"description":"Returns a single connection; pending rows lazily sync vendor auth state (the Desktop app polls this during the OAuth window).","parameters":[{"description":"Connection ID","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationConnection"}}},"description":"Connection"},"401":{"description":"Unauthorized"},"404":{"description":"Not found or feature disabled"}},"summary":"Get one integration connection","tags":["Integrations"]}},"/api/v1/integrations/{provider}/connect":{"post":{"description":"Creates a provider auth flow and returns an oauth_url the user opens to authorize. Broker-routed providers (Composio custody) may additionally require declared connect params (see requestBody); native X uses OAuth 2.0 confidential-client PKCE with encrypted token custody, and users only approve in x.com — they never supply developer keys. Shannon stores no broker credentials, only an opaque reference. A provider missing server-side configuration returns provider_unavailable without creating pending state.","parameters":[{"description":"Provider slug (e.g. notion, figma, google_calendar)","in":"path","name":"provider","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"params":{"additionalProperties":{"type":"string"},"description":"Provider-declared connect parameters; undeclared keys are ignored.","type":"object"}},"type":"object"}}},"description":"Optional connect-time parameters, forwarded to the vendor as connection initiation fields. Providers may declare required params in their config (connect_params); e.g. shopify requires {\"params\": {\"subdomain\": \"\u003cstore\u003e\"}} (the store subdomain only, not the full myshopify.com domain), and salesforce/jira/confluence each require a subdomain too. Requests missing a declared param are rejected with 400 naming it; a value failing the declared format returns 400 with a dedicated code (e.g. invalid_shop_domain).","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"connection_id":{"format":"uuid","type":"string"},"oauth_url":{"description":"URL the user opens to authorize with the vendor","type":"string"},"status":{"enum":["pending"],"type":"string"}},"type":"object"}}},"description":"Authorization flow started: open oauth_url, then poll GET /api/v1/integrations/{id} until status becomes active. status is always \"pending\" here; the field is kept so clients can switch on it rather than infer the flow from oauth_url presence."},"400":{"description":"Unknown provider, missing/oversized declared connect parameter, or malformed JSON body"},"401":{"description":"Unauthorized"},"404":{"description":"Feature disabled"},"409":{"description":"Already connected (already_connected) — disconnect first to re-authorize"},"502":{"description":"Vendor failed to start the authorization flow"},"503":{"description":"provider_unavailable"}},"summary":"Start an integration authorization flow","tags":["Integrations"]}},"/api/v1/sessions":{"get":{"description":"Retrieve all sessions for the authenticated user with pagination support","parameters":[{"description":"Maximum number of sessions to return","in":"query","name":"limit","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Number of sessions to skip","in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSessionsResponse"}}},"description":"Sessions retrieved successfully"},"401":{"description":"Unauthorized"}},"summary":"List all sessions"}},"/api/v1/sessions/{sessionId}":{"delete":{"description":"Marks the session as deleted; data remains and is filtered from reads.","parameters":[{"description":"Session UUID","in":"path","name":"sessionId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Session deleted (idempotent)"},"400":{"description":"Invalid session ID"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - Not the owner"},"404":{"description":"Session not found"}},"summary":"Delete session (soft delete)"},"get":{"description":"Retrieve session information including token usage and task count","parameters":[{"description":"Session UUID","in":"path","name":"sessionId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}},"description":"Session metadata retrieved"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - User does not have access to this session"},"404":{"description":"Session not found"}},"summary":"Get session metadata"},"patch":{"description":"Update the title of an existing session (max 60 characters)","parameters":[{"description":"Session UUID","in":"path","name":"sessionId","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSessionTitleRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSessionTitleResponse"}}},"description":"Session title updated successfully"},"400":{"description":"Invalid request - title empty or too long"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - Not the owner"},"404":{"description":"Session not found"}},"summary":"Update session title"}},"/api/v1/sessions/{sessionId}/events":{"get":{"description":"Returns chat history grouped by task/turn, with full events per turn. Excludes LLM_PARTIAL.","parameters":[{"description":"Session ID (UUID or external_id)","in":"path","name":"sessionId","required":true,"schema":{"type":"string"}},{"description":"Maximum number of turns to return","in":"query","name":"limit","schema":{"default":10,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Number of turns to skip","in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionTurnsResponse"}}},"description":"Session turns retrieved"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - User does not have access to this session"},"404":{"description":"Session not found"}},"summary":"Get session events grouped by turn"}},"/api/v1/sessions/{sessionId}/history":{"get":{"description":"Retrieve all tasks in a session with full execution details","parameters":[{"description":"Session UUID","in":"path","name":"sessionId","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionHistoryResponse"}}},"description":"Session task history retrieved"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden - User does not have access to this session"},"404":{"description":"Session not found"}},"summary":"Get session task history"}},"/api/v1/stream/sse":{"get":{"description":"Server-Sent Events stream for workflow events","parameters":[{"description":"Workflow ID to stream events for","in":"query","name":"workflow_id","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}}],"responses":{"200":{"description":"Event stream"}},"summary":"SSE stream"}},"/api/v1/tasks":{"get":{"description":"List tasks for the authenticated user (optionally filter by session/status)","parameters":[{"in":"query","name":"limit","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}},{"in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}},{"in":"query","name":"status","schema":{"enum":["QUEUED","RUNNING","COMPLETED","FAILED","CANCELLED","TIMEOUT"],"type":"string"}},{"in":"query","name":"session_id","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTasksResponse"}}},"description":"List of tasks"}},"summary":"List tasks"},"post":{"description":"Submit a new task for processing","parameters":[{"description":"Unique key for idempotent requests","in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskResponse"}}},"description":"Task submitted successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"429":{"description":"Rate limit exceeded"}},"summary":"Submit a task"}},"/api/v1/tasks/stream":{"post":{"description":"Submit a task and immediately receive the workflow ID and SSE stream URL for real-time updates. This is a convenience endpoint that combines task submission with stream URL generation in a single call.","parameters":[{"description":"Unique key for idempotent requests","in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskStreamResponse"}}},"description":"Task submitted successfully with stream URL","headers":{"Link":{"description":"Link header with stream URL (rel=stream)","schema":{"type":"string"}},"X-Session-ID":{"description":"The session ID associated with this task","schema":{"type":"string"}},"X-Workflow-ID":{"description":"The workflow ID for this task","schema":{"type":"string"}}}},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"429":{"description":"Rate limit exceeded"}},"summary":"Submit task and get stream URL"}},"/api/v1/tasks/{id}":{"get":{"description":"Get the status of a submitted task","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskStatusResponse"}}},"description":"Task status retrieved"},"404":{"description":"Task not found"}},"summary":"Get task status"}},"/api/v1/tasks/{id}/cancel":{"post":{"description":"Cancel a running or queued task. Returns 202 Accepted for async cancellation, 409 if task is already in terminal state, 404 if not found.","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelTaskRequest"}}},"required":false},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelTaskResponse"}}},"description":"Cancellation accepted"},"401":{"description":"Unauthorized - Invalid or missing API key"},"403":{"description":"Forbidden - User does not have permission to cancel this task"},"404":{"description":"Task not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelTaskResponse"}}},"description":"Task already in terminal state"},"429":{"description":"Rate limit exceeded"}},"summary":"Cancel a task"}},"/api/v1/tasks/{id}/events":{"get":{"description":"Get historical events for a task","parameters":[{"in":"path","name":"id","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},{"in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"count":{"type":"integer"},"events":{"items":{"$ref":"#/components/schemas/TaskEvent"},"type":"array"}},"type":"object"}}},"description":"Task events"}},"summary":"Get task events"}},"/api/v1/tasks/{id}/stream":{"get":{"description":"Stream real-time events for a task via Server-Sent Events","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"schema":{"maxLength":128,"minLength":1,"type":"string"}},{"description":"Comma-separated list of event types to filter","in":"query","name":"types","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"Event stream established"}},"summary":"Stream task events"}},"/api/v1/tasks/{id}/timeline":{"get":{"description":"Derive a human-readable timeline from Temporal history; optionally persist asynchronously","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"query","name":"run_id","schema":{"type":"string"}},{"in":"query","name":"mode","schema":{"default":"summary","enum":["summary","full"],"type":"string"}},{"in":"query","name":"include_payloads","schema":{"default":false,"type":"boolean"}},{"in":"query","name":"persist","schema":{"default":true,"type":"boolean"}}],"responses":{"200":{"description":"Timeline returned inline"},"202":{"description":"Accepted for async persistence"}},"summary":"Build timeline from Temporal history"}},"/api/v1/tools":{"get":{"description":"Returns tool schemas (name, description, parameters). Dangerous tools are excluded.","parameters":[{"description":"Filter by category","in":"query","name":"category","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"properties":{"description":{"type":"string"},"name":{"type":"string"},"parameters":{"type":"object"}},"type":"object"},"type":"array"}}},"description":"List of available tools"}},"summary":"List available tools","tags":["Tools"]}},"/api/v1/tools/{name}":{"get":{"description":"Returns merged metadata and parameter schema. Returns 403 for dangerous tools.","parameters":[{"description":"Tool name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"description":{"type":"string"},"name":{"type":"string"},"parameters":{"type":"object"}},"type":"object"}}},"description":"Tool details"},"403":{"description":"Tool not available (dangerous)"},"404":{"description":"Tool not found"}},"summary":"Get tool details","tags":["Tools"]}},"/api/v1/tools/{name}/execute":{"post":{"description":"Execute a tool without full task orchestration. Dangerous tools are blocked. Usage is recorded against quota.","parameters":[{"description":"Tool name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"arguments":{"description":"Tool-specific parameters","type":"object"},"session_id":{"description":"Optional session ID for context","type":"string"}},"required":["arguments"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string"},"execution_time_ms":{"type":"integer"},"metadata":{"description":"Tool-specific metadata from execution","type":"object"},"output":{},"success":{"type":"boolean"},"usage":{"properties":{"cost_usd":{"format":"double","type":"number"},"tokens":{"type":"integer"}},"type":"object"}},"type":"object"}}},"description":"Tool execution result"},"403":{"description":"Tool not available (dangerous)"},"404":{"description":"Tool not found"},"429":{"description":"Rate limit or quota exceeded"}},"summary":"Execute a tool directly","tags":["Tools"]}},"/api/v1/uploads":{"get":{"description":"Paginated list of files the caller previously uploaded (and has not deleted). Cross-tenant and cross-user rows are filtered out.","parameters":[{"description":"Maximum number of rows to return","in":"query","name":"limit","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}},{"description":"Number of rows to skip","in":"query","name":"offset","schema":{"default":0,"minimum":0,"type":"integer"}},{"description":"Filter to one business kind. Invalid values return 400.","in":"query","name":"kind","schema":{"enum":["session_share","report","landing_page","image","agent_avatar","other"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"total_count":{"type":"integer"},"uploads":{"items":{"properties":{"content_type":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"filename":{"type":"string"},"id":{"format":"uuid","type":"string"},"kind":{"enum":["session_share","report","landing_page","image","agent_avatar","other"],"type":"string"},"metadata":{"type":"object"},"size":{"type":"integer"},"updated_at":{"format":"date-time","type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"List of uploads"},"400":{"description":"Invalid kind filter"},"401":{"description":"Unauthorized"}},"summary":"List current user's active uploads","tags":["Uploads"]},"post":{"description":"Stores a multipart-uploaded file in the static-asset bucket and returns a publicly-readable CDN URL. tenant_id and user_id are preserved in S3 object metadata for audit. A row is also written to user_uploads so the file can later be listed and deleted by the same user.","requestBody":{"content":{"multipart/form-data":{"schema":{"properties":{"content_type":{"description":"Override stored Content-Type. Defaults to part header, extension sniff, then application/octet-stream.","type":"string"},"file":{"description":"The file binary (any MIME type).","format":"binary","type":"string"},"filename":{"description":"Override filename used in the S3 key. Defaults to the multipart filename.","type":"string"},"kind":{"default":"other","description":"Business classification used by GET ?kind= to filter. Whitelist-enforced; new values require a DB migration.","enum":["session_share","report","landing_page","image","agent_avatar","other"],"type":"string"},"metadata":{"description":"Free-form JSON object (e.g. {\"session_id\":\"...\"}) up to 8 KiB. Must parse as a JSON object — arrays/scalars are rejected.","type":"string"}},"required":["file"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"content_type":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"description":"user_uploads row id — pass to DELETE /api/v1/uploads/{id} to retract.","format":"uuid","type":"string"},"key":{"type":"string"},"kind":{"enum":["session_share","report","landing_page","image","agent_avatar","other"],"type":"string"},"metadata":{"description":"Caller-supplied JSON metadata, canonicalized.","type":"object"},"size":{"type":"integer"},"updated_at":{"format":"date-time","type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"}}},"description":"Upload accepted; file is now publicly readable at `url`."},"400":{"description":"Missing or malformed multipart body, invalid kind, or invalid metadata"},"401":{"description":"Unauthorized"},"413":{"description":"File exceeds MAX_UPLOAD_BYTES"},"500":{"description":"S3 PUT or DB INSERT failed (on DB failure the S3 object is rolled back)"}},"summary":"Upload a file to the public CDN","tags":["Uploads"]}},"/api/v1/uploads/ephemeral":{"post":{"description":"Same as POST /api/v1/uploads, but writes NO user_uploads row. The file is not part of the resource library: it cannot be listed via GET /api/v1/uploads, nor patched or deleted via the API. Its only handle is the returned unguessable CDN URL; cleanup is governed by the bucket/CDN lifecycle policy. tenant_id and user_id are still preserved in S3 object metadata for audit.","requestBody":{"content":{"multipart/form-data":{"schema":{"properties":{"content_type":{"description":"Override stored Content-Type. Defaults to part header, extension sniff, then application/octet-stream.","type":"string"},"file":{"description":"The file binary (any MIME type).","format":"binary","type":"string"},"filename":{"description":"Override filename used in the S3 key. Defaults to the multipart filename.","type":"string"}},"required":["file"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"content_type":{"type":"string"},"filename":{"type":"string"},"key":{"type":"string"},"size":{"type":"integer"},"url":{"format":"uri","type":"string"}},"type":"object"}}},"description":"Upload accepted; file is now publicly readable at `url`. No tracking row is created."},"400":{"description":"Missing or malformed multipart body"},"401":{"description":"Unauthorized"},"413":{"description":"File exceeds MAX_UPLOAD_BYTES"},"500":{"description":"S3 PUT failed"}},"summary":"Upload a file to the public CDN without tracking it","tags":["Uploads"]}},"/api/v1/uploads/{id}":{"delete":{"description":"Soft-deletes the user_uploads row and hard-deletes every S3 version + delete marker for the underlying object. The CDN cache may still serve the file for up to `cdn_eviction_seconds` after success. Only the original uploader can call this — cross-user or cross-tenant attempts return 404 (not 403) to avoid existence leakage.","parameters":[{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"cdn_eviction_seconds":{"description":"Maximum window during which the CDN may still serve the deleted file from edge cache.","type":"integer"},"deleted":{"type":"boolean"},"id":{"format":"uuid","type":"string"}},"type":"object"}}},"description":"Upload retracted"},"401":{"description":"Unauthorized"},"404":{"description":"Upload not found, already deleted, or owned by another user"},"500":{"description":"S3 delete failed; DB row preserved for retry"}},"summary":"Retract an upload (owner-only)","tags":["Uploads"]},"patch":{"description":"DB-only update. The S3 object and its CDN URL are NOT rewritten — already-shared links keep resolving to the same bytes after a rename. Any combination of `filename`, `kind`, and `metadata` may be sent; at least one is required. Cross-user / cross-tenant requests return 404 (same as DELETE).","parameters":[{"in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"description":"At least one of filename, kind, metadata is required.","properties":{"filename":{"description":"New display filename. Same sanitization rules as POST. The CDN URL path segment does NOT change.","type":"string"},"kind":{"description":"New business classification.","enum":["session_share","report","landing_page","image","agent_avatar","other"],"type":"string"},"metadata":{"description":"New free-form JSON object (replaces previous value entirely; not merged). Up to 8 KiB.","type":"object"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"content_type":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"filename":{"type":"string"},"id":{"format":"uuid","type":"string"},"kind":{"type":"string"},"metadata":{"type":"object"},"size":{"type":"integer"},"updated_at":{"format":"date-time","type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"}}},"description":"Updated row (same shape as a LIST item plus `updated_at`)."},"400":{"description":"Empty PATCH, invalid filename / kind / metadata, or malformed JSON"},"401":{"description":"Unauthorized"},"404":{"description":"Upload not found, already deleted, or owned by another user"},"413":{"description":"PATCH body exceeds 32 KiB"},"500":{"description":"DB update failed"}},"summary":"Update an upload's filename / kind / metadata (owner-only)","tags":["Uploads"]}},"/health":{"get":{"description":"Check if the gateway is healthy","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Gateway is healthy"}},"security":[],"summary":"Health check"}}},"security":[{"apiKey":[]}],"servers":[{"description":"Local development server","url":"http://localhost:8080"},{"description":"Production server","url":"https://api.shannon.ai"}]}
