From 13ea4fa32d7959ed3f306bb1e145da2d52b5cb07 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Wed, 25 Mar 2026 22:40:58 -0700 Subject: [PATCH 1/7] Added support for OAuth SSO and profiles for account switching --- README.md | 50 +- openapi/seclai.openapi.json | 1846 ++++++++++++++++++++------------ seclai/__init__.py | 25 + seclai/auth.py | 571 ++++++++++ seclai/seclai.py | 182 +++- tests/test_auth_and_headers.py | 77 ++ 6 files changed, 2013 insertions(+), 738 deletions(-) create mode 100644 seclai/auth.py diff --git a/README.md b/README.md index 81a2390..6b72655 100644 --- a/README.md +++ b/README.md @@ -68,24 +68,58 @@ asyncio.run(main()) | Option | Environment variable | Default | | --- | --- | --- | -| `api_key` | `SECLAI_API_KEY` | *required* | +| `api_key` | `SECLAI_API_KEY` | — | +| `access_token` | — | — | +| `profile` | `SECLAI_PROFILE` | `"default"` | +| `config_dir` | `SECLAI_CONFIG_DIR` | `~/.seclai` | +| `auto_refresh` | — | `True` | +| `account_id` | — | — | | `timeout` | — | `30.0` (seconds) | | `api_key_header` | — | `x-api-key` | | `default_headers` | — | `None` | | `http_client` | — | `None` (auto-created `httpx.Client`) | +Set `SECLAI_API_URL` to point at a different API host (e.g., staging): + +```bash +export SECLAI_API_URL="https://staging-api.seclai.com" +``` + +### Authentication + +Credentials are resolved via a chain (first match wins): + +1. Explicit `api_key` option +2. Explicit `access_token` option (string or callable) +3. `SECLAI_API_KEY` environment variable +4. SSO profile from `~/.seclai/config` with cached tokens in `~/.seclai/sso/cache/` + ```python -client = Seclai( - api_key="...", - timeout=60.0, - default_headers={"x-custom": "value"}, -) +# API key +client = Seclai(api_key="sk-...") + +# Static bearer token +client = Seclai(access_token="eyJhbGciOi...") + +# Dynamic bearer token provider (sync or async callable) +client = Seclai(access_token=lambda: get_token_from_vault()) + +# Async provider with AsyncSeclai +client = AsyncSeclai(access_token=get_token_async) + +# SSO profile (uses cached tokens, auto-refreshes) +client = Seclai(profile="my-profile") + +# Environment variable (no options needed) +# export SECLAI_API_KEY="sk-..." +client = Seclai() ``` -Set `SECLAI_API_URL` to point at a different API host (e.g., staging): +To set up SSO authentication, install the [Seclai CLI](https://pypi.org/project/seclai/) and run: ```bash -export SECLAI_API_URL="https://staging-api.seclai.com" +seclai configure sso # set up an SSO profile +seclai auth login # authenticate via browser ``` ## API documentation diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 4e39803..1ab345e 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -1,5 +1,17 @@ { "components": { + "parameters": { + "X-Account-Id": { + "description": "Target a different organization account (OAuth only). When omitted, the user's default account is used. Ignored for API key authentication \u2014 the key's account is always used.", + "in": "header", + "name": "X-Account-Id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + } + }, "schemas": { "AddConversationTurnRequest": { "properties": { @@ -710,6 +722,62 @@ "title": "AgentTraceSearchResponse", "type": "object" }, + "AiAssistantAcceptResponse": { + "description": "Response from accepting and executing a plan.", + "properties": { + "conversation_id": { + "description": "Conversation ID.", + "format": "uuid", + "title": "Conversation Id", + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Error message if failed.", + "title": "Error" + }, + "executed_actions": { + "description": "Results of each executed action.", + "items": { + "$ref": "#/components/schemas/ExecutedActionResponse" + }, + "title": "Executed Actions", + "type": "array" + }, + "solution_id": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Solution ID when a new solution was auto-created.", + "title": "Solution Id" + }, + "success": { + "default": true, + "description": "Whether execution succeeded.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "executed_actions" + ], + "title": "AiAssistantAcceptResponse", + "type": "object" + }, "AiAssistantFeedbackResponse": { "description": "Response after submitting feedback.", "properties": { @@ -756,6 +824,57 @@ "title": "AiAssistantGenerateRequest", "type": "object" }, + "AiAssistantGenerateResponse": { + "description": "Response from an AI assistant generate endpoint.", + "properties": { + "conversation_id": { + "description": "Conversation ID for accept/decline.", + "format": "uuid", + "title": "Conversation Id", + "type": "string" + }, + "example_prompts": { + "description": "Example natural-language prompts that demonstrate the capabilities of this AI assistant.", + "items": { + "$ref": "#/components/schemas/ExamplePrompt" + }, + "title": "Example Prompts", + "type": "array" + }, + "note": { + "description": "AI-generated note about the plan.", + "title": "Note", + "type": "string" + }, + "proposed_actions": { + "description": "List of proposed actions.", + "items": { + "$ref": "#/components/schemas/ProposedActionResponse" + }, + "title": "Proposed Actions", + "type": "array" + }, + "requires_delete_confirmation": { + "default": false, + "description": "Whether destructive actions require explicit confirmation.", + "title": "Requires Delete Confirmation", + "type": "boolean" + }, + "success": { + "default": false, + "description": "Whether plan generation succeeded.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "note", + "proposed_actions" + ], + "title": "AiAssistantGenerateResponse", + "type": "object" + }, "AiConversationHistoryResponse": { "properties": { "total": { @@ -865,6 +984,57 @@ "title": "AiConversationTurnResponse", "type": "object" }, + "AppliedActionResponse": { + "description": "Result of a single executed governance action.", + "properties": { + "action_type": { + "description": "Type of action that was executed.", + "title": "Action Type", + "type": "string" + }, + "description": { + "description": "Human-readable description of the executed action.", + "title": "Description", + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Error message if this action failed, or null.", + "title": "Error" + }, + "policy_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ID of the policy that was created or modified, or null.", + "title": "Policy Id" + }, + "success": { + "description": "Whether this individual action succeeded.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "action_type", + "description", + "success" + ], + "title": "AppliedActionResponse", + "type": "object" + }, "Body_upload_file_to_content_api_contents__source_connection_content_version__upload_post": { "properties": { "file": { @@ -2408,6 +2578,69 @@ }, "type": "object" }, + "ExecutedActionResponse": { + "description": "A single executed action result.", + "properties": { + "action_type": { + "description": "Type of the executed action.", + "title": "Action Type", + "type": "string" + }, + "description": { + "description": "Human-readable description.", + "title": "Description", + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Error message if failed.", + "title": "Error" + }, + "resource_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ID of the affected resource.", + "title": "Resource Id" + }, + "resource_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Type of the affected resource.", + "title": "Resource Type" + }, + "success": { + "default": true, + "description": "Whether the action succeeded.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "action_type", + "description" + ], + "title": "ExecutedActionResponse", + "type": "object" + }, "ExportFormat": { "description": "Supported export file formats.", "enum": [ @@ -2669,23 +2902,23 @@ "title": "GenerateStepConfigResponse", "type": "object" }, - "HTTPValidationError": { + "GovernanceAiAcceptResponse": { + "description": "Response from accepting a governance AI assistant plan.", "properties": { - "detail": { + "actions_applied": { + "description": "Results of each action that was executed.", "items": { - "$ref": "#/components/schemas/ValidationError" + "$ref": "#/components/schemas/AppliedActionResponse" }, - "title": "Detail", + "title": "Actions Applied", "type": "array" - } - }, - "title": "HTTPValidationError", - "type": "object" - }, - "InlineTextReplaceRequest": { - "description": "Request model for inline text content replacement.", - "properties": { - "content_type": { + }, + "conversation_id": { + "description": "Conversation ID that was accepted.", + "title": "Conversation Id", + "type": "string" + }, + "error": { "anyOf": [ { "type": "string" @@ -2694,19 +2927,117 @@ "type": "null" } ], - "default": "text/plain", - "description": "MIME type for the text content", - "title": "Content Type" + "description": "Overall error message if the plan failed, or null.", + "title": "Error" }, - "metadata": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } + "success": { + "description": "Whether all actions were applied successfully.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "actions_applied", + "success" + ], + "title": "GovernanceAiAcceptResponse", + "type": "object" + }, + "GovernanceAiAssistantResponse": { + "description": "Response from the governance AI assistant generate endpoint.", + "properties": { + "conversation_id": { + "description": "Conversation ID to accept or decline this plan.", + "title": "Conversation Id", + "type": "string" + }, + "example_prompts": { + "description": "Example natural-language prompts that demonstrate the capabilities of the governance AI assistant.", + "items": { + "$ref": "#/components/schemas/ExamplePrompt" + }, + "title": "Example Prompts", + "type": "array" + }, + "note": { + "description": "AI-generated summary of the proposed changes.", + "title": "Note", + "type": "string" + }, + "prompt_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Prompt call ID for credit tracking, or null.", + "title": "Prompt Call Id" + }, + "proposed_actions": { + "description": "Ordered list of policy actions the AI proposes to execute.", + "items": { + "$ref": "#/components/schemas/ProposedPolicyActionResponse" + }, + "title": "Proposed Actions", + "type": "array" + }, + "success": { + "description": "Whether the plan was generated successfully.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "note", + "proposed_actions", + "success" + ], + "title": "GovernanceAiAssistantResponse", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "InlineTextReplaceRequest": { + "description": "Request model for inline text content replacement.", + "properties": { + "content_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "text/plain", + "description": "MIME type for the text content", + "title": "Content Type" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } ], "description": "Optional metadata object", "title": "Metadata" @@ -2979,87 +3310,39 @@ "title": "MarkConversationTurnRequest", "type": "object" }, - "MemoryBankListResponseModel": { - "description": "Paginated list of memory banks.", - "properties": { - "limit": { - "description": "Items per page.", - "title": "Limit", - "type": "integer" - }, - "memory_banks": { - "description": "List of memory banks on this page.", - "items": { - "$ref": "#/components/schemas/MemoryBankResponseModel" - }, - "title": "Memory Banks", - "type": "array" - }, - "page": { - "description": "Current page number (1-based).", - "title": "Page", - "type": "integer" - }, - "total": { - "description": "Total number of memory banks.", - "title": "Total", - "type": "integer" - } - }, - "required": [ - "memory_banks", - "page", - "limit", - "total" - ], - "title": "MemoryBankListResponseModel", - "type": "object" - }, - "MemoryBankResponseModel": { - "description": "Response model for a single memory bank.", + "MemoryBankAiAssistantResponse": { + "description": "Response from the memory bank AI assistant.", "properties": { - "chunk_overlap": { + "config": { "anyOf": [ { - "type": "integer" + "$ref": "#/components/schemas/MemoryBankConfigResponse" }, { "type": "null" } ], - "description": "Character overlap between chunks.", - "title": "Chunk Overlap" + "description": "Proposed configuration, or null." }, - "chunk_size": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Characters per chunk.", - "title": "Chunk Size" + "conversation_id": { + "description": "Conversation ID for follow-up.", + "title": "Conversation Id", + "type": "string" }, - "compaction_prompt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Custom prompt used when compacting older entries. When set, entries that exceed a threshold are summarized into a new entry before being soft-deleted.", - "title": "Compaction Prompt" + "example_prompts": { + "description": "Example natural-language prompts that demonstrate the capabilities of the memory bank AI assistant.", + "items": { + "$ref": "#/components/schemas/ExamplePrompt" + }, + "title": "Example Prompts", + "type": "array" }, - "created_at": { - "description": "ISO-8601 creation timestamp.", - "title": "Created At", + "note": { + "description": "AI-generated explanation.", + "title": "Note", "type": "string" }, - "description": { + "prompt_call_id": { "anyOf": [ { "type": "string" @@ -3068,22 +3351,39 @@ "type": "null" } ], - "description": "Optional description of the memory bank's purpose.", - "title": "Description" + "description": "Prompt call ID for credit tracking.", + "title": "Prompt Call Id" }, - "dimensions": { + "success": { + "default": false, + "description": "Whether generation succeeded.", + "title": "Success", + "type": "boolean" + } + }, + "required": [ + "conversation_id", + "note" + ], + "title": "MemoryBankAiAssistantResponse", + "type": "object" + }, + "MemoryBankConfigResponse": { + "description": "Suggested memory bank configuration from the AI assistant.", + "properties": { + "compaction_prompt": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "description": "Vector embedding dimensions.", - "title": "Dimensions" + "description": "Suggested compaction prompt.", + "title": "Compaction Prompt" }, - "embedding_model": { + "description": { "anyOf": [ { "type": "string" @@ -3092,13 +3392,8 @@ "type": "null" } ], - "description": "Embedding model identifier.", - "title": "Embedding Model" - }, - "id": { - "description": "Unique memory bank identifier.", - "title": "Id", - "type": "string" + "description": "Suggested description.", + "title": "Description" }, "max_age_days": { "anyOf": [ @@ -3109,7 +3404,7 @@ "type": "null" } ], - "description": "Max entry age in days before compaction. Checked both inline after each write and by the hourly background sweep.", + "description": "Max age in days.", "title": "Max Age Days" }, "max_size_tokens": { @@ -3121,7 +3416,7 @@ "type": "null" } ], - "description": "Max total tokens (per partition) before compaction. Checked both inline after each write and by the hourly background sweep.", + "description": "Max size in tokens.", "title": "Max Size Tokens" }, "max_turns": { @@ -3133,16 +3428,16 @@ "type": "null" } ], - "description": "Max conversation turns (per partition) before compaction. Checked both inline after each write and by the hourly background sweep.", + "description": "Max conversation turns.", "title": "Max Turns" }, "mode": { - "description": "Embedding mode: fast_and_cheap, balanced, slow_and_thorough, or custom.", + "description": "Memory bank mode.", "title": "Mode", "type": "string" }, "name": { - "description": "Human-readable name.", + "description": "Suggested name.", "title": "Name", "type": "string" }, @@ -3155,23 +3450,216 @@ "type": "null" } ], - "description": "Content retention period in days (null = indefinite).", + "description": "Retention in days.", "title": "Retention Days" }, - "source_connection_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Linked content source ID (null if not yet provisioned).", - "title": "Source Connection Id" - }, "type": { - "description": "Bank type: conversation (chat-turn with speaker) or general (flat entries).", + "description": "Memory bank type: conversation or general.", + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type", + "mode" + ], + "title": "MemoryBankConfigResponse", + "type": "object" + }, + "MemoryBankListResponseModel": { + "description": "Paginated list of memory banks.", + "properties": { + "limit": { + "description": "Items per page.", + "title": "Limit", + "type": "integer" + }, + "memory_banks": { + "description": "List of memory banks on this page.", + "items": { + "$ref": "#/components/schemas/MemoryBankResponseModel" + }, + "title": "Memory Banks", + "type": "array" + }, + "page": { + "description": "Current page number (1-based).", + "title": "Page", + "type": "integer" + }, + "total": { + "description": "Total number of memory banks.", + "title": "Total", + "type": "integer" + } + }, + "required": [ + "memory_banks", + "page", + "limit", + "total" + ], + "title": "MemoryBankListResponseModel", + "type": "object" + }, + "MemoryBankResponseModel": { + "description": "Response model for a single memory bank.", + "properties": { + "chunk_overlap": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Character overlap between chunks.", + "title": "Chunk Overlap" + }, + "chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Characters per chunk.", + "title": "Chunk Size" + }, + "compaction_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Custom prompt used when compacting older entries. When set, entries that exceed a threshold are summarized into a new entry before being soft-deleted.", + "title": "Compaction Prompt" + }, + "created_at": { + "description": "ISO-8601 creation timestamp.", + "title": "Created At", + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional description of the memory bank's purpose.", + "title": "Description" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Vector embedding dimensions.", + "title": "Dimensions" + }, + "embedding_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Embedding model identifier.", + "title": "Embedding Model" + }, + "id": { + "description": "Unique memory bank identifier.", + "title": "Id", + "type": "string" + }, + "max_age_days": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Max entry age in days before compaction. Checked both inline after each write and by the hourly background sweep.", + "title": "Max Age Days" + }, + "max_size_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Max total tokens (per partition) before compaction. Checked both inline after each write and by the hourly background sweep.", + "title": "Max Size Tokens" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Max conversation turns (per partition) before compaction. Checked both inline after each write and by the hourly background sweep.", + "title": "Max Turns" + }, + "mode": { + "description": "Embedding mode: fast_and_cheap, balanced, slow_and_thorough, or custom.", + "title": "Mode", + "type": "string" + }, + "name": { + "description": "Human-readable name.", + "title": "Name", + "type": "string" + }, + "retention_days": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Content retention period in days (null = indefinite).", + "title": "Retention Days" + }, + "source_connection_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Linked content source ID (null if not yet provisioned).", + "title": "Source Connection Id" + }, + "type": { + "description": "Bank type: conversation (chat-turn with speaker) or general (flat entries).", "title": "Type", "type": "string" }, @@ -3272,6 +3760,68 @@ "title": "PromptModelAutoUpgradeStrategy", "type": "string" }, + "ProposedActionResponse": { + "description": "A single proposed action.", + "properties": { + "action_type": { + "description": "Type of the proposed action.", + "title": "Action Type", + "type": "string" + }, + "description": { + "description": "Human-readable description of the action.", + "title": "Description", + "type": "string" + }, + "is_destructive": { + "default": false, + "description": "Whether the action is destructive.", + "title": "Is Destructive", + "type": "boolean" + }, + "params": { + "additionalProperties": true, + "description": "Parameters for the action.", + "title": "Params", + "type": "object" + } + }, + "required": [ + "action_type", + "params", + "description" + ], + "title": "ProposedActionResponse", + "type": "object" + }, + "ProposedPolicyActionResponse": { + "description": "A single proposed governance policy action.", + "properties": { + "action_type": { + "description": "Type of action: create, update, delete, enable, or disable.", + "title": "Action Type", + "type": "string" + }, + "description": { + "description": "Human-readable description of what this action will do.", + "title": "Description", + "type": "string" + }, + "params": { + "additionalProperties": true, + "description": "Parameters for the action (e.g. policy_document_id, thresholds).", + "title": "Params", + "type": "object" + } + }, + "required": [ + "action_type", + "description", + "params" + ], + "title": "ProposedPolicyActionResponse", + "type": "object" + }, "SolutionSourceConnectionResponse": { "properties": { "id": { @@ -5415,101 +5965,7 @@ "title": "Filename", "type": "string" }, - "source_connection_content_version_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "ID of the source connection content version", - "title": "Source Connection Content Version Id" - }, - "status": { - "description": "Processing status", - "title": "Status", - "type": "string" - } - }, - "required": [ - "content_version_id", - "source_connection_content_version_id", - "filename", - "status" - ], - "title": "FileUploadResponse", - "type": "object" - }, - "routers__api__governance__AppliedActionResponse": { - "description": "Result of a single executed governance action.", - "properties": { - "action_type": { - "description": "Type of action that was executed.", - "title": "Action Type", - "type": "string" - }, - "description": { - "description": "Human-readable description of the executed action.", - "title": "Description", - "type": "string" - }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Error message if this action failed, or null.", - "title": "Error" - }, - "policy_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "ID of the policy that was created or modified, or null.", - "title": "Policy Id" - }, - "success": { - "description": "Whether this individual action succeeded.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "action_type", - "description", - "success" - ], - "title": "AppliedActionResponse", - "type": "object" - }, - "routers__api__governance__GovernanceAiAcceptResponse": { - "description": "Response from accepting a governance AI assistant plan.", - "properties": { - "actions_applied": { - "description": "Results of each action that was executed.", - "items": { - "$ref": "#/components/schemas/routers__api__governance__AppliedActionResponse" - }, - "title": "Actions Applied", - "type": "array" - }, - "conversation_id": { - "description": "Conversation ID that was accepted.", - "title": "Conversation Id", - "type": "string" - }, - "error": { + "source_connection_content_version_id": { "anyOf": [ { "type": "string" @@ -5518,21 +5974,22 @@ "type": "null" } ], - "description": "Overall error message if the plan failed, or null.", - "title": "Error" + "description": "ID of the source connection content version", + "title": "Source Connection Content Version Id" }, - "success": { - "description": "Whether all actions were applied successfully.", - "title": "Success", - "type": "boolean" + "status": { + "description": "Processing status", + "title": "Status", + "type": "string" } }, "required": [ - "conversation_id", - "actions_applied", - "success" + "content_version_id", + "source_connection_content_version_id", + "filename", + "status" ], - "title": "GovernanceAiAcceptResponse", + "title": "FileUploadResponse", "type": "object" }, "routers__api__governance__GovernanceAiAssistantRequest": { @@ -5550,62 +6007,6 @@ "title": "GovernanceAiAssistantRequest", "type": "object" }, - "routers__api__governance__GovernanceAiAssistantResponse": { - "description": "Response from the governance AI assistant generate endpoint.", - "properties": { - "conversation_id": { - "description": "Conversation ID to accept or decline this plan.", - "title": "Conversation Id", - "type": "string" - }, - "example_prompts": { - "description": "Example natural-language prompts that demonstrate the capabilities of the governance AI assistant.", - "items": { - "$ref": "#/components/schemas/ExamplePrompt" - }, - "title": "Example Prompts", - "type": "array" - }, - "note": { - "description": "AI-generated summary of the proposed changes.", - "title": "Note", - "type": "string" - }, - "prompt_call_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Prompt call ID for credit tracking, or null.", - "title": "Prompt Call Id" - }, - "proposed_actions": { - "description": "Ordered list of policy actions the AI proposes to execute.", - "items": { - "$ref": "#/components/schemas/routers__api__governance__ProposedPolicyActionResponse" - }, - "title": "Proposed Actions", - "type": "array" - }, - "success": { - "description": "Whether the plan was generated successfully.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "conversation_id", - "note", - "proposed_actions", - "success" - ], - "title": "GovernanceAiAssistantResponse", - "type": "object" - }, "routers__api__governance__GovernanceConversationResponse": { "description": "A governance AI assistant conversation entry.", "properties": { @@ -5673,34 +6074,6 @@ "title": "GovernanceConversationResponse", "type": "object" }, - "routers__api__governance__ProposedPolicyActionResponse": { - "description": "A single proposed governance policy action.", - "properties": { - "action_type": { - "description": "Type of action: create, update, delete, enable, or disable.", - "title": "Action Type", - "type": "string" - }, - "description": { - "description": "Human-readable description of what this action will do.", - "title": "Description", - "type": "string" - }, - "params": { - "additionalProperties": true, - "description": "Parameters for the action (e.g. policy_document_id, thresholds).", - "title": "Params", - "type": "object" - } - }, - "required": [ - "action_type", - "description", - "params" - ], - "title": "ProposedPolicyActionResponse", - "type": "object" - }, "routers__api__memory_banks__MemoryBankAcceptRequest": { "description": "Accept or decline a memory bank AI suggestion.", "properties": { @@ -5731,186 +6104,29 @@ "description": "Previous conversation ID to continue.", "title": "Conversation Id" }, - "current_config": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "description": "Current configuration to refine, if any.", - "title": "Current Config" - }, - "user_input": { - "description": "Natural-language description of the memory bank.", - "title": "User Input", - "type": "string" - } - }, - "required": [ - "user_input" - ], - "title": "MemoryBankAiAssistantRequest", - "type": "object" - }, - "routers__api__memory_banks__MemoryBankAiAssistantResponse": { - "description": "Response from the memory bank AI assistant.", - "properties": { - "config": { - "anyOf": [ - { - "$ref": "#/components/schemas/routers__api__memory_banks__MemoryBankConfigResponse" - }, - { - "type": "null" - } - ], - "description": "Proposed configuration, or null." - }, - "conversation_id": { - "description": "Conversation ID for follow-up.", - "title": "Conversation Id", - "type": "string" - }, - "example_prompts": { - "description": "Example natural-language prompts that demonstrate the capabilities of the memory bank AI assistant.", - "items": { - "$ref": "#/components/schemas/ExamplePrompt" - }, - "title": "Example Prompts", - "type": "array" - }, - "note": { - "description": "AI-generated explanation.", - "title": "Note", - "type": "string" - }, - "prompt_call_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Prompt call ID for credit tracking.", - "title": "Prompt Call Id" - }, - "success": { - "default": false, - "description": "Whether generation succeeded.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "conversation_id", - "note" - ], - "title": "MemoryBankAiAssistantResponse", - "type": "object" - }, - "routers__api__memory_banks__MemoryBankConfigResponse": { - "description": "Suggested memory bank configuration from the AI assistant.", - "properties": { - "compaction_prompt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Suggested compaction prompt.", - "title": "Compaction Prompt" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Suggested description.", - "title": "Description" - }, - "max_age_days": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Max age in days.", - "title": "Max Age Days" - }, - "max_size_tokens": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Max size in tokens.", - "title": "Max Size Tokens" - }, - "max_turns": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "description": "Max conversation turns.", - "title": "Max Turns" - }, - "mode": { - "description": "Memory bank mode.", - "title": "Mode", - "type": "string" - }, - "name": { - "description": "Suggested name.", - "title": "Name", - "type": "string" - }, - "retention_days": { + "current_config": { "anyOf": [ { - "type": "integer" + "additionalProperties": true, + "type": "object" }, { "type": "null" } ], - "description": "Retention in days.", - "title": "Retention Days" + "description": "Current configuration to refine, if any.", + "title": "Current Config" }, - "type": { - "description": "Memory bank type: conversation or general.", - "title": "Type", + "user_input": { + "description": "Natural-language description of the memory bank.", + "title": "User Input", "type": "string" } }, "required": [ - "name", - "type", - "mode" + "user_input" ], - "title": "MemoryBankConfigResponse", + "title": "MemoryBankAiAssistantRequest", "type": "object" }, "routers__api__memory_banks__MemoryBankConversationTurnResponse": { @@ -6065,210 +6281,6 @@ "title": "AiAssistantAcceptRequest", "type": "object" }, - "routers__api__solutions__AiAssistantAcceptResponse": { - "description": "Response from accepting and executing a plan.", - "properties": { - "conversation_id": { - "description": "Conversation ID.", - "format": "uuid", - "title": "Conversation Id", - "type": "string" - }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Error message if failed.", - "title": "Error" - }, - "executed_actions": { - "description": "Results of each executed action.", - "items": { - "$ref": "#/components/schemas/routers__api__solutions__ExecutedActionResponse" - }, - "title": "Executed Actions", - "type": "array" - }, - "solution_id": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Solution ID when a new solution was auto-created.", - "title": "Solution Id" - }, - "success": { - "default": true, - "description": "Whether execution succeeded.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "conversation_id", - "executed_actions" - ], - "title": "AiAssistantAcceptResponse", - "type": "object" - }, - "routers__api__solutions__AiAssistantGenerateResponse": { - "description": "Response from an AI assistant generate endpoint.", - "properties": { - "conversation_id": { - "description": "Conversation ID for accept/decline.", - "format": "uuid", - "title": "Conversation Id", - "type": "string" - }, - "example_prompts": { - "description": "Example natural-language prompts that demonstrate the capabilities of this AI assistant.", - "items": { - "$ref": "#/components/schemas/ExamplePrompt" - }, - "title": "Example Prompts", - "type": "array" - }, - "note": { - "description": "AI-generated note about the plan.", - "title": "Note", - "type": "string" - }, - "proposed_actions": { - "description": "List of proposed actions.", - "items": { - "$ref": "#/components/schemas/routers__api__solutions__ProposedActionResponse" - }, - "title": "Proposed Actions", - "type": "array" - }, - "requires_delete_confirmation": { - "default": false, - "description": "Whether destructive actions require explicit confirmation.", - "title": "Requires Delete Confirmation", - "type": "boolean" - }, - "success": { - "default": false, - "description": "Whether plan generation succeeded.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "conversation_id", - "note", - "proposed_actions" - ], - "title": "AiAssistantGenerateResponse", - "type": "object" - }, - "routers__api__solutions__ExecutedActionResponse": { - "description": "A single executed action result.", - "properties": { - "action_type": { - "description": "Type of the executed action.", - "title": "Action Type", - "type": "string" - }, - "description": { - "description": "Human-readable description.", - "title": "Description", - "type": "string" - }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Error message if failed.", - "title": "Error" - }, - "resource_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "ID of the affected resource.", - "title": "Resource Id" - }, - "resource_type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Type of the affected resource.", - "title": "Resource Type" - }, - "success": { - "default": true, - "description": "Whether the action succeeded.", - "title": "Success", - "type": "boolean" - } - }, - "required": [ - "action_type", - "description" - ], - "title": "ExecutedActionResponse", - "type": "object" - }, - "routers__api__solutions__ProposedActionResponse": { - "description": "A single proposed action.", - "properties": { - "action_type": { - "description": "Type of the proposed action.", - "title": "Action Type", - "type": "string" - }, - "description": { - "description": "Human-readable description of the action.", - "title": "Description", - "type": "string" - }, - "is_destructive": { - "default": false, - "description": "Whether the action is destructive.", - "title": "Is Destructive", - "type": "boolean" - }, - "params": { - "additionalProperties": true, - "description": "Parameters for the action.", - "title": "Params", - "type": "object" - } - }, - "required": [ - "action_type", - "params", - "description" - ], - "title": "ProposedActionResponse", - "type": "object" - }, "routers__api__solutions__SolutionAgentResponse": { "properties": { "id": { @@ -7041,6 +7053,20 @@ "title": "NonManualEvaluationSummaryResponse", "type": "object" } + }, + "securitySchemes": { + "ApiKeyAuth": { + "description": "API key issued from the Seclai dashboard.", + "in": "header", + "name": "X-API-Key", + "type": "apiKey" + }, + "BearerAuth": { + "bearerFormat": "JWT", + "description": "OAuth2 access token from AWS Cognito.", + "scheme": "bearer", + "type": "http" + } } }, "info": { @@ -7081,6 +7107,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7113,6 +7142,11 @@ "post": { "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires `X-API-Key`. Agent is created in the API key's account.", "operationId": "create_agent_api_agents_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -7167,6 +7201,9 @@ "title": "Criteria Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7201,6 +7238,9 @@ "title": "Criteria Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7242,6 +7282,9 @@ "title": "Criteria Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -7335,6 +7378,9 @@ ], "title": "Started After" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7459,6 +7505,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7500,6 +7549,9 @@ "title": "Criteria Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -7553,6 +7605,9 @@ "title": "Criteria Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7631,6 +7686,9 @@ ], "title": "End Date" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7665,6 +7723,11 @@ "post": { "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires `X-API-Key`. Searches only within your account's traces.", "operationId": "search_agent_runs_api_agents_runs_search_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -7716,6 +7779,9 @@ "title": "Run Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7769,6 +7835,9 @@ "title": "Include Step Outputs", "type": "boolean" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7812,6 +7881,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7846,6 +7918,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -7887,6 +7962,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -7996,6 +8074,9 @@ "title": "Offset", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8039,6 +8120,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8092,6 +8176,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8154,6 +8241,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8211,6 +8301,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8252,6 +8345,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8305,6 +8401,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8350,6 +8449,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8403,6 +8505,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8553,6 +8658,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8683,6 +8791,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8735,6 +8846,9 @@ "title": "Upload Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8823,6 +8937,9 @@ "description": "Filter runs by status", "title": "Status" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -8864,6 +8981,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8917,6 +9037,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -8983,6 +9106,9 @@ "title": "Run Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9030,6 +9156,9 @@ "title": "Agent Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9064,6 +9193,11 @@ "post": { "description": "Submit thumbs-up/down feedback on any AI assistant interaction. Negative feedback with a comment is analyzed for concerning issues.\n\nAuth: requires ``X-API-Key``.", "operationId": "api_ai_feedback_api_ai_assistant_feedback_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9106,6 +9240,11 @@ "post": { "description": "Generate a knowledge base creation/modification plan without requiring an existing solution. May also propose prerequisite source creation actions.\n\nAuth: requires ``X-API-Key``.", "operationId": "api_ai_knowledge_base_api_ai_assistant_knowledge_base_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9121,7 +9260,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -9148,6 +9287,11 @@ "post": { "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings.\n\nAuth: requires ``X-API-Key``.", "operationId": "api_ai_memory_bank_api_ai_assistant_memory_bank_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9163,7 +9307,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__memory_banks__MemoryBankAiAssistantResponse" + "$ref": "#/components/schemas/MemoryBankAiAssistantResponse" } } }, @@ -9217,6 +9361,9 @@ "title": "Offset", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9261,6 +9408,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -9309,6 +9459,11 @@ "post": { "description": "Generate a complete solution plan covering sources, knowledge bases, and agents without requiring an existing solution. Supports SSE streaming when ``Accept: text/event-stream`` is set.\n\nAuth: requires ``X-API-Key``.", "operationId": "api_ai_solution_api_ai_assistant_solution_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9324,7 +9479,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -9351,6 +9506,11 @@ "post": { "description": "Generate a content source creation/modification plan without requiring an existing solution. The AI proposes actions for the user to review before any changes are made.\n\nAuth: requires ``X-API-Key``.", "operationId": "api_ai_source_api_ai_assistant_source_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9366,7 +9526,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -9403,6 +9563,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -9420,7 +9583,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantAcceptResponse" + "$ref": "#/components/schemas/AiAssistantAcceptResponse" } } }, @@ -9457,6 +9620,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9603,6 +9769,9 @@ "description": "To (ISO 8601)", "title": "Time To" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9693,6 +9862,9 @@ "description": "Set to 'source' to list account-level source alert configs", "title": "Scope" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9727,6 +9899,11 @@ "post": { "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "create_alert_config_api_alerts_configs_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -9780,6 +9957,9 @@ "title": "Config Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9814,6 +9994,9 @@ "title": "Config Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9857,6 +10040,9 @@ "title": "Config Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -9934,6 +10120,9 @@ "title": "Include Defaults", "type": "boolean" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -9987,6 +10176,9 @@ "title": "Alert Type", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -10040,6 +10232,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10085,6 +10280,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -10140,6 +10338,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -10195,6 +10396,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10240,6 +10444,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10285,6 +10492,9 @@ "title": "Source Connection Content Version", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10339,6 +10549,9 @@ "title": "End", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10380,6 +10593,9 @@ "title": "Source Connection Content Version", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -10453,6 +10669,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10496,6 +10715,9 @@ "title": "Source Connection Content Version", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -10540,6 +10762,11 @@ "post": { "description": "Send a natural-language request to the governance AI assistant to generate a plan of policy changes. Returns a conversation with proposed actions that can be accepted or declined.\n\nAuth: requires `X-API-Key` with governance access.", "operationId": "governance_ai_generate_api_governance_ai_assistant_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -10555,7 +10782,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__governance__GovernanceAiAssistantResponse" + "$ref": "#/components/schemas/GovernanceAiAssistantResponse" } } }, @@ -10602,6 +10829,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10653,6 +10883,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10660,7 +10893,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__governance__GovernanceAiAcceptResponse" + "$ref": "#/components/schemas/GovernanceAiAcceptResponse" } } }, @@ -10703,6 +10936,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10787,6 +11023,9 @@ "title": "Order", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10819,6 +11058,11 @@ "post": { "description": "Create a new knowledge base.\n\nAt least one `source_id` is required. The source connections must belong to the same account.", "operationId": "create_knowledge_base_api_knowledge_bases_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -10873,6 +11117,9 @@ "title": "Knowledge Base Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10907,6 +11154,9 @@ "title": "Knowledge Base Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -10948,6 +11198,9 @@ "title": "Knowledge Base Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -11061,6 +11314,9 @@ "description": "Filter by bank type: conversation or general.", "title": "Type" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11093,6 +11349,11 @@ "post": { "description": "Create a new memory bank.\n\nModes: `fast_and_cheap` (256-dim), `balanced` (512-dim), `slow_and_thorough` (1024-dim), or `custom` (supply your own embedding params).", "operationId": "create_memory_bank_api_memory_banks_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -11135,6 +11396,11 @@ "post": { "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings based on the user's description.\n\nAuth: requires `X-API-Key`.", "operationId": "memory_bank_ai_generate_api_memory_banks_ai_assistant_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -11150,7 +11416,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__memory_banks__MemoryBankAiAssistantResponse" + "$ref": "#/components/schemas/MemoryBankAiAssistantResponse" } } }, @@ -11207,6 +11473,9 @@ "title": "Offset", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11251,6 +11520,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -11299,6 +11571,11 @@ "get": { "description": "Return pre-built template configurations for common memory bank use cases.\n\nEach template includes a name, description, suggested use case, and full default settings that can be used directly with the create endpoint.", "operationId": "list_templates_api_memory_banks_templates_get", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "responses": { "200": { "content": { @@ -11326,6 +11603,11 @@ "post": { "description": "Test a compaction prompt by running the summarizer and evaluating the result with an LLM-as-judge. Returns original entries, compaction summary, surviving entries, and a structured quality evaluation with verdict, score, and reasoning.", "operationId": "test_compaction_prompt_standalone_api_memory_banks_test_compaction_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -11377,6 +11659,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11411,6 +11696,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11452,6 +11740,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -11505,6 +11796,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11555,6 +11849,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11600,6 +11897,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11682,6 +11982,9 @@ ], "title": "End Date" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11727,6 +12030,9 @@ "title": "Memory Bank Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -11828,6 +12134,9 @@ "title": "Offset", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -11864,6 +12173,11 @@ "post": { "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. Scoped to the API key's account.", "operationId": "mark_all_read_api_models_alerts_mark_all_read_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "responses": { "204": { "description": "Successful Response" @@ -11879,6 +12193,11 @@ "get": { "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires `X-API-Key`. Count is scoped to the API key's account.", "operationId": "get_alert_unread_count_api_models_alerts_unread_count_get", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "responses": { "200": { "content": { @@ -11913,6 +12232,9 @@ "title": "Alert Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12039,6 +12361,9 @@ "description": "Minimum output token limit.", "title": "Min Output Tokens" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12120,6 +12445,9 @@ "description": "Optional entity type filter (e.g. 'agent', 'knowledge_base')", "title": "Entity Type" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12226,6 +12554,9 @@ "description": "Filter by solution name (case-insensitive partial match)", "title": "Search" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12258,6 +12589,11 @@ "post": { "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires `X-API-Key`.", "operationId": "create_solution_api_solutions_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -12310,6 +12646,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12345,6 +12684,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12387,6 +12729,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12441,6 +12786,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12493,6 +12841,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12547,6 +12898,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12564,7 +12918,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -12601,6 +12955,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12618,7 +12975,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -12655,6 +13012,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12672,7 +13032,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantGenerateResponse" + "$ref": "#/components/schemas/AiAssistantGenerateResponse" } } }, @@ -12719,6 +13079,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12736,7 +13099,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/routers__api__solutions__AiAssistantAcceptResponse" + "$ref": "#/components/schemas/AiAssistantAcceptResponse" } } }, @@ -12783,6 +13146,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12820,6 +13186,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -12866,6 +13235,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12930,6 +13302,9 @@ "title": "Conversation Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -12977,6 +13352,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13029,6 +13407,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13083,6 +13464,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13135,6 +13519,9 @@ "title": "Solution Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13179,6 +13566,11 @@ "post": { "description": "Create a new content source.\n\nSource types: `rss`, `website`, `file_uploads`, `custom_index`.\n\nFor RSS and website sources, provide the URL. For file upload and custom index sources, the URL is created automatically.", "operationId": "create_source_api_sources_post", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "requestBody": { "content": { "application/json": { @@ -13294,6 +13686,9 @@ "description": "List sources for the given account. Defaults to the api key's account.", "title": "Account Id" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13338,6 +13733,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13373,6 +13771,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13415,6 +13816,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13467,6 +13871,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13521,6 +13928,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13571,6 +13981,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13628,6 +14041,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13695,6 +14111,9 @@ "title": "Limit", "type": "integer" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13737,6 +14156,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13791,6 +14213,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -13855,6 +14280,9 @@ "title": "Export Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13900,6 +14328,9 @@ "title": "Export Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -13954,6 +14385,9 @@ "title": "Export Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -14008,6 +14442,9 @@ "title": "Export Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "responses": { @@ -14049,6 +14486,9 @@ "title": "Source Connection Id", "type": "string" } + }, + { + "$ref": "#/components/parameters/X-Account-Id" } ], "requestBody": { @@ -14089,5 +14529,13 @@ ] } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] } diff --git a/seclai/__init__.py b/seclai/__init__.py index 8b84a30..8412091 100644 --- a/seclai/__init__.py +++ b/seclai/__init__.py @@ -34,6 +34,20 @@ SeclaiStreamingError, ) +from .auth import ( + SsoProfile, + SsoCacheEntry, + is_token_valid, + load_sso_profile, + read_sso_cache, + write_sso_cache, + delete_sso_cache, + cache_file_name, + resolve_config_dir, + refresh_token_sync, + refresh_token_async, +) + __all__ = [ "AgentRunStreamRequest", "AsyncSeclai", @@ -44,4 +58,15 @@ "SeclaiConfigurationError", "SeclaiError", "SeclaiStreamingError", + "SsoProfile", + "SsoCacheEntry", + "is_token_valid", + "load_sso_profile", + "read_sso_cache", + "write_sso_cache", + "delete_sso_cache", + "cache_file_name", + "resolve_config_dir", + "refresh_token_sync", + "refresh_token_async", ] diff --git a/seclai/auth.py b/seclai/auth.py new file mode 100644 index 0000000..6dc9bd6 --- /dev/null +++ b/seclai/auth.py @@ -0,0 +1,571 @@ +"""SSO credential resolution: config file, token caching, and automatic refresh. + +This module implements the credential provider chain used by :class:`Seclai` +and :class:`AsyncSeclai`. + +Internal — not part of the public API surface. +""" + +from __future__ import annotations + +import asyncio +import configparser +import hashlib +import json +import os +import tempfile +import threading +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Awaitable, Callable + +import httpx + +_DEFAULT_CONFIG_DIR = ".seclai" +_SSO_CACHE_DIR = "sso/cache" +_CONFIG_FILE = "config" +_EXPIRY_BUFFER_SECONDS = 30 + +_SSO_EXPIRED_MSG = "SSO token expired. Run `seclai auth login` to re-authenticate." + + +# ── Types ───────────────────────────────────────────────────────────────────── + + +@dataclass(frozen=True, slots=True) +class SsoProfile: + """Resolved SSO profile settings from the config file. + + Attributes: + sso_account_id: AWS Cognito account ID. + sso_region: AWS region for the Cognito user pool. + sso_client_id: Cognito app client ID. + sso_domain: Cognito domain (e.g. ``"auth.example.com"``). + """ + + sso_account_id: str + sso_region: str + sso_client_id: str + sso_domain: str + + +@dataclass(frozen=True, slots=True) +class SsoCacheEntry: + """Contents of a single SSO cache file. + + Attributes: + access_token: JWT access token. + refresh_token: Refresh token for obtaining new access tokens. + id_token: OIDC ID token (optional). + expires_at: ISO-8601 expiry timestamp for the access token. + client_id: Cognito app client ID. + region: AWS region. + cognito_domain: Cognito domain. + """ + + access_token: str + refresh_token: str | None + id_token: str | None + expires_at: str # ISO-8601 + client_id: str + region: str + cognito_domain: str + + def to_dict(self) -> dict[str, Any]: + """Serialize to a JSON-compatible dict with camelCase keys.""" + d: dict[str, Any] = { + "accessToken": self.access_token, + "expiresAt": self.expires_at, + "clientId": self.client_id, + "region": self.region, + "cognitoDomain": self.cognito_domain, + } + if self.refresh_token is not None: + d["refreshToken"] = self.refresh_token + if self.id_token is not None: + d["idToken"] = self.id_token + return d + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SsoCacheEntry: + """Deserialize from a JSON dict with camelCase keys.""" + return cls( + access_token=data["accessToken"], + refresh_token=data.get("refreshToken"), + id_token=data.get("idToken"), + expires_at=data["expiresAt"], + client_id=data["clientId"], + region=data["region"], + cognito_domain=data["cognitoDomain"], + ) + + +@dataclass(slots=True) +class AuthState: + """Resolved authentication state used throughout the client lifecycle. + + Attributes: + mode: Auth mode — one of ``"api_key"``, ``"bearer_static"``, + ``"bearer_provider"``, or ``"sso"``. + api_key: API key value (for ``api_key`` mode). + api_key_header: Header name for API key auth. + access_token: Static bearer token (for ``bearer_static`` mode). + access_token_provider: Callable returning a token string or awaitable + (for ``bearer_provider`` mode). + account_id: Account ID sent as ``X-Account-Id`` header. + sso_profile: Resolved SSO profile (for ``sso`` mode). + config_dir: Config directory path (for SSO cache lookup). + auto_refresh: Whether to auto-refresh expired SSO tokens. + """ + + mode: str # "api_key" | "bearer_static" | "bearer_provider" | "sso" + api_key: str | None = None + api_key_header: str = "x-api-key" + access_token: str | None = None + access_token_provider: Callable[[], str | Awaitable[str]] | None = None + account_id: str | None = None + sso_profile: SsoProfile | None = None + config_dir: str | None = None + auto_refresh: bool = True + _sync_refresh_lock: threading.Lock = field(default_factory=threading.Lock, repr=False) + _async_refresh_lock: asyncio.Lock = field(default_factory=asyncio.Lock, repr=False) + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + + +def cache_file_name(domain: str, client_id: str) -> str: + """Compute SHA-1 hex digest of ``domain|client_id`` for cache filename.""" + return hashlib.sha1(f"{domain}|{client_id}".encode()).hexdigest() + + +def resolve_config_dir(override: str | None = None) -> Path: + """Resolve the config directory path.""" + if override: + return Path(override) + env_dir = os.getenv("SECLAI_CONFIG_DIR") + if env_dir: + return Path(env_dir) + home = Path.home() + return home / _DEFAULT_CONFIG_DIR + + +def parse_ini_config(config_path: Path) -> configparser.ConfigParser: + """Read an INI config file with AWS-style ``[profile X]`` sections.""" + cp = configparser.ConfigParser() + if config_path.exists(): + cp.read(str(config_path)) + return cp + + +def load_sso_profile(config_dir: Path, profile_name: str) -> SsoProfile | None: + """Load and resolve an SSO profile from the config file. + + Non-default profiles inherit unset values from ``[default]``. + """ + config_path = config_dir / _CONFIG_FILE + if not config_path.exists(): + return None + + cp = parse_ini_config(config_path) + + default_section: dict[str, str] = {} + if cp.has_section("default"): + default_section = dict(cp.items("default")) + + if profile_name == "default": + section = default_section + else: + section_name = f"profile {profile_name}" + if not cp.has_section(section_name): + return None + section = {**default_section, **dict(cp.items(section_name))} + + sso_account_id = section.get("sso_account_id") + sso_region = section.get("sso_region") + sso_client_id = section.get("sso_client_id") + sso_domain = section.get("sso_domain") + + if not all([sso_account_id, sso_region, sso_client_id, sso_domain]): + return None + + return SsoProfile( + sso_account_id=sso_account_id, # type: ignore[arg-type] + sso_region=sso_region, # type: ignore[arg-type] + sso_client_id=sso_client_id, # type: ignore[arg-type] + sso_domain=sso_domain, # type: ignore[arg-type] + ) + + +# ── Cache I/O ───────────────────────────────────────────────────────────────── + + +def _sso_cache_path(config_dir: Path, profile: SsoProfile) -> Path: + """Resolve the full path to a profile's SSO cache file.""" + hash_name = cache_file_name(profile.sso_domain, profile.sso_client_id) + return config_dir / _SSO_CACHE_DIR / f"{hash_name}.json" + + +def read_sso_cache(config_dir: Path, profile: SsoProfile) -> SsoCacheEntry | None: + """Read a cached SSO token from disk.""" + cache_path = _sso_cache_path(config_dir, profile) + if not cache_path.exists(): + return None + try: + data = json.loads(cache_path.read_text()) + return SsoCacheEntry.from_dict(data) + except (json.JSONDecodeError, KeyError, OSError): + return None + + +def write_sso_cache( + config_dir: Path, profile: SsoProfile, entry: SsoCacheEntry +) -> None: + """Write a cached SSO token to disk atomically.""" + cache_dir = config_dir / _SSO_CACHE_DIR + cache_dir.mkdir(parents=True, exist_ok=True) + os.chmod(str(cache_dir), 0o700) + + hash_name = cache_file_name(profile.sso_domain, profile.sso_client_id) + cache_path = cache_dir / f"{hash_name}.json" + + fd, tmp_path = tempfile.mkstemp(dir=str(cache_dir), suffix=".tmp") + try: + with os.fdopen(fd, "w") as f: + json.dump(entry.to_dict(), f, indent=2) + os.chmod(tmp_path, 0o600) + os.replace(tmp_path, str(cache_path)) + except Exception: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + + +def delete_sso_cache(config_dir: Path, profile: SsoProfile) -> None: + """Delete a cached SSO token file.""" + cache_path = _sso_cache_path(config_dir, profile) + if cache_path.exists(): + cache_path.unlink() + + +# ── Token validation ────────────────────────────────────────────────────────── + + +def is_token_valid(entry: SsoCacheEntry) -> bool: + """Check if a cached token is still valid (with 30s buffer).""" + try: + expires_at = datetime.fromisoformat(entry.expires_at.replace("Z", "+00:00")) + except ValueError: + return False + now = datetime.now(timezone.utc) + return now + timedelta(seconds=_EXPIRY_BUFFER_SECONDS) < expires_at + + +# ── Token refresh (sync) ───────────────────────────────────────────────────── + + +def refresh_token_sync( + profile: SsoProfile, + refresh_token_value: str, + http_client: httpx.Client | None = None, +) -> SsoCacheEntry: + """Refresh an access token using a Cognito ``refresh_token`` grant (sync). + + Args: + profile: SSO profile with Cognito domain and client ID. + refresh_token_value: The refresh token to exchange. + http_client: Optional pre-configured HTTP client. + + Returns: + A fresh :class:`SsoCacheEntry` with the new tokens. + + Raises: + httpx.HTTPStatusError: If the Cognito token endpoint returns a non-2xx status. + """ + token_url = f"https://{profile.sso_domain}/oauth2/token" + body = { + "grant_type": "refresh_token", + "client_id": profile.sso_client_id, + "refresh_token": refresh_token_value, + } + + client = http_client or httpx.Client() + try: + response = client.post( + token_url, + data=body, + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + response.raise_for_status() + finally: + if http_client is None: + client.close() + + data = response.json() + from datetime import timedelta + + expires_at = ( + datetime.now(timezone.utc) + timedelta(seconds=data["expires_in"]) + ).isoformat() + + return SsoCacheEntry( + access_token=data["access_token"], + refresh_token=data.get("refresh_token", refresh_token_value), + id_token=data.get("id_token"), + expires_at=expires_at, + client_id=profile.sso_client_id, + region=profile.sso_region, + cognito_domain=profile.sso_domain, + ) + + +# ── Token refresh (async) ──────────────────────────────────────────────────── + + +async def refresh_token_async( + profile: SsoProfile, + refresh_token_value: str, + http_client: httpx.AsyncClient | None = None, +) -> SsoCacheEntry: + """Refresh an access token using a Cognito ``refresh_token`` grant (async). + + Args: + profile: SSO profile with Cognito domain and client ID. + refresh_token_value: The refresh token to exchange. + http_client: Optional pre-configured async HTTP client. + + Returns: + A fresh :class:`SsoCacheEntry` with the new tokens. + + Raises: + httpx.HTTPStatusError: If the Cognito token endpoint returns a non-2xx status. + """ + token_url, body = _build_refresh_request(profile, refresh_token_value) + + client = http_client or httpx.AsyncClient() + try: + response = await client.post( + token_url, + data=body, + headers={"content-type": "application/x-www-form-urlencoded"}, + ) + response.raise_for_status() + finally: + if http_client is None: + await client.aclose() + + return _parse_refresh_response(response.json(), profile, refresh_token_value) + + +# ── Credential chain ───────────────────────────────────────────────────────── + + +def resolve_credential_chain( + *, + api_key: str | None = None, + access_token: str | None = None, + access_token_provider: Callable[[], str | Awaitable[str]] | None = None, + profile: str | None = None, + config_dir: str | None = None, + auto_refresh: bool = True, + account_id: str | None = None, + api_key_header: str = "x-api-key", +) -> AuthState: + """Resolve the credential chain (synchronous). + + Resolution order: + 1. Explicit ``api_key`` + 2. Explicit ``access_token`` (static string) + 3. Explicit ``access_token_provider`` (callback) + 4. ``SECLAI_API_KEY`` environment variable + 5. SSO profile from config file + cached tokens + 6. Error + + Raises: + RuntimeError: If no credentials are found. + """ + # 1. Explicit API key + if api_key: + return AuthState( + mode="api_key", + api_key=api_key.strip(), + api_key_header=api_key_header, + account_id=account_id, + auto_refresh=False, + ) + + # 2. Static access token + if access_token: + return AuthState( + mode="bearer_static", + access_token=access_token, + api_key_header=api_key_header, + account_id=account_id, + auto_refresh=False, + ) + + # 3. Access token provider + if access_token_provider: + return AuthState( + mode="bearer_provider", + access_token_provider=access_token_provider, + api_key_header=api_key_header, + account_id=account_id, + auto_refresh=False, + ) + + # 4. SECLAI_API_KEY env var + env_key = os.getenv("SECLAI_API_KEY", "").strip() + if env_key: + return AuthState( + mode="api_key", + api_key=env_key, + api_key_header=api_key_header, + account_id=account_id, + auto_refresh=False, + ) + + # 5. SSO profile + try: + resolved_dir = resolve_config_dir(config_dir) + profile_name = profile or os.getenv("SECLAI_PROFILE") or "default" + sso = load_sso_profile(resolved_dir, profile_name) + if sso: + return AuthState( + mode="sso", + api_key_header=api_key_header, + account_id=account_id or sso.sso_account_id, + sso_profile=sso, + config_dir=str(resolved_dir), + auto_refresh=auto_refresh, + ) + except (OSError, configparser.Error): + pass + + # 6. Nothing found + raise RuntimeError( + "Missing credentials. Pass api_key=..., access_token=..., " + "set SECLAI_API_KEY, or run `seclai auth login`." + ) + + +# ── Per-request auth header resolution ──────────────────────────────────────── + + +def resolve_auth_headers_sync(state: AuthState) -> dict[str, str]: + """Resolve auth headers for a sync request. + + Args: + state: Resolved authentication state. + + Returns: + Header dict with the appropriate auth header(s). + + Raises: + TypeError: If an async ``access_token_provider`` is used with a sync client. + """ + headers: dict[str, str] = {} + + if state.mode == "api_key": + headers[state.api_key_header] = state.api_key # type: ignore[assignment] + elif state.mode == "bearer_static": + headers["authorization"] = f"Bearer {state.access_token}" + elif state.mode == "bearer_provider": + token = state.access_token_provider() # type: ignore[misc] + if hasattr(token, "__await__"): + raise TypeError( + "Async access_token_provider used with sync Seclai client. " + "Use a sync provider or switch to AsyncSeclai." + ) + headers["authorization"] = f"Bearer {token}" + elif state.mode == "sso": + token = _resolve_sso_token_sync(state) + headers["authorization"] = f"Bearer {token}" + + if state.account_id: + headers["x-account-id"] = state.account_id + + return headers + + +async def resolve_auth_headers_async(state: AuthState) -> dict[str, str]: + """Resolve auth headers for an async request. + + Args: + state: Resolved authentication state. + + Returns: + Header dict with the appropriate auth header(s). + """ + headers: dict[str, str] = {} + + if state.mode == "api_key": + headers[state.api_key_header] = state.api_key # type: ignore[assignment] + elif state.mode == "bearer_static": + headers["authorization"] = f"Bearer {state.access_token}" + elif state.mode == "bearer_provider": + token = state.access_token_provider() # type: ignore[misc] + if hasattr(token, "__await__"): + token = await token # type: ignore[misc] + headers["authorization"] = f"Bearer {token}" + elif state.mode == "sso": + token = await _resolve_sso_token_async(state) + headers["authorization"] = f"Bearer {token}" + + if state.account_id: + headers["x-account-id"] = state.account_id + + return headers + + +def _resolve_sso_token_sync(state: AuthState) -> str: + """Resolve a valid SSO token, refreshing from cache if needed (sync).""" + profile = state.sso_profile + assert profile is not None + config_dir = Path(state.config_dir) if state.config_dir else resolve_config_dir() + + cached = read_sso_cache(config_dir, profile) + + if cached and is_token_valid(cached): + return cached.access_token + + if cached and cached.refresh_token and state.auto_refresh: + with state._sync_refresh_lock: + # Re-check after acquiring lock — another thread may have refreshed + cached = read_sso_cache(config_dir, profile) + if cached and is_token_valid(cached): + return cached.access_token + if cached and cached.refresh_token: + refreshed = refresh_token_sync(profile, cached.refresh_token) + write_sso_cache(config_dir, profile, refreshed) + return refreshed.access_token + + raise RuntimeError(_SSO_EXPIRED_MSG) + + +async def _resolve_sso_token_async(state: AuthState) -> str: + """Resolve a valid SSO token, refreshing from cache if needed (async).""" + profile = state.sso_profile + assert profile is not None + config_dir = Path(state.config_dir) if state.config_dir else resolve_config_dir() + + cached = read_sso_cache(config_dir, profile) + + if cached and is_token_valid(cached): + return cached.access_token + + if cached and cached.refresh_token and state.auto_refresh: + async with state._async_refresh_lock: + # Re-check after acquiring lock — another task may have refreshed + cached = read_sso_cache(config_dir, profile) + if cached and is_token_valid(cached): + return cached.access_token + if cached and cached.refresh_token: + refreshed = await refresh_token_async(profile, cached.refresh_token) + write_sso_cache(config_dir, profile, refreshed) + return refreshed.access_token + + raise RuntimeError(_SSO_EXPIRED_MSG) diff --git a/seclai/seclai.py b/seclai/seclai.py index 188d5f9..a975fae 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -46,7 +46,7 @@ from http import HTTPStatus from io import BytesIO from pathlib import Path -from typing import Any, BinaryIO, Self, TypedDict, cast, overload +from typing import Any, Awaitable, BinaryIO, Callable, Self, TypedDict, cast, overload import httpx @@ -62,6 +62,12 @@ from seclai._generated.models.http_validation_error import HTTPValidationError from seclai._generated.models.source_list_response import SourceListResponse from seclai._generated.types import Response as OpenAPIResponse +from seclai.auth import ( + AuthState, + resolve_auth_headers_async, + resolve_auth_headers_sync, + resolve_credential_chain, +) logger = logging.getLogger(__name__) @@ -143,6 +149,7 @@ def __init__( response_text: str | None, validation_error: HTTPValidationError, ) -> None: + """Initialize with validation error details.""" super().__init__( message=message, status_code=status_code, @@ -162,12 +169,14 @@ class SeclaiStreamingError(SeclaiError): """ def __init__(self, message: str, *, run_id: str | None = None) -> None: + """Initialize with stream error message and optional run ID.""" super().__init__(message) self.message = message self.run_id = run_id def _guess_mime_type(file_name: str | None) -> str | None: + """Guess the MIME type from a filename, or return ``None``.""" if not file_name: return None guessed, _ = mimetypes.guess_type(file_name) @@ -195,13 +204,13 @@ class ClientOptions: All values are resolved at construction time and immutable thereafter. Attributes: - api_key: Resolved API key. + auth_state: Resolved authentication state. timeout: Request timeout in seconds. api_key_header: HTTP header name used to transmit the API key. default_headers: Extra headers included on every request. """ - api_key: str + auth_state: AuthState timeout: float api_key_header: str default_headers: Mapping[str, str] @@ -209,15 +218,21 @@ class ClientOptions: def _build_default_headers( *, - api_key: str, - api_key_header: str, + auth_state: AuthState, default_headers: Mapping[str, str] | None, ) -> dict[str, str]: - """Build default request headers including API key auth and user-agent.""" + """Build default request headers including auth and user-agent.""" headers: dict[str, str] = { - api_key_header: api_key, "user-agent": "seclai-python", } + # Add static auth headers (for api_key and bearer_static modes). + # Dynamic modes (bearer_provider, sso) are resolved per-request. + if auth_state.mode == "api_key": + headers[auth_state.api_key_header] = auth_state.api_key # type: ignore[assignment] + elif auth_state.mode == "bearer_static": + headers["authorization"] = f"Bearer {auth_state.access_token}" + if auth_state.account_id: + headers["x-account-id"] = auth_state.account_id if default_headers: headers.update(default_headers) return headers @@ -228,12 +243,34 @@ def _merge_request_headers( options: ClientOptions, request_headers: Mapping[str, str] | None, ) -> dict[str, str]: - """Merge client default headers with per-request overrides.""" + """Merge client default headers with per-request overrides including dynamic auth.""" + merged = _build_default_headers( + auth_state=options.auth_state, + default_headers=options.default_headers, + ) + # For dynamic auth modes, resolve per-request headers + if options.auth_state.mode in ("bearer_provider", "sso"): + auth_headers = resolve_auth_headers_sync(options.auth_state) + merged.update(auth_headers) + if request_headers: + merged.update(request_headers) + return merged + + +async def _merge_request_headers_async( + *, + options: ClientOptions, + request_headers: Mapping[str, str] | None, +) -> dict[str, str]: + """Merge client default headers with per-request overrides including dynamic auth (async).""" merged = _build_default_headers( - api_key=options.api_key, - api_key_header=options.api_key_header, + auth_state=options.auth_state, default_headers=options.default_headers, ) + # For dynamic auth modes, resolve per-request headers asynchronously + if options.auth_state.mode in ("bearer_provider", "sso"): + auth_headers = await resolve_auth_headers_async(options.auth_state) + merged.update(auth_headers) if request_headers: merged.update(request_headers) return merged @@ -259,7 +296,7 @@ def _raise_for_status(response: httpx.Response) -> None: class _SeclaiBase: """Shared implementation for Seclai sync/async clients. - This class centralizes API key resolution, option storage, and configuration of + This class centralizes credential resolution, option storage, and configuration of the generated OpenAPI client. """ @@ -267,24 +304,62 @@ def __init__( self, *, api_key: str | None, + access_token: str | Callable[[], str | Awaitable[str]] | None, timeout: float, api_key_header: str, default_headers: Mapping[str, str] | None, + profile: str | None, + config_dir: str | None, + auto_refresh: bool, + account_id: str | None, ) -> None: """Initialize shared client state. Args: - api_key: API key used for authentication. If omitted, `SECLAI_API_KEY` is used. + api_key: API key used for authentication. If omitted, ``SECLAI_API_KEY`` is used. + access_token: Static bearer token string or a provider callable. + Mutually exclusive with ``api_key``. timeout: Request timeout (seconds). api_key_header: Header name to use for the API key. default_headers: Extra headers to include on every request. + profile: SSO profile name from ``~/.seclai/config``. + config_dir: Override the config directory path. + auto_refresh: Whether to auto-refresh expired SSO tokens. Defaults to ``True``. + account_id: Target organization account ID (``X-Account-Id`` header). Raises: - SeclaiConfigurationError: If no API key is provided and `SECLAI_API_KEY` is not set. + SeclaiConfigurationError: If no credentials are found or if both ``api_key`` + and ``access_token`` are provided. """ - resolved_key = _resolve_api_key(api_key) + if api_key and access_token: + raise SeclaiConfigurationError( + "Provide either api_key or access_token, not both." + ) + + # Determine access_token vs provider + access_token_str: str | None = None + access_token_provider: Callable[[], str | Awaitable[str]] | None = None + if callable(access_token): + access_token_provider = access_token + elif isinstance(access_token, str): + access_token_str = access_token + + try: + auth_state = resolve_credential_chain( + api_key=api_key, + access_token=access_token_str, + access_token_provider=access_token_provider, + profile=profile, + config_dir=config_dir, + auto_refresh=auto_refresh, + account_id=account_id, + api_key_header=api_key_header, + ) + except RuntimeError as e: + raise SeclaiConfigurationError(str(e)) from e + self._options = ClientOptions( - api_key=resolved_key, + auth_state=auth_state, timeout=timeout, api_key_header=api_key_header, default_headers=default_headers or {}, @@ -294,19 +369,18 @@ def __init__( self._owns_generated_client = False @property - def api_key(self) -> str: - """Return the resolved API key used for authentication.""" - return self._options.api_key + def api_key(self) -> str | None: + """Return the resolved API key used for authentication, or ``None`` for bearer auth.""" + return self._options.auth_state.api_key def _default_headers(self) -> dict[str, str]: """Build default request headers for the underlying HTTP clients. Returns: - A header dictionary containing API key auth, user-agent, and any configured defaults. + A header dictionary containing auth, user-agent, and any configured defaults. """ return _build_default_headers( - api_key=self._options.api_key, - api_key_header=self._options.api_key_header, + auth_state=self._options.auth_state, default_headers=self._options.default_headers, ) @@ -401,28 +475,51 @@ def __init__( self, *, api_key: str | None = None, + access_token: str | Callable[[], str] | None = None, timeout: float = 30.0, api_key_header: str = "x-api-key", default_headers: Mapping[str, str] | None = None, http_client: httpx.Client | None = None, + profile: str | None = None, + config_dir: str | None = None, + auto_refresh: bool = True, + account_id: str | None = None, ) -> None: """Create a synchronous Seclai client. + Credentials are resolved via a chain (first match wins): + + 1. Explicit ``api_key`` + 2. Explicit ``access_token`` (static string or provider callable) + 3. ``SECLAI_API_KEY`` environment variable + 4. SSO profile from ``~/.seclai/config`` + cached tokens + Args: - api_key: API key used for authentication. If omitted, `SECLAI_API_KEY` is used. + api_key: API key used for authentication. If omitted, ``SECLAI_API_KEY`` is used. + access_token: Static bearer token string or a callable returning one. + Mutually exclusive with ``api_key``. timeout: Request timeout (seconds). api_key_header: Header name to use for the API key. default_headers: Extra headers to include on every request. - http_client: Optional pre-configured `httpx.Client` to use. + http_client: Optional pre-configured ``httpx.Client`` to use. + profile: SSO profile name from ``~/.seclai/config``. + config_dir: Override the config directory path. + auto_refresh: Auto-refresh expired SSO tokens. Defaults to ``True``. + account_id: Target organization account ID (``X-Account-Id`` header). Raises: - SeclaiConfigurationError: If no API key is provided and `SECLAI_API_KEY` is not set. + SeclaiConfigurationError: If no credentials are found. """ super().__init__( api_key=api_key, + access_token=access_token, timeout=timeout, api_key_header=api_key_header, default_headers=default_headers, + profile=profile, + config_dir=config_dir, + auto_refresh=auto_refresh, + account_id=account_id, ) self._client = http_client or httpx.Client( base_url=SECLAI_API_URL, @@ -3562,28 +3659,51 @@ def __init__( self, *, api_key: str | None = None, + access_token: str | Callable[[], str | Awaitable[str]] | None = None, timeout: float = 30.0, api_key_header: str = "x-api-key", default_headers: Mapping[str, str] | None = None, http_client: httpx.AsyncClient | None = None, + profile: str | None = None, + config_dir: str | None = None, + auto_refresh: bool = True, + account_id: str | None = None, ) -> None: """Create an asynchronous Seclai client. + Credentials are resolved via a chain (first match wins): + + 1. Explicit ``api_key`` + 2. Explicit ``access_token`` (static string or provider callable) + 3. ``SECLAI_API_KEY`` environment variable + 4. SSO profile from ``~/.seclai/config`` + cached tokens + Args: - api_key: API key used for authentication. If omitted, `SECLAI_API_KEY` is used. + api_key: API key used for authentication. If omitted, ``SECLAI_API_KEY`` is used. + access_token: Static bearer token string or a callable returning one + (sync or async). Mutually exclusive with ``api_key``. timeout: Request timeout (seconds). api_key_header: Header name to use for the API key. default_headers: Extra headers to include on every request. - http_client: Optional pre-configured `httpx.AsyncClient` to use. + http_client: Optional pre-configured ``httpx.AsyncClient`` to use. + profile: SSO profile name from ``~/.seclai/config``. + config_dir: Override the config directory path. + auto_refresh: Auto-refresh expired SSO tokens. Defaults to ``True``. + account_id: Target organization account ID (``X-Account-Id`` header). Raises: - SeclaiConfigurationError: If no API key is provided and `SECLAI_API_KEY` is not set. + SeclaiConfigurationError: If no credentials are found. """ super().__init__( api_key=api_key, + access_token=access_token, timeout=timeout, api_key_header=api_key_header, default_headers=default_headers, + profile=profile, + config_dir=config_dir, + auto_refresh=auto_refresh, + account_id=account_id, ) self._client = http_client or httpx.AsyncClient( base_url=SECLAI_API_URL, @@ -3649,7 +3769,7 @@ async def request( path, params=params, json=json, - headers=_merge_request_headers( + headers=await _merge_request_headers_async( options=self._options, request_headers=headers ), ) @@ -3729,7 +3849,7 @@ async def run_streaming_agent_and_wait( path = f"/agents/{agent_id}/runs/stream" - merged_headers = _merge_request_headers( + merged_headers = await _merge_request_headers_async( options=self._options, request_headers=headers ) merged_headers.setdefault("accept", "text/event-stream") @@ -4678,7 +4798,7 @@ async def upload_agent_input( response = await self._client.post( f"/agents/{agent_id}/upload-input", files={"file": payload}, - headers=_merge_request_headers( + headers=await _merge_request_headers_async( options=self._options, request_headers=None ), ) @@ -6631,7 +6751,7 @@ async def run_streaming_agent( import json as _json path = f"/agents/{agent_id}/runs/stream" - merged_headers = _merge_request_headers( + merged_headers = await _merge_request_headers_async( options=self._options, request_headers=headers ) merged_headers.setdefault("accept", "text/event-stream") diff --git a/tests/test_auth_and_headers.py b/tests/test_auth_and_headers.py index 3297856..799f957 100644 --- a/tests/test_auth_and_headers.py +++ b/tests/test_auth_and_headers.py @@ -18,10 +18,17 @@ def test_api_key_param_takes_precedence(monkeypatch: pytest.MonkeyPatch) -> None def test_missing_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SECLAI_API_KEY", raising=False) + monkeypatch.setenv("SECLAI_CONFIG_DIR", "/nonexistent-seclai-dir") with pytest.raises(SeclaiConfigurationError): _ = Seclai() +def test_both_api_key_and_access_token_raises(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + with pytest.raises(SeclaiConfigurationError, match="not both"): + _ = Seclai(api_key="k", access_token="t") + + def test_header_injected_sync(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("SECLAI_API_KEY", "env-key") @@ -50,3 +57,73 @@ async def handler(request: httpx.Request) -> httpx.Response: client = AsyncSeclai(http_client=http_client) assert await client.request("GET", "/ping") == {"ok": True} await http_client.aclose() + + +# ── Bearer Token Auth ──────────────────────────────────────────────────────── + + +def test_bearer_token_sync(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("authorization") == "Bearer my-jwt" + assert "x-api-key" not in request.headers + return httpx.Response(200, json={"ok": True}) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(access_token="my-jwt", http_client=http_client) + assert client.request("GET", "/ping") == {"ok": True} + + +def test_bearer_provider_sync(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + call_count = 0 + + def provider() -> str: + nonlocal call_count + call_count += 1 + return f"token-{call_count}" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("authorization", "").startswith("Bearer token-") + return httpx.Response(200, json={"ok": True}) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(access_token=provider, http_client=http_client) + client.request("GET", "/ping1") + client.request("GET", "/ping2") + assert call_count == 2 + + +def test_account_id_header_sync(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("x-account-id") == "acct-123" + assert request.headers.get("authorization") == "Bearer tok" + return httpx.Response(200, json={"ok": True}) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(access_token="tok", account_id="acct-123", http_client=http_client) + assert client.request("GET", "/ping") == {"ok": True} + + +@pytest.mark.asyncio +async def test_bearer_token_async(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + async def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("authorization") == "Bearer async-jwt" + return httpx.Response(200, json={"ok": True}) + + transport = httpx.MockTransport(handler) + http_client = httpx.AsyncClient( + transport=transport, base_url="https://example.invalid" + ) + client = AsyncSeclai(access_token="async-jwt", http_client=http_client) + assert await client.request("GET", "/ping") == {"ok": True} + await http_client.aclose() From 3bc12138289c849cec0300a82a75e52da7116c4b Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Wed, 25 Mar 2026 23:22:16 -0700 Subject: [PATCH 2/7] Addressed review comments --- README.md | 14 +++- openapi/seclai.openapi.json | 134 ++++++++++++++++----------------- seclai/__init__.py | 27 +------ seclai/auth.py | 66 ++++++++++------ seclai/seclai.py | 90 ++++++++++++++++------ tests/test_auth_and_headers.py | 95 +++++++++++++++++++++++ 6 files changed, 286 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 6b72655..6fe3000 100644 --- a/README.md +++ b/README.md @@ -97,19 +97,29 @@ Credentials are resolved via a chain (first match wins): ```python # API key client = Seclai(api_key="sk-...") +``` +```python # Static bearer token client = Seclai(access_token="eyJhbGciOi...") +``` -# Dynamic bearer token provider (sync or async callable) +```python +# Dynamic bearer token provider (sync callable, called per request) client = Seclai(access_token=lambda: get_token_from_vault()) +``` -# Async provider with AsyncSeclai +```python +# Async provider — use AsyncSeclai for async callables client = AsyncSeclai(access_token=get_token_async) +``` +```python # SSO profile (uses cached tokens, auto-refreshes) client = Seclai(profile="my-profile") +``` +```python # Environment variable (no options needed) # export SECLAI_API_KEY="sk-..." client = Seclai() diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 1ab345e..fbb6e91 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -7078,7 +7078,7 @@ "paths": { "/agents": { "get": { - "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "list_agents_api_agents_get", "parameters": [ { @@ -7140,7 +7140,7 @@ ] }, "post": { - "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires `X-API-Key`. Agent is created in the API key's account.", + "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Agent is created in the API key's account.", "operationId": "create_agent_api_agents_post", "parameters": [ { @@ -7721,7 +7721,7 @@ }, "/agents/runs/search": { "post": { - "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires `X-API-Key`. Searches only within your account's traces.", + "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Searches only within your account's traces.", "operationId": "search_agent_runs_api_agents_runs_search_post", "parameters": [ { @@ -7768,7 +7768,7 @@ }, "/agents/runs/{run_id}": { "delete": { - "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only cancel runs belonging to your account.", + "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only cancel runs belonging to your account.", "operationId": "delete_agent_run_api_agents_runs__run_id__delete", "parameters": [ { @@ -7812,7 +7812,7 @@ ] }, "get": { - "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access runs belonging to your account.", + "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access runs belonging to your account.", "operationId": "get_agent_run_api_agents_runs__run_id__get", "parameters": [ { @@ -7870,7 +7870,7 @@ }, "/agents/{agent_id}": { "delete": { - "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only delete agents belonging to your account.", + "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete agents belonging to your account.", "operationId": "delete_agent_api_agents__agent_id__delete", "parameters": [ { @@ -7907,7 +7907,7 @@ ] }, "get": { - "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access agents belonging to your account.", + "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", "operationId": "get_agent_metadata_api_agents__agent_id__get", "parameters": [ { @@ -7951,7 +7951,7 @@ ] }, "put": { - "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update agents belonging to your account.", + "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", "operationId": "update_agent_api_agents__agent_id__put", "parameters": [ { @@ -8007,7 +8007,7 @@ }, "/agents/{agent_id}/ai-assistant/conversations": { "get": { - "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be queried.", + "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be queried.", "operationId": "get_ai_conversation_history_api_agents__agent_id__ai_assistant_conversations_get", "parameters": [ { @@ -8109,7 +8109,7 @@ }, "/agents/{agent_id}/ai-assistant/generate-steps": { "post": { - "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", "operationId": "generate_agent_steps_api_agents__agent_id__ai_assistant_generate_steps_post", "parameters": [ { @@ -8165,7 +8165,7 @@ }, "/agents/{agent_id}/ai-assistant/step-config": { "post": { - "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", "operationId": "generate_step_config_api_agents__agent_id__ai_assistant_step_config_post", "parameters": [ { @@ -8221,7 +8221,7 @@ }, "/agents/{agent_id}/ai-assistant/{conversation_id}": { "patch": { - "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. The conversation must belong to one of your agents.", + "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The conversation must belong to one of your agents.", "operationId": "mark_ai_suggestion_api_agents__agent_id__ai_assistant__conversation_id__patch", "parameters": [ { @@ -8290,7 +8290,7 @@ }, "/agents/{agent_id}/definition": { "get": { - "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access agents belonging to your account.", + "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", "operationId": "get_agent_definition_api_agents__agent_id__definition_get", "parameters": [ { @@ -8334,7 +8334,7 @@ ] }, "put": { - "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update agents belonging to your account.", + "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", "operationId": "update_agent_definition_api_agents__agent_id__definition_put", "parameters": [ { @@ -8826,7 +8826,7 @@ }, "/agents/{agent_id}/input-uploads/{upload_id}": { "get": { - "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "api_get_agent_input_upload_status_api_agents__agent_id__input_uploads__upload_id__get", "parameters": [ { @@ -8881,7 +8881,7 @@ }, "/agents/{agent_id}/runs": { "get": { - "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only list runs for agents in your account.", + "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only list runs for agents in your account.", "operationId": "list_agent_runs_api_agents__agent_id__runs_get", "parameters": [ { @@ -8970,7 +8970,7 @@ ] }, "post": { - "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "run_agent_api_agents__agent_id__runs_post", "parameters": [ { @@ -9026,7 +9026,7 @@ }, "/agents/{agent_id}/runs/stream": { "post": { - "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "run_streaming_agent_api_agents__agent_id__runs_stream_post", "parameters": [ { @@ -9145,7 +9145,7 @@ }, "/agents/{agent_id}/upload-input": { "post": { - "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "api_upload_agent_input_api_agents__agent_id__upload_input_post", "parameters": [ { @@ -9648,7 +9648,7 @@ }, "/alerts": { "get": { - "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires `X-API-Key` with user association. Results are scoped to the API key's account.", + "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association. Results are scoped to the API key's account.", "operationId": "list_alerts_api_alerts_get", "parameters": [ { @@ -9806,7 +9806,7 @@ }, "/alerts/configs": { "get": { - "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "list_alert_configs_api_alerts_configs_get", "parameters": [ { @@ -9897,7 +9897,7 @@ ] }, "post": { - "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "create_alert_config_api_alerts_configs_post", "parameters": [ { @@ -9946,7 +9946,7 @@ }, "/alerts/configs/{config_id}": { "delete": { - "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "delete_alert_config_api_alerts_configs__config_id__delete", "parameters": [ { @@ -9983,7 +9983,7 @@ ] }, "get": { - "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "get_alert_config_api_alerts_configs__config_id__get", "parameters": [ { @@ -10029,7 +10029,7 @@ ] }, "patch": { - "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "update_alert_config_api_alerts_configs__config_id__patch", "parameters": [ { @@ -10087,7 +10087,7 @@ }, "/alerts/organization-preferences/list": { "get": { - "description": "List per-organization alert delivery preferences for the API key's associated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.\n- Only organizations where the user is an owner or administrator are included.", + "description": "List per-organization alert delivery preferences for the API key's associated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only organizations where the user is an owner or administrator are included.", "operationId": "list_organization_preferences_api_alerts_organization_preferences_list_get", "parameters": [ { @@ -10155,7 +10155,7 @@ }, "/alerts/organization-preferences/{organization_id}/{alert_type}": { "patch": { - "description": "Update the API key user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.\n- Only owners and administrators can update preferences for an organization.", + "description": "Update the API key user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only owners and administrators can update preferences for an organization.", "operationId": "update_organization_preference_api_alerts_organization_preferences__organization_id___alert_type__patch", "parameters": [ { @@ -10221,7 +10221,7 @@ }, "/alerts/{alert_id}": { "get": { - "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "get_alert_detail_api_alerts__alert_id__get", "parameters": [ { @@ -10269,7 +10269,7 @@ }, "/alerts/{alert_id}/comments": { "post": { - "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "add_alert_comment_api_alerts__alert_id__comments_post", "parameters": [ { @@ -10327,7 +10327,7 @@ }, "/alerts/{alert_id}/status": { "post": { - "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "change_alert_status_api_alerts__alert_id__status_post", "parameters": [ { @@ -10385,7 +10385,7 @@ }, "/alerts/{alert_id}/subscribe": { "post": { - "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "subscribe_to_alert_api_alerts__alert_id__subscribe_post", "parameters": [ { @@ -10433,7 +10433,7 @@ }, "/alerts/{alert_id}/unsubscribe": { "post": { - "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", + "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", "operationId": "unsubscribe_from_alert_api_alerts__alert_id__unsubscribe_post", "parameters": [ { @@ -10481,7 +10481,7 @@ }, "/contents/{source_connection_content_version}": { "delete": { - "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only delete content belonging to your account.", + "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete content belonging to your account.", "operationId": "delete_content_api_contents__source_connection_content_version__delete", "parameters": [ { @@ -10518,7 +10518,7 @@ ] }, "get": { - "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access content belonging to your account.", + "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access content belonging to your account.", "operationId": "get_content_detail_api_contents__source_connection_content_version__get", "parameters": [ { @@ -10638,7 +10638,7 @@ }, "/contents/{source_connection_content_version}/embeddings": { "get": { - "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access embeddings for content belonging to your account.", + "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access embeddings for content belonging to your account.", "operationId": "list_content_embeddings_api_contents__source_connection_content_version__embeddings_get", "parameters": [ { @@ -10704,7 +10704,7 @@ }, "/contents/{source_connection_content_version}/upload": { "post": { - "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only replace content belonging to your account.", + "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only replace content belonging to your account.", "operationId": "upload_file_to_content_api_contents__source_connection_content_version__upload_post", "parameters": [ { @@ -10970,7 +10970,7 @@ }, "/knowledge_bases": { "get": { - "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "list_knowledge_bases_api_knowledge_bases_get", "parameters": [ { @@ -11143,7 +11143,7 @@ ] }, "get": { - "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access knowledge bases belonging to your account.", + "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access knowledge bases belonging to your account.", "operationId": "get_knowledge_base_api_knowledge_bases__knowledge_base_id__get", "parameters": [ { @@ -11187,7 +11187,7 @@ ] }, "put": { - "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update knowledge bases belonging to your account.", + "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update knowledge bases belonging to your account.", "operationId": "update_knowledge_base_api_knowledge_bases__knowledge_base_id__put", "parameters": [ { @@ -11243,7 +11243,7 @@ }, "/memory_banks": { "get": { - "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", + "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", "operationId": "list_memory_banks_api_memory_banks_get", "parameters": [ { @@ -11685,7 +11685,7 @@ ] }, "get": { - "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access memory banks belonging to your account.", + "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access memory banks belonging to your account.", "operationId": "get_memory_bank_api_memory_banks__memory_bank_id__get", "parameters": [ { @@ -12075,7 +12075,7 @@ }, "/models/alerts": { "get": { - "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires `X-API-Key`. Alerts are scoped to the API key's account.", + "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Alerts are scoped to the API key's account.", "operationId": "list_alerts_api_models_alerts_get", "parameters": [ { @@ -12171,7 +12171,7 @@ }, "/models/alerts/mark-all-read": { "post": { - "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. Scoped to the API key's account.", + "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Scoped to the API key's account.", "operationId": "mark_all_read_api_models_alerts_mark_all_read_post", "parameters": [ { @@ -12191,7 +12191,7 @@ }, "/models/alerts/unread-count": { "get": { - "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires `X-API-Key`. Count is scoped to the API key's account.", + "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Count is scoped to the API key's account.", "operationId": "get_alert_unread_count_api_models_alerts_unread_count_get", "parameters": [ { @@ -12220,7 +12220,7 @@ }, "/models/alerts/{alert_id}/read": { "patch": { - "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires `X-API-Key`. The alert must belong to the API key's account.", + "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The alert must belong to the API key's account.", "operationId": "mark_read_api_models_alerts__alert_id__read_patch", "parameters": [ { @@ -12260,7 +12260,7 @@ }, "/models/{model_id}/recommendations": { "get": { - "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires `X-API-Key`.", + "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token).", "operationId": "get_recommendations_api_models__model_id__recommendations_get", "parameters": [ { @@ -12482,7 +12482,7 @@ }, "/solutions": { "get": { - "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires `X-API-Key`. Results are scoped to the API key's account.", + "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the API key's account.", "operationId": "list_solutions_api_solutions_get", "parameters": [ { @@ -12587,7 +12587,7 @@ ] }, "post": { - "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires `X-API-Key`.", + "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires authentication (API key or bearer token).", "operationId": "create_solution_api_solutions_post", "parameters": [ { @@ -12634,7 +12634,7 @@ }, "/solutions/{solution_id}": { "delete": { - "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted. Requires `X-API-Key`.", + "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted. Requires authentication (API key or bearer token).", "operationId": "delete_solution_api_solutions__solution_id__delete", "parameters": [ { @@ -12672,7 +12672,7 @@ ] }, "get": { - "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information. Requires `X-API-Key`.", + "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information. Requires authentication (API key or bearer token).", "operationId": "get_solution_api_solutions__solution_id__get", "parameters": [ { @@ -12717,7 +12717,7 @@ ] }, "patch": { - "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged. Requires `X-API-Key`.", + "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged. Requires authentication (API key or bearer token).", "operationId": "update_solution_api_solutions__solution_id__patch", "parameters": [ { @@ -12774,7 +12774,7 @@ }, "/solutions/{solution_id}/agents": { "delete": { - "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources. Requires `X-API-Key`.", + "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources. Requires authentication (API key or bearer token).", "operationId": "unlink_agents_api_solutions__solution_id__agents_delete", "parameters": [ { @@ -12829,7 +12829,7 @@ ] }, "post": { - "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", + "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", "operationId": "link_agents_api_solutions__solution_id__agents_post", "parameters": [ { @@ -12886,7 +12886,7 @@ }, "/solutions/{solution_id}/ai-assistant/generate": { "post": { - "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint. Requires `X-API-Key`.\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", + "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", "operationId": "ai_assistant_generate_api_solutions__solution_id__ai_assistant_generate_post", "parameters": [ { @@ -12943,7 +12943,7 @@ }, "/solutions/{solution_id}/ai-assistant/knowledge-base": { "post": { - "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint. Requires `X-API-Key`.", + "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).", "operationId": "ai_assistant_knowledge_base_api_solutions__solution_id__ai_assistant_knowledge_base_post", "parameters": [ { @@ -13000,7 +13000,7 @@ }, "/solutions/{solution_id}/ai-assistant/source": { "post": { - "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan. Requires `X-API-Key`.", + "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan. Requires authentication (API key or bearer token).", "operationId": "ai_assistant_source_api_solutions__solution_id__ai_assistant_source_post", "parameters": [ { @@ -13057,7 +13057,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status. Requires `X-API-Key`.", + "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status. Requires authentication (API key or bearer token).", "operationId": "ai_assistant_accept_api_solutions__solution_id__ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -13124,7 +13124,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed. Requires `X-API-Key`.", + "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed. Requires authentication (API key or bearer token).", "operationId": "ai_assistant_decline_api_solutions__solution_id__ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -13174,7 +13174,7 @@ }, "/solutions/{solution_id}/conversations": { "get": { - "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status. Requires `X-API-Key`.", + "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status. Requires authentication (API key or bearer token).", "operationId": "list_conversations_api_solutions__solution_id__conversations_get", "parameters": [ { @@ -13223,7 +13223,7 @@ ] }, "post": { - "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions. Requires `X-API-Key`.", + "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions. Requires authentication (API key or bearer token).", "operationId": "add_conversation_turn_api_solutions__solution_id__conversations_post", "parameters": [ { @@ -13280,7 +13280,7 @@ }, "/solutions/{solution_id}/conversations/{conversation_id}": { "patch": { - "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user. Requires `X-API-Key`.", + "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user. Requires authentication (API key or bearer token).", "operationId": "mark_conversation_turn_api_solutions__solution_id__conversations__conversation_id__patch", "parameters": [ { @@ -13340,7 +13340,7 @@ }, "/solutions/{solution_id}/knowledge-bases": { "delete": { - "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution. Requires `X-API-Key`.", + "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", "operationId": "unlink_knowledge_bases_api_solutions__solution_id__knowledge_bases_delete", "parameters": [ { @@ -13395,7 +13395,7 @@ ] }, "post": { - "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", + "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", "operationId": "link_knowledge_bases_api_solutions__solution_id__knowledge_bases_post", "parameters": [ { @@ -13452,7 +13452,7 @@ }, "/solutions/{solution_id}/source-connections": { "delete": { - "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution. Requires `X-API-Key`.", + "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", "operationId": "unlink_source_connections_api_solutions__solution_id__source_connections_delete", "parameters": [ { @@ -13507,7 +13507,7 @@ ] }, "post": { - "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", + "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", "operationId": "link_source_connections_api_solutions__solution_id__source_connections_post", "parameters": [ { @@ -13614,7 +13614,7 @@ }, "/sources/": { "get": { - "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires `X-API-Key`. Results are scoped to the API key's account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", + "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the API key's account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", "operationId": "list_sources_api_sources__get", "parameters": [ { @@ -13759,7 +13759,7 @@ ] }, "get": { - "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access sources belonging to your account.", + "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access sources belonging to your account.", "operationId": "get_source_api_sources__source_connection_id__get", "parameters": [ { diff --git a/seclai/__init__.py b/seclai/__init__.py index 8412091..bc141f8 100644 --- a/seclai/__init__.py +++ b/seclai/__init__.py @@ -22,6 +22,10 @@ - :data:`JSONValue` — recursive JSON type alias """ +from .auth import ( + SsoCacheEntry, + SsoProfile, +) from .seclai import ( AgentRunStreamRequest, AsyncSeclai, @@ -34,20 +38,6 @@ SeclaiStreamingError, ) -from .auth import ( - SsoProfile, - SsoCacheEntry, - is_token_valid, - load_sso_profile, - read_sso_cache, - write_sso_cache, - delete_sso_cache, - cache_file_name, - resolve_config_dir, - refresh_token_sync, - refresh_token_async, -) - __all__ = [ "AgentRunStreamRequest", "AsyncSeclai", @@ -60,13 +50,4 @@ "SeclaiStreamingError", "SsoProfile", "SsoCacheEntry", - "is_token_valid", - "load_sso_profile", - "read_sso_cache", - "write_sso_cache", - "delete_sso_cache", - "cache_file_name", - "resolve_config_dir", - "refresh_token_sync", - "refresh_token_async", ] diff --git a/seclai/auth.py b/seclai/auth.py index 6dc9bd6..28045e4 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -15,10 +15,11 @@ import os import tempfile import threading +from collections.abc import Awaitable, Callable from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import Any, Awaitable, Callable +from typing import Any import httpx @@ -260,10 +261,45 @@ def is_token_valid(entry: SsoCacheEntry) -> bool: expires_at = datetime.fromisoformat(entry.expires_at.replace("Z", "+00:00")) except ValueError: return False - now = datetime.now(timezone.utc) + now = datetime.now(UTC) return now + timedelta(seconds=_EXPIRY_BUFFER_SECONDS) < expires_at +# ── Token refresh helpers ─────────────────────────────────────────────────── + + +def _build_refresh_request( + profile: SsoProfile, refresh_token_value: str +) -> tuple[str, dict[str, str]]: + """Build the token refresh URL and form body.""" + token_url = f"https://{profile.sso_domain}/oauth2/token" + body = { + "grant_type": "refresh_token", + "client_id": profile.sso_client_id, + "refresh_token": refresh_token_value, + } + return token_url, body + + +def _parse_refresh_response( + data: dict[str, Any], profile: SsoProfile, refresh_token_value: str +) -> SsoCacheEntry: + """Parse a Cognito token response into an SsoCacheEntry.""" + expires_at = ( + datetime.now(UTC) + timedelta(seconds=data["expires_in"]) + ).isoformat() + + return SsoCacheEntry( + access_token=data["access_token"], + refresh_token=data.get("refresh_token", refresh_token_value), + id_token=data.get("id_token"), + expires_at=expires_at, + client_id=profile.sso_client_id, + region=profile.sso_region, + cognito_domain=profile.sso_domain, + ) + + # ── Token refresh (sync) ───────────────────────────────────────────────────── @@ -285,12 +321,7 @@ def refresh_token_sync( Raises: httpx.HTTPStatusError: If the Cognito token endpoint returns a non-2xx status. """ - token_url = f"https://{profile.sso_domain}/oauth2/token" - body = { - "grant_type": "refresh_token", - "client_id": profile.sso_client_id, - "refresh_token": refresh_token_value, - } + token_url, body = _build_refresh_request(profile, refresh_token_value) client = http_client or httpx.Client() try: @@ -304,22 +335,7 @@ def refresh_token_sync( if http_client is None: client.close() - data = response.json() - from datetime import timedelta - - expires_at = ( - datetime.now(timezone.utc) + timedelta(seconds=data["expires_in"]) - ).isoformat() - - return SsoCacheEntry( - access_token=data["access_token"], - refresh_token=data.get("refresh_token", refresh_token_value), - id_token=data.get("id_token"), - expires_at=expires_at, - client_id=profile.sso_client_id, - region=profile.sso_region, - cognito_domain=profile.sso_domain, - ) + return _parse_refresh_response(response.json(), profile, refresh_token_value) # ── Token refresh (async) ──────────────────────────────────────────────────── diff --git a/seclai/seclai.py b/seclai/seclai.py index a975fae..d6e1ed3 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -41,12 +41,12 @@ import mimetypes import os import time -from collections.abc import AsyncGenerator, Generator, Mapping +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator, Mapping from dataclasses import dataclass from http import HTTPStatus from io import BytesIO from pathlib import Path -from typing import Any, Awaitable, BinaryIO, Callable, Self, TypedDict, cast, overload +from typing import Any, BinaryIO, Self, TypedDict, cast, overload import httpx @@ -231,7 +231,13 @@ def _build_default_headers( headers[auth_state.api_key_header] = auth_state.api_key # type: ignore[assignment] elif auth_state.mode == "bearer_static": headers["authorization"] = f"Bearer {auth_state.access_token}" - if auth_state.account_id: + # X-Account-Id is only meaningful for bearer/SSO modes. + # For api_key mode the key's account is always used. + if auth_state.account_id and auth_state.mode in ( + "bearer_static", + "bearer_provider", + "sso", + ): headers["x-account-id"] = auth_state.account_id if default_headers: headers.update(default_headers) @@ -250,7 +256,10 @@ def _merge_request_headers( ) # For dynamic auth modes, resolve per-request headers if options.auth_state.mode in ("bearer_provider", "sso"): - auth_headers = resolve_auth_headers_sync(options.auth_state) + try: + auth_headers = resolve_auth_headers_sync(options.auth_state) + except (RuntimeError, TypeError) as exc: + raise SeclaiConfigurationError(str(exc)) from exc merged.update(auth_headers) if request_headers: merged.update(request_headers) @@ -269,7 +278,10 @@ async def _merge_request_headers_async( ) # For dynamic auth modes, resolve per-request headers asynchronously if options.auth_state.mode in ("bearer_provider", "sso"): - auth_headers = await resolve_auth_headers_async(options.auth_state) + try: + auth_headers = await resolve_auth_headers_async(options.auth_state) + except (RuntimeError, TypeError) as exc: + raise SeclaiConfigurationError(str(exc)) from exc merged.update(auth_headers) if request_headers: merged.update(request_headers) @@ -398,6 +410,38 @@ def _generated_client(self) -> GeneratedClient: self._owns_generated_client = True return self._generated_client_instance + def _sync_generated_client(self) -> GeneratedClient: + """Return the generated client with dynamic auth headers applied (sync). + + For ``bearer_provider`` and ``sso`` modes, this resolves fresh auth headers + and updates the generated client before returning it. For static modes + (``api_key``, ``bearer_static``) this is identical to :meth:`_generated_client`. + """ + gc = self._generated_client() + if self._options.auth_state.mode in ("bearer_provider", "sso"): + try: + auth_headers = resolve_auth_headers_sync(self._options.auth_state) + except (RuntimeError, TypeError) as exc: + raise SeclaiConfigurationError(str(exc)) from exc + gc.with_headers(auth_headers) + return gc + + async def _async_generated_client(self) -> GeneratedClient: + """Return the generated client with dynamic auth headers applied (async). + + For ``bearer_provider`` and ``sso`` modes, this resolves fresh auth headers + and updates the generated client before returning it. For static modes + (``api_key``, ``bearer_static``) this is identical to :meth:`_generated_client`. + """ + gc = self._generated_client() + if self._options.auth_state.mode in ("bearer_provider", "sso"): + try: + auth_headers = await resolve_auth_headers_async(self._options.auth_state) + except (RuntimeError, TypeError) as exc: + raise SeclaiConfigurationError(str(exc)) from exc + gc.with_headers(auth_headers) + return gc + def _build_url(self, path: str) -> str: """Build an absolute URL string from a request path. @@ -619,7 +663,7 @@ def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunResponse: path = f"/agents/{agent_id}/runs" response = sync_detailed( - agent_id=agent_id, client=self._generated_client(), body=body + agent_id=agent_id, client=self._sync_generated_client(), body=body ) self._raise_for_openapi_response( method="POST", @@ -791,7 +835,7 @@ def list_agent_runs( path = f"/agents/{agent_id}/runs" response = sync_detailed( agent_id=agent_id, - client=self._generated_client(), + client=self._sync_generated_client(), page=page, limit=limit, ) @@ -866,7 +910,7 @@ def get_agent_run( path = f"/agents/runs/{run_id}" response = sync_detailed( run_id=run_id, - client=self._generated_client(), + client=self._sync_generated_client(), include_step_outputs=include_step_outputs, ) self._raise_for_openapi_response( @@ -930,7 +974,7 @@ def delete_agent_run(self, *args: str) -> AgentRunResponse: ) path = f"/agents/runs/{run_id}" - response = sync_detailed(run_id=run_id, client=self._generated_client()) + response = sync_detailed(run_id=run_id, client=self._sync_generated_client()) self._raise_for_openapi_response( method="DELETE", path=path, @@ -987,7 +1031,7 @@ def get_content_detail( path = f"/contents/{source_connection_content_version}" response = sync_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=self._sync_generated_client(), start=start, end=end, ) @@ -1036,7 +1080,7 @@ def delete_content(self, source_connection_content_version: str) -> None: path = f"/contents/{source_connection_content_version}" response = sync_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=self._sync_generated_client(), ) self._raise_for_openapi_response( method="DELETE", @@ -1076,7 +1120,7 @@ def list_content_embeddings( path = f"/contents/{source_connection_content_version}/embeddings" response = sync_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=self._sync_generated_client(), page=page, limit=limit, ) @@ -1139,7 +1183,7 @@ def list_sources( path = "/sources/" response = sync_detailed( - client=self._generated_client(), + client=self._sync_generated_client(), page=page, limit=limit, sort=sort, @@ -1435,7 +1479,7 @@ def upload_file_to_content( endpoint_path = f"/contents/{source_connection_content_version}/upload" response = sync_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=self._sync_generated_client(), body=body, ) self._raise_for_openapi_response( @@ -3803,7 +3847,7 @@ async def run_agent(self, agent_id: str, body: AgentRunRequest) -> AgentRunRespo path = f"/agents/{agent_id}/runs" response = await asyncio_detailed( - agent_id=agent_id, client=self._generated_client(), body=body + agent_id=agent_id, client=(await self._async_generated_client()), body=body ) self._raise_for_openapi_response( method="POST", @@ -3960,7 +4004,7 @@ async def list_agent_runs( path = f"/agents/{agent_id}/runs" response = await asyncio_detailed( agent_id=agent_id, - client=self._generated_client(), + client=(await self._async_generated_client()), page=page, limit=limit, ) @@ -4035,7 +4079,7 @@ async def get_agent_run( path = f"/agents/runs/{run_id}" response = await asyncio_detailed( run_id=run_id, - client=self._generated_client(), + client=(await self._async_generated_client()), include_step_outputs=include_step_outputs, ) self._raise_for_openapi_response( @@ -4103,7 +4147,7 @@ async def delete_agent_run(self, *args: str) -> AgentRunResponse: path = f"/agents/runs/{run_id}" response = await asyncio_detailed( run_id=run_id, - client=self._generated_client(), + client=(await self._async_generated_client()), ) self._raise_for_openapi_response( method="DELETE", @@ -4161,7 +4205,7 @@ async def get_content_detail( path = f"/contents/{source_connection_content_version}" response = await asyncio_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=(await self._async_generated_client()), start=start, end=end, ) @@ -4210,7 +4254,7 @@ async def delete_content(self, source_connection_content_version: str) -> None: path = f"/contents/{source_connection_content_version}" response = await asyncio_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=(await self._async_generated_client()), ) self._raise_for_openapi_response( method="DELETE", @@ -4250,7 +4294,7 @@ async def list_content_embeddings( path = f"/contents/{source_connection_content_version}/embeddings" response = await asyncio_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=(await self._async_generated_client()), page=page, limit=limit, ) @@ -4313,7 +4357,7 @@ async def list_sources( path = "/sources/" response = await asyncio_detailed( - client=self._generated_client(), + client=(await self._async_generated_client()), page=page, limit=limit, sort=sort, @@ -4606,7 +4650,7 @@ async def upload_file_to_content( endpoint_path = f"/contents/{source_connection_content_version}/upload" response = await asyncio_detailed( source_connection_content_version=source_connection_content_version, - client=self._generated_client(), + client=(await self._async_generated_client()), body=body, ) self._raise_for_openapi_response( diff --git a/tests/test_auth_and_headers.py b/tests/test_auth_and_headers.py index 799f957..af03dd7 100644 --- a/tests/test_auth_and_headers.py +++ b/tests/test_auth_and_headers.py @@ -127,3 +127,98 @@ async def handler(request: httpx.Request) -> httpx.Response: client = AsyncSeclai(access_token="async-jwt", http_client=http_client) assert await client.request("GET", "/ping") == {"ok": True} await http_client.aclose() + + +# ── Bearer auth on typed (generated-client) methods ────────────────────────── + + +def test_bearer_token_on_typed_method_sync(monkeypatch: pytest.MonkeyPatch) -> None: + """Typed wrapper methods (list_sources etc.) must send bearer auth headers.""" + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("authorization") == "Bearer typed-jwt" + assert "x-api-key" not in request.headers + return httpx.Response( + 200, + json={ + "data": [], + "pagination": { + "page": 1, + "limit": 20, + "total": 0, + "pages": 0, + "has_next": False, + "has_prev": False, + }, + }, + ) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(access_token="typed-jwt", http_client=http_client) + # Also wire mock transport into the generated client used by typed methods + gc = client._generated_client() + gc.set_httpx_client( + httpx.Client(transport=transport, base_url="https://example.invalid", headers=dict(gc._headers)) + ) + result = client.list_sources() + assert result.data == [] + + +def test_bearer_provider_on_typed_method_sync(monkeypatch: pytest.MonkeyPatch) -> None: + """Typed wrapper methods must resolve dynamic bearer providers per-request.""" + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + call_count = 0 + + def provider() -> str: + nonlocal call_count + call_count += 1 + return f"dyn-token-{call_count}" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.headers.get("authorization", "").startswith("Bearer dyn-token-") + return httpx.Response( + 200, + json={ + "data": [], + "pagination": { + "page": 1, + "limit": 20, + "total": 0, + "pages": 0, + "has_next": False, + "has_prev": False, + }, + }, + ) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(access_token=provider, http_client=http_client) + # Also wire mock transport into the generated client used by typed methods + gc = client._generated_client() + gc.set_httpx_client( + httpx.Client(transport=transport, base_url="https://example.invalid", headers=dict(gc._headers)) + ) + client.list_sources() + client.list_sources() + assert call_count == 2 + + +# ── X-Account-Id only for bearer/SSO ───────────────────────────────────────── + + +def test_account_id_not_sent_for_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + """X-Account-Id should NOT be sent when using api_key auth.""" + monkeypatch.delenv("SECLAI_API_KEY", raising=False) + + def handler(request: httpx.Request) -> httpx.Response: + assert "x-account-id" not in request.headers + return httpx.Response(200, json={"ok": True}) + + transport = httpx.MockTransport(handler) + http_client = httpx.Client(transport=transport, base_url="https://example.invalid") + client = Seclai(api_key="k", account_id="acct-123", http_client=http_client) + assert client.request("GET", "/ping") == {"ok": True} From 3bc592f5a124dd95a158a78bc27044f6da74b0f0 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Wed, 25 Mar 2026 23:32:35 -0700 Subject: [PATCH 3/7] Fixed build failures --- .github/copilot-instructions.md | 32 ++++++++++++++++++++++++++++++++ seclai/auth.py | 10 +++++----- seclai/seclai.py | 4 +++- tests/test_auth_and_headers.py | 12 ++++++++++-- 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..9ebbe69 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +# Copilot Instructions — seclai-python + +## Build & Lint Pipeline + +Run the full CI check before committing: + +```sh +make lint # ruff check + black + mypy +make test # pytest +``` + +Individual commands: + +- **Formatter**: `poetry run black .` — CI enforces formatting; always run before committing +- **Linter**: `poetry run ruff check --fix .` +- **Type checker**: `poetry run mypy .` — strict mode with `warn_unused_ignores` +- **Tests**: `poetry run pytest tests/ -q` + +## Key Rules + +- Always run `poetry run black .` after making changes — CI will fail if formatting differs. +- When fixing a type issue, remove the corresponding `# type: ignore` comment or mypy will error on the unused suppression. +- Do NOT edit files under `seclai/_generated/` — they are auto-generated from the OpenAPI spec. +- The OpenAPI spec at `openapi/seclai.openapi.json` is shared identically with `seclai-go`. Changes must be synced to both repos. +- Use the existing virtualenv (`poetry run ...`); do not create or reconfigure Python environments. + +## Architecture Notes + +- Auth modes: `api_key`, `bearer_static`, `bearer_provider`, `sso`. +- `_build_default_headers()` only sets static auth; dynamic modes (`bearer_provider`, `sso`) are resolved per-request in `_merge_request_headers` / `_merge_request_headers_async`. +- Typed wrapper methods use `_sync_generated_client()` / `_async_generated_client()` which return a `GeneratedClient` with its own internal `httpx.Client` (separate from the SDK's `self._client`). +- In tests, to mock typed methods you must wire the mock transport into the generated client via `gc.set_httpx_client(httpx.Client(transport=transport, base_url=..., headers=dict(gc._headers)))`. diff --git a/seclai/auth.py b/seclai/auth.py index 28045e4..cb8e6cb 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -129,7 +129,9 @@ class AuthState: sso_profile: SsoProfile | None = None config_dir: str | None = None auto_refresh: bool = True - _sync_refresh_lock: threading.Lock = field(default_factory=threading.Lock, repr=False) + _sync_refresh_lock: threading.Lock = field( + default_factory=threading.Lock, repr=False + ) _async_refresh_lock: asyncio.Lock = field(default_factory=asyncio.Lock, repr=False) @@ -285,9 +287,7 @@ def _parse_refresh_response( data: dict[str, Any], profile: SsoProfile, refresh_token_value: str ) -> SsoCacheEntry: """Parse a Cognito token response into an SsoCacheEntry.""" - expires_at = ( - datetime.now(UTC) + timedelta(seconds=data["expires_in"]) - ).isoformat() + expires_at = (datetime.now(UTC) + timedelta(seconds=data["expires_in"])).isoformat() return SsoCacheEntry( access_token=data["access_token"], @@ -525,7 +525,7 @@ async def resolve_auth_headers_async(state: AuthState) -> dict[str, str]: elif state.mode == "bearer_provider": token = state.access_token_provider() # type: ignore[misc] if hasattr(token, "__await__"): - token = await token # type: ignore[misc] + token = await token headers["authorization"] = f"Bearer {token}" elif state.mode == "sso": token = await _resolve_sso_token_async(state) diff --git a/seclai/seclai.py b/seclai/seclai.py index d6e1ed3..aedc859 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -436,7 +436,9 @@ async def _async_generated_client(self) -> GeneratedClient: gc = self._generated_client() if self._options.auth_state.mode in ("bearer_provider", "sso"): try: - auth_headers = await resolve_auth_headers_async(self._options.auth_state) + auth_headers = await resolve_auth_headers_async( + self._options.auth_state + ) except (RuntimeError, TypeError) as exc: raise SeclaiConfigurationError(str(exc)) from exc gc.with_headers(auth_headers) diff --git a/tests/test_auth_and_headers.py b/tests/test_auth_and_headers.py index af03dd7..76648a2 100644 --- a/tests/test_auth_and_headers.py +++ b/tests/test_auth_and_headers.py @@ -160,7 +160,11 @@ def handler(request: httpx.Request) -> httpx.Response: # Also wire mock transport into the generated client used by typed methods gc = client._generated_client() gc.set_httpx_client( - httpx.Client(transport=transport, base_url="https://example.invalid", headers=dict(gc._headers)) + httpx.Client( + transport=transport, + base_url="https://example.invalid", + headers=dict(gc._headers), + ) ) result = client.list_sources() assert result.data == [] @@ -200,7 +204,11 @@ def handler(request: httpx.Request) -> httpx.Response: # Also wire mock transport into the generated client used by typed methods gc = client._generated_client() gc.set_httpx_client( - httpx.Client(transport=transport, base_url="https://example.invalid", headers=dict(gc._headers)) + httpx.Client( + transport=transport, + base_url="https://example.invalid", + headers=dict(gc._headers), + ) ) client.list_sources() client.list_sources() From 5c29f1c28d1d221b27e2780f1b289c6180c4c151 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Thu, 26 Mar 2026 10:38:47 -0700 Subject: [PATCH 4/7] Addressed review comments --- openapi/seclai.openapi.json | 24 ++++++++++++------------ seclai/auth.py | 4 +++- seclai/seclai.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index fbb6e91..df967f3 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -7078,7 +7078,7 @@ "paths": { "/agents": { "get": { - "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "list_agents_api_agents_get", "parameters": [ { @@ -8826,7 +8826,7 @@ }, "/agents/{agent_id}/input-uploads/{upload_id}": { "get": { - "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "api_get_agent_input_upload_status_api_agents__agent_id__input_uploads__upload_id__get", "parameters": [ { @@ -8970,7 +8970,7 @@ ] }, "post": { - "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "run_agent_api_agents__agent_id__runs_post", "parameters": [ { @@ -9026,7 +9026,7 @@ }, "/agents/{agent_id}/runs/stream": { "post": { - "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "run_streaming_agent_api_agents__agent_id__runs_stream_post", "parameters": [ { @@ -9145,7 +9145,7 @@ }, "/agents/{agent_id}/upload-input": { "post": { - "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "api_upload_agent_input_api_agents__agent_id__upload_input_post", "parameters": [ { @@ -9648,7 +9648,7 @@ }, "/alerts": { "get": { - "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association. Results are scoped to the API key's account.", + "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association. Results are scoped to the authenticated account.", "operationId": "list_alerts_api_alerts_get", "parameters": [ { @@ -10970,7 +10970,7 @@ }, "/knowledge_bases": { "get": { - "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "list_knowledge_bases_api_knowledge_bases_get", "parameters": [ { @@ -11243,7 +11243,7 @@ }, "/memory_banks": { "get": { - "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the API key's account.", + "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", "operationId": "list_memory_banks_api_memory_banks_get", "parameters": [ { @@ -12075,7 +12075,7 @@ }, "/models/alerts": { "get": { - "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Alerts are scoped to the API key's account.", + "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Alerts are scoped to the authenticated account.", "operationId": "list_alerts_api_models_alerts_get", "parameters": [ { @@ -12191,7 +12191,7 @@ }, "/models/alerts/unread-count": { "get": { - "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Count is scoped to the API key's account.", + "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Count is scoped to the authenticated account.", "operationId": "get_alert_unread_count_api_models_alerts_unread_count_get", "parameters": [ { @@ -12482,7 +12482,7 @@ }, "/solutions": { "get": { - "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the API key's account.", + "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.", "operationId": "list_solutions_api_solutions_get", "parameters": [ { @@ -13614,7 +13614,7 @@ }, "/sources/": { "get": { - "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the API key's account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", + "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", "operationId": "list_sources_api_sources__get", "parameters": [ { diff --git a/seclai/auth.py b/seclai/auth.py index cb8e6cb..ede5188 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -3,7 +3,9 @@ This module implements the credential provider chain used by :class:`Seclai` and :class:`AsyncSeclai`. -Internal — not part of the public API surface. +The data classes :class:`SsoProfile` and :class:`SsoCacheEntry` are re-exported +from the top-level package as public API for callers that need to interact with +cached SSO tokens directly. All other symbols in this module are internal. """ from __future__ import annotations diff --git a/seclai/seclai.py b/seclai/seclai.py index aedc859..c10ef98 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -260,6 +260,8 @@ def _merge_request_headers( auth_headers = resolve_auth_headers_sync(options.auth_state) except (RuntimeError, TypeError) as exc: raise SeclaiConfigurationError(str(exc)) from exc + except Exception as exc: + raise SeclaiConfigurationError(f"Auth resolution failed: {exc}") from exc merged.update(auth_headers) if request_headers: merged.update(request_headers) @@ -282,6 +284,8 @@ async def _merge_request_headers_async( auth_headers = await resolve_auth_headers_async(options.auth_state) except (RuntimeError, TypeError) as exc: raise SeclaiConfigurationError(str(exc)) from exc + except Exception as exc: + raise SeclaiConfigurationError(f"Auth resolution failed: {exc}") from exc merged.update(auth_headers) if request_headers: merged.update(request_headers) @@ -423,8 +427,22 @@ def _sync_generated_client(self) -> GeneratedClient: auth_headers = resolve_auth_headers_sync(self._options.auth_state) except (RuntimeError, TypeError) as exc: raise SeclaiConfigurationError(str(exc)) from exc - gc.with_headers(auth_headers) - return gc + except Exception as exc: + raise SeclaiConfigurationError( + f"Auth resolution failed: {exc}" + ) from exc + # with_headers() mutates existing httpx clients in-place and + # returns an evolved copy with updated _headers. We keep the + # original instance (preserving any user-set httpx client) but + # store the evolved copy so get_httpx_client() will use the + # refreshed headers when it lazily creates an httpx.Client. + evolved = gc.with_headers(auth_headers) + self._generated_client_instance = evolved + # Re-attach the existing httpx client to the evolved instance + # so pre-configured transports (e.g. in tests) aren't lost. + if gc._client is not None: + evolved.set_httpx_client(gc._client) + return self._generated_client_instance # type: ignore[return-value] async def _async_generated_client(self) -> GeneratedClient: """Return the generated client with dynamic auth headers applied (async). @@ -441,8 +459,15 @@ async def _async_generated_client(self) -> GeneratedClient: ) except (RuntimeError, TypeError) as exc: raise SeclaiConfigurationError(str(exc)) from exc - gc.with_headers(auth_headers) - return gc + except Exception as exc: + raise SeclaiConfigurationError( + f"Auth resolution failed: {exc}" + ) from exc + evolved = gc.with_headers(auth_headers) + self._generated_client_instance = evolved + if gc._async_client is not None: + evolved.set_async_httpx_client(gc._async_client) + return self._generated_client_instance # type: ignore[return-value] def _build_url(self, path: str) -> str: """Build an absolute URL string from a request path. From 6c7ed063f1c4ff26f16e3d397f5681ba4ae8bfe8 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Thu, 26 Mar 2026 23:35:36 -0700 Subject: [PATCH 5/7] Added /me endpoint and made SSO login tweaks --- README.md | 19 ++- openapi/seclai.openapi.json | 208 ++++++++++++++++++++++----------- seclai/__init__.py | 6 + seclai/auth.py | 106 +++++++++-------- tests/test_auth_and_headers.py | 7 +- 5 files changed, 222 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 6fe3000..4b3ecd7 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Credentials are resolved via a chain (first match wins): 1. Explicit `api_key` option 2. Explicit `access_token` option (string or callable) 3. `SECLAI_API_KEY` environment variable -4. SSO profile from `~/.seclai/config` with cached tokens in `~/.seclai/sso/cache/` +4. SSO — cached tokens from `~/.seclai/sso/cache/` (always available as fallback) ```python # API key @@ -125,13 +125,24 @@ client = Seclai(profile="my-profile") client = Seclai() ``` -To set up SSO authentication, install the [Seclai CLI](https://pypi.org/project/seclai/) and run: +#### SSO authentication + +SSO is the default fallback when no explicit credentials are provided. The SDK +includes built-in production SSO defaults, so no configuration is needed: ```bash -seclai configure sso # set up an SSO profile -seclai auth login # authenticate via browser +npx @seclai/cli auth login # authenticate via browser — works immediately ``` +To customize SSO settings (e.g. for a staging environment), use `seclai configure sso` +or set environment variables: + +| Variable | Description | Default | +|---|---|---| +| `SECLAI_SSO_DOMAIN` | Cognito domain | `auth.seclai.com` | +| `SECLAI_SSO_CLIENT_ID` | Cognito app client ID | `4bgf8v9qmc5puivbaqon9n5lmr` | +| `SECLAI_SSO_REGION` | AWS region | `us-west-2` | + ## API documentation Online API documentation (latest): diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index df967f3..39566f3 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -3310,6 +3310,28 @@ "title": "MarkConversationTurnRequest", "type": "object" }, + "MeResponse": { + "properties": { + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" + }, + "organizations": { + "items": { + "$ref": "#/components/schemas/OrganizationInfoResponse" + }, + "title": "Organizations", + "type": "array" + } + }, + "required": [ + "account_id", + "organizations" + ], + "title": "MeResponse", + "type": "object" + }, "MemoryBankAiAssistantResponse": { "description": "Response from the memory bank AI assistant.", "properties": { @@ -3701,6 +3723,31 @@ "title": "OrganizationAlertPreferenceListResponse", "type": "object" }, + "OrganizationInfoResponse": { + "properties": { + "account_id": { + "format": "uuid", + "title": "Account Id", + "type": "string" + }, + "id": { + "format": "uuid", + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "id", + "name", + "account_id" + ], + "title": "OrganizationInfoResponse", + "type": "object" + }, "PaginationResponse": { "description": "Pagination information.", "properties": { @@ -7078,7 +7125,7 @@ "paths": { "/agents": { "get": { - "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "list_agents_api_agents_get", "parameters": [ { @@ -7140,7 +7187,7 @@ ] }, "post": { - "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Agent is created in the API key's account.", + "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires `X-API-Key`. Agent is created in the API key's account.", "operationId": "create_agent_api_agents_post", "parameters": [ { @@ -7721,7 +7768,7 @@ }, "/agents/runs/search": { "post": { - "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Searches only within your account's traces.", + "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires `X-API-Key`. Searches only within your account's traces.", "operationId": "search_agent_runs_api_agents_runs_search_post", "parameters": [ { @@ -7768,7 +7815,7 @@ }, "/agents/runs/{run_id}": { "delete": { - "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only cancel runs belonging to your account.", + "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only cancel runs belonging to your account.", "operationId": "delete_agent_run_api_agents_runs__run_id__delete", "parameters": [ { @@ -7812,7 +7859,7 @@ ] }, "get": { - "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access runs belonging to your account.", + "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access runs belonging to your account.", "operationId": "get_agent_run_api_agents_runs__run_id__get", "parameters": [ { @@ -7870,7 +7917,7 @@ }, "/agents/{agent_id}": { "delete": { - "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete agents belonging to your account.", + "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only delete agents belonging to your account.", "operationId": "delete_agent_api_agents__agent_id__delete", "parameters": [ { @@ -7907,7 +7954,7 @@ ] }, "get": { - "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", + "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access agents belonging to your account.", "operationId": "get_agent_metadata_api_agents__agent_id__get", "parameters": [ { @@ -7951,7 +7998,7 @@ ] }, "put": { - "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", + "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update agents belonging to your account.", "operationId": "update_agent_api_agents__agent_id__put", "parameters": [ { @@ -8007,7 +8054,7 @@ }, "/agents/{agent_id}/ai-assistant/conversations": { "get": { - "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be queried.", + "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be queried.", "operationId": "get_ai_conversation_history_api_agents__agent_id__ai_assistant_conversations_get", "parameters": [ { @@ -8109,7 +8156,7 @@ }, "/agents/{agent_id}/ai-assistant/generate-steps": { "post": { - "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be used.", "operationId": "generate_agent_steps_api_agents__agent_id__ai_assistant_generate_steps_post", "parameters": [ { @@ -8165,7 +8212,7 @@ }, "/agents/{agent_id}/ai-assistant/step-config": { "post": { - "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. Only agents belonging to your account can be used.", "operationId": "generate_step_config_api_agents__agent_id__ai_assistant_step_config_post", "parameters": [ { @@ -8221,7 +8268,7 @@ }, "/agents/{agent_id}/ai-assistant/{conversation_id}": { "patch": { - "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The conversation must belong to one of your agents.", + "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires a user-scoped `X-API-Key`. The conversation must belong to one of your agents.", "operationId": "mark_ai_suggestion_api_agents__agent_id__ai_assistant__conversation_id__patch", "parameters": [ { @@ -8290,7 +8337,7 @@ }, "/agents/{agent_id}/definition": { "get": { - "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", + "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access agents belonging to your account.", "operationId": "get_agent_definition_api_agents__agent_id__definition_get", "parameters": [ { @@ -8334,7 +8381,7 @@ ] }, "put": { - "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", + "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update agents belonging to your account.", "operationId": "update_agent_definition_api_agents__agent_id__definition_put", "parameters": [ { @@ -8826,7 +8873,7 @@ }, "/agents/{agent_id}/input-uploads/{upload_id}": { "get": { - "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "api_get_agent_input_upload_status_api_agents__agent_id__input_uploads__upload_id__get", "parameters": [ { @@ -8881,7 +8928,7 @@ }, "/agents/{agent_id}/runs": { "get": { - "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only list runs for agents in your account.", + "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only list runs for agents in your account.", "operationId": "list_agent_runs_api_agents__agent_id__runs_get", "parameters": [ { @@ -8970,7 +9017,7 @@ ] }, "post": { - "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "run_agent_api_agents__agent_id__runs_post", "parameters": [ { @@ -9026,7 +9073,7 @@ }, "/agents/{agent_id}/runs/stream": { "post": { - "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "run_streaming_agent_api_agents__agent_id__runs_stream_post", "parameters": [ { @@ -9145,7 +9192,7 @@ }, "/agents/{agent_id}/upload-input": { "post": { - "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "api_upload_agent_input_api_agents__agent_id__upload_input_post", "parameters": [ { @@ -9648,7 +9695,7 @@ }, "/alerts": { "get": { - "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association. Results are scoped to the authenticated account.", + "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires `X-API-Key` with user association. Results are scoped to the API key's account.", "operationId": "list_alerts_api_alerts_get", "parameters": [ { @@ -9806,7 +9853,7 @@ }, "/alerts/configs": { "get": { - "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "list_alert_configs_api_alerts_configs_get", "parameters": [ { @@ -9897,7 +9944,7 @@ ] }, "post": { - "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "create_alert_config_api_alerts_configs_post", "parameters": [ { @@ -9946,7 +9993,7 @@ }, "/alerts/configs/{config_id}": { "delete": { - "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "delete_alert_config_api_alerts_configs__config_id__delete", "parameters": [ { @@ -9983,7 +10030,7 @@ ] }, "get": { - "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "get_alert_config_api_alerts_configs__config_id__get", "parameters": [ { @@ -10029,7 +10076,7 @@ ] }, "patch": { - "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "update_alert_config_api_alerts_configs__config_id__patch", "parameters": [ { @@ -10087,7 +10134,7 @@ }, "/alerts/organization-preferences/list": { "get": { - "description": "List per-organization alert delivery preferences for the API key's associated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only organizations where the user is an owner or administrator are included.", + "description": "List per-organization alert delivery preferences for the API key's associated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.\n- Only organizations where the user is an owner or administrator are included.", "operationId": "list_organization_preferences_api_alerts_organization_preferences_list_get", "parameters": [ { @@ -10155,7 +10202,7 @@ }, "/alerts/organization-preferences/{organization_id}/{alert_type}": { "patch": { - "description": "Update the API key user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only owners and administrators can update preferences for an organization.", + "description": "Update the API key user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.\n- Only owners and administrators can update preferences for an organization.", "operationId": "update_organization_preference_api_alerts_organization_preferences__organization_id___alert_type__patch", "parameters": [ { @@ -10221,7 +10268,7 @@ }, "/alerts/{alert_id}": { "get": { - "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "get_alert_detail_api_alerts__alert_id__get", "parameters": [ { @@ -10269,7 +10316,7 @@ }, "/alerts/{alert_id}/comments": { "post": { - "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "add_alert_comment_api_alerts__alert_id__comments_post", "parameters": [ { @@ -10327,7 +10374,7 @@ }, "/alerts/{alert_id}/status": { "post": { - "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "change_alert_status_api_alerts__alert_id__status_post", "parameters": [ { @@ -10385,7 +10432,7 @@ }, "/alerts/{alert_id}/subscribe": { "post": { - "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "subscribe_to_alert_api_alerts__alert_id__subscribe_post", "parameters": [ { @@ -10433,7 +10480,7 @@ }, "/alerts/{alert_id}/unsubscribe": { "post": { - "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires `X-API-Key` with user association.", "operationId": "unsubscribe_from_alert_api_alerts__alert_id__unsubscribe_post", "parameters": [ { @@ -10481,7 +10528,7 @@ }, "/contents/{source_connection_content_version}": { "delete": { - "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete content belonging to your account.", + "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only delete content belonging to your account.", "operationId": "delete_content_api_contents__source_connection_content_version__delete", "parameters": [ { @@ -10518,7 +10565,7 @@ ] }, "get": { - "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access content belonging to your account.", + "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access content belonging to your account.", "operationId": "get_content_detail_api_contents__source_connection_content_version__get", "parameters": [ { @@ -10638,7 +10685,7 @@ }, "/contents/{source_connection_content_version}/embeddings": { "get": { - "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access embeddings for content belonging to your account.", + "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access embeddings for content belonging to your account.", "operationId": "list_content_embeddings_api_contents__source_connection_content_version__embeddings_get", "parameters": [ { @@ -10704,7 +10751,7 @@ }, "/contents/{source_connection_content_version}/upload": { "post": { - "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only replace content belonging to your account.", + "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only replace content belonging to your account.", "operationId": "upload_file_to_content_api_contents__source_connection_content_version__upload_post", "parameters": [ { @@ -10970,7 +11017,7 @@ }, "/knowledge_bases": { "get": { - "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "list_knowledge_bases_api_knowledge_bases_get", "parameters": [ { @@ -11143,7 +11190,7 @@ ] }, "get": { - "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access knowledge bases belonging to your account.", + "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access knowledge bases belonging to your account.", "operationId": "get_knowledge_base_api_knowledge_bases__knowledge_base_id__get", "parameters": [ { @@ -11187,7 +11234,7 @@ ] }, "put": { - "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update knowledge bases belonging to your account.", + "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only update knowledge bases belonging to your account.", "operationId": "update_knowledge_base_api_knowledge_bases__knowledge_base_id__put", "parameters": [ { @@ -11241,9 +11288,36 @@ ] } }, + "/me": { + "get": { + "description": "Returns the authenticated user's personal account ID and a list of organisations they belong to. Each organisation entry includes the organisation's own id, display name, and account_id. Useful for CLI tooling that needs to let the user pick an org context.", + "operationId": "get_me_api_me_get", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeResponse" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Get current user identity", + "tags": [ + "identity" + ] + } + }, "/memory_banks": { "get": { - "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. All resources are scoped to the API key's account.", "operationId": "list_memory_banks_api_memory_banks_get", "parameters": [ { @@ -11685,7 +11759,7 @@ ] }, "get": { - "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access memory banks belonging to your account.", + "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access memory banks belonging to your account.", "operationId": "get_memory_bank_api_memory_banks__memory_bank_id__get", "parameters": [ { @@ -12075,7 +12149,7 @@ }, "/models/alerts": { "get": { - "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Alerts are scoped to the authenticated account.", + "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires `X-API-Key`. Alerts are scoped to the API key's account.", "operationId": "list_alerts_api_models_alerts_get", "parameters": [ { @@ -12171,7 +12245,7 @@ }, "/models/alerts/mark-all-read": { "post": { - "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Scoped to the API key's account.", + "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires `X-API-Key`. Scoped to the API key's account.", "operationId": "mark_all_read_api_models_alerts_mark_all_read_post", "parameters": [ { @@ -12191,7 +12265,7 @@ }, "/models/alerts/unread-count": { "get": { - "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Count is scoped to the authenticated account.", + "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires `X-API-Key`. Count is scoped to the API key's account.", "operationId": "get_alert_unread_count_api_models_alerts_unread_count_get", "parameters": [ { @@ -12220,7 +12294,7 @@ }, "/models/alerts/{alert_id}/read": { "patch": { - "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The alert must belong to the API key's account.", + "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires `X-API-Key`. The alert must belong to the API key's account.", "operationId": "mark_read_api_models_alerts__alert_id__read_patch", "parameters": [ { @@ -12260,7 +12334,7 @@ }, "/models/{model_id}/recommendations": { "get": { - "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token).", + "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires `X-API-Key`.", "operationId": "get_recommendations_api_models__model_id__recommendations_get", "parameters": [ { @@ -12482,7 +12556,7 @@ }, "/solutions": { "get": { - "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.", + "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires `X-API-Key`. Results are scoped to the API key's account.", "operationId": "list_solutions_api_solutions_get", "parameters": [ { @@ -12587,7 +12661,7 @@ ] }, "post": { - "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires authentication (API key or bearer token).", + "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires `X-API-Key`.", "operationId": "create_solution_api_solutions_post", "parameters": [ { @@ -12634,7 +12708,7 @@ }, "/solutions/{solution_id}": { "delete": { - "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted. Requires authentication (API key or bearer token).", + "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted. Requires `X-API-Key`.", "operationId": "delete_solution_api_solutions__solution_id__delete", "parameters": [ { @@ -12672,7 +12746,7 @@ ] }, "get": { - "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information. Requires authentication (API key or bearer token).", + "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information. Requires `X-API-Key`.", "operationId": "get_solution_api_solutions__solution_id__get", "parameters": [ { @@ -12717,7 +12791,7 @@ ] }, "patch": { - "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged. Requires authentication (API key or bearer token).", + "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged. Requires `X-API-Key`.", "operationId": "update_solution_api_solutions__solution_id__patch", "parameters": [ { @@ -12774,7 +12848,7 @@ }, "/solutions/{solution_id}/agents": { "delete": { - "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources. Requires authentication (API key or bearer token).", + "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources. Requires `X-API-Key`.", "operationId": "unlink_agents_api_solutions__solution_id__agents_delete", "parameters": [ { @@ -12829,7 +12903,7 @@ ] }, "post": { - "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", "operationId": "link_agents_api_solutions__solution_id__agents_post", "parameters": [ { @@ -12886,7 +12960,7 @@ }, "/solutions/{solution_id}/ai-assistant/generate": { "post": { - "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", + "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint. Requires `X-API-Key`.\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", "operationId": "ai_assistant_generate_api_solutions__solution_id__ai_assistant_generate_post", "parameters": [ { @@ -12943,7 +13017,7 @@ }, "/solutions/{solution_id}/ai-assistant/knowledge-base": { "post": { - "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).", + "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint. Requires `X-API-Key`.", "operationId": "ai_assistant_knowledge_base_api_solutions__solution_id__ai_assistant_knowledge_base_post", "parameters": [ { @@ -13000,7 +13074,7 @@ }, "/solutions/{solution_id}/ai-assistant/source": { "post": { - "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan. Requires authentication (API key or bearer token).", + "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan. Requires `X-API-Key`.", "operationId": "ai_assistant_source_api_solutions__solution_id__ai_assistant_source_post", "parameters": [ { @@ -13057,7 +13131,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status. Requires authentication (API key or bearer token).", + "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status. Requires `X-API-Key`.", "operationId": "ai_assistant_accept_api_solutions__solution_id__ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -13124,7 +13198,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed. Requires authentication (API key or bearer token).", + "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed. Requires `X-API-Key`.", "operationId": "ai_assistant_decline_api_solutions__solution_id__ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -13174,7 +13248,7 @@ }, "/solutions/{solution_id}/conversations": { "get": { - "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status. Requires authentication (API key or bearer token).", + "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status. Requires `X-API-Key`.", "operationId": "list_conversations_api_solutions__solution_id__conversations_get", "parameters": [ { @@ -13223,7 +13297,7 @@ ] }, "post": { - "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions. Requires authentication (API key or bearer token).", + "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions. Requires `X-API-Key`.", "operationId": "add_conversation_turn_api_solutions__solution_id__conversations_post", "parameters": [ { @@ -13280,7 +13354,7 @@ }, "/solutions/{solution_id}/conversations/{conversation_id}": { "patch": { - "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user. Requires authentication (API key or bearer token).", + "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user. Requires `X-API-Key`.", "operationId": "mark_conversation_turn_api_solutions__solution_id__conversations__conversation_id__patch", "parameters": [ { @@ -13340,7 +13414,7 @@ }, "/solutions/{solution_id}/knowledge-bases": { "delete": { - "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", + "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution. Requires `X-API-Key`.", "operationId": "unlink_knowledge_bases_api_solutions__solution_id__knowledge_bases_delete", "parameters": [ { @@ -13395,7 +13469,7 @@ ] }, "post": { - "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", "operationId": "link_knowledge_bases_api_solutions__solution_id__knowledge_bases_post", "parameters": [ { @@ -13452,7 +13526,7 @@ }, "/solutions/{solution_id}/source-connections": { "delete": { - "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", + "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution. Requires `X-API-Key`.", "operationId": "unlink_source_connections_api_solutions__solution_id__source_connections_delete", "parameters": [ { @@ -13507,7 +13581,7 @@ ] }, "post": { - "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources. Requires `X-API-Key`.", "operationId": "link_source_connections_api_solutions__solution_id__source_connections_post", "parameters": [ { @@ -13614,7 +13688,7 @@ }, "/sources/": { "get": { - "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", + "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires `X-API-Key`. Results are scoped to the API key's account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", "operationId": "list_sources_api_sources__get", "parameters": [ { @@ -13759,7 +13833,7 @@ ] }, "get": { - "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access sources belonging to your account.", + "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires `X-API-Key`. You can only access sources belonging to your account.", "operationId": "get_source_api_sources__source_connection_id__get", "parameters": [ { diff --git a/seclai/__init__.py b/seclai/__init__.py index bc141f8..81c18d1 100644 --- a/seclai/__init__.py +++ b/seclai/__init__.py @@ -23,6 +23,9 @@ """ from .auth import ( + DEFAULT_SSO_CLIENT_ID, + DEFAULT_SSO_DOMAIN, + DEFAULT_SSO_REGION, SsoCacheEntry, SsoProfile, ) @@ -41,6 +44,9 @@ __all__ = [ "AgentRunStreamRequest", "AsyncSeclai", + "DEFAULT_SSO_CLIENT_ID", + "DEFAULT_SSO_DOMAIN", + "DEFAULT_SSO_REGION", "JSONValue", "Seclai", "SeclaiAPIStatusError", diff --git a/seclai/auth.py b/seclai/auth.py index ede5188..80de15c 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -32,6 +32,13 @@ _SSO_EXPIRED_MSG = "SSO token expired. Run `seclai auth login` to re-authenticate." +#: Default SSO domain (production Cognito). Override with ``SECLAI_SSO_DOMAIN`` or config file. +DEFAULT_SSO_DOMAIN = "auth.seclai.com" +#: Default SSO client ID (production public client). Override with ``SECLAI_SSO_CLIENT_ID`` or config file. +DEFAULT_SSO_CLIENT_ID = "4bgf8v9qmc5puivbaqon9n5lmr" +#: Default SSO region. Override with ``SECLAI_SSO_REGION`` or config file. +DEFAULT_SSO_REGION = "us-west-2" + # ── Types ───────────────────────────────────────────────────────────────────── @@ -41,16 +48,16 @@ class SsoProfile: """Resolved SSO profile settings from the config file. Attributes: - sso_account_id: AWS Cognito account ID. + sso_account_id: Account ID (resolved after login via ``/me``). sso_region: AWS region for the Cognito user pool. sso_client_id: Cognito app client ID. sso_domain: Cognito domain (e.g. ``"auth.example.com"``). """ - sso_account_id: str sso_region: str sso_client_id: str sso_domain: str + sso_account_id: str | None = None @dataclass(frozen=True, slots=True) @@ -164,42 +171,51 @@ def parse_ini_config(config_path: Path) -> configparser.ConfigParser: return cp -def load_sso_profile(config_dir: Path, profile_name: str) -> SsoProfile | None: +def load_sso_profile(config_dir: Path, profile_name: str) -> SsoProfile: """Load and resolve an SSO profile from the config file. Non-default profiles inherit unset values from ``[default]``. + All profiles fall back to built-in defaults and environment variable + overrides (``SECLAI_SSO_DOMAIN``, ``SECLAI_SSO_CLIENT_ID``, + ``SECLAI_SSO_REGION``). Always returns a valid profile. """ config_path = config_dir / _CONFIG_FILE - if not config_path.exists(): - return None - - cp = parse_ini_config(config_path) - - default_section: dict[str, str] = {} - if cp.has_section("default"): - default_section = dict(cp.items("default")) - if profile_name == "default": - section = default_section - else: - section_name = f"profile {profile_name}" - if not cp.has_section(section_name): - return None - section = {**default_section, **dict(cp.items(section_name))} + merged: dict[str, str] = {} - sso_account_id = section.get("sso_account_id") - sso_region = section.get("sso_region") - sso_client_id = section.get("sso_client_id") - sso_domain = section.get("sso_domain") - - if not all([sso_account_id, sso_region, sso_client_id, sso_domain]): - return None + if config_path.exists(): + cp = parse_ini_config(config_path) + + default_section: dict[str, str] = {} + if cp.has_section("default"): + default_section = dict(cp.items("default")) + + if profile_name == "default": + merged = default_section + else: + section_name = f"profile {profile_name}" + if cp.has_section(section_name): + merged = {**default_section, **dict(cp.items(section_name))} + + # Environment variables override config file values + sso_domain = ( + os.getenv("SECLAI_SSO_DOMAIN") or merged.get("sso_domain") or DEFAULT_SSO_DOMAIN + ) + sso_client_id = ( + os.getenv("SECLAI_SSO_CLIENT_ID") + or merged.get("sso_client_id") + or DEFAULT_SSO_CLIENT_ID + ) + sso_region = ( + os.getenv("SECLAI_SSO_REGION") or merged.get("sso_region") or DEFAULT_SSO_REGION + ) + sso_account_id = merged.get("sso_account_id") or None return SsoProfile( - sso_account_id=sso_account_id, # type: ignore[arg-type] - sso_region=sso_region, # type: ignore[arg-type] - sso_client_id=sso_client_id, # type: ignore[arg-type] - sso_domain=sso_domain, # type: ignore[arg-type] + sso_region=sso_region, + sso_client_id=sso_client_id, + sso_domain=sso_domain, + sso_account_id=sso_account_id, ) @@ -446,27 +462,17 @@ def resolve_credential_chain( auto_refresh=False, ) - # 5. SSO profile - try: - resolved_dir = resolve_config_dir(config_dir) - profile_name = profile or os.getenv("SECLAI_PROFILE") or "default" - sso = load_sso_profile(resolved_dir, profile_name) - if sso: - return AuthState( - mode="sso", - api_key_header=api_key_header, - account_id=account_id or sso.sso_account_id, - sso_profile=sso, - config_dir=str(resolved_dir), - auto_refresh=auto_refresh, - ) - except (OSError, configparser.Error): - pass - - # 6. Nothing found - raise RuntimeError( - "Missing credentials. Pass api_key=..., access_token=..., " - "set SECLAI_API_KEY, or run `seclai auth login`." + # 5. SSO profile (always available via built-in defaults) + resolved_dir = resolve_config_dir(config_dir) + profile_name = profile or os.getenv("SECLAI_PROFILE") or "default" + sso = load_sso_profile(resolved_dir, profile_name) + return AuthState( + mode="sso", + api_key_header=api_key_header, + account_id=account_id or sso.sso_account_id, + sso_profile=sso, + config_dir=str(resolved_dir), + auto_refresh=auto_refresh, ) diff --git a/tests/test_auth_and_headers.py b/tests/test_auth_and_headers.py index 76648a2..d7aaad5 100644 --- a/tests/test_auth_and_headers.py +++ b/tests/test_auth_and_headers.py @@ -16,11 +16,12 @@ def test_api_key_param_takes_precedence(monkeypatch: pytest.MonkeyPatch) -> None assert client.api_key == "param-key" -def test_missing_api_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: +def test_missing_api_key_falls_back_to_sso(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SECLAI_API_KEY", raising=False) monkeypatch.setenv("SECLAI_CONFIG_DIR", "/nonexistent-seclai-dir") - with pytest.raises(SeclaiConfigurationError): - _ = Seclai() + client = Seclai() + # With built-in SSO defaults, client succeeds and falls back to SSO mode + assert client._options.auth_state.mode == "sso" def test_both_api_key_and_access_token_raises(monkeypatch: pytest.MonkeyPatch) -> None: From 59526605c167565caccd5a6e46724abb20374033 Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Fri, 27 Mar 2026 14:54:44 -0700 Subject: [PATCH 6/7] Addressed review comments --- openapi/seclai.openapi.json | 49 +++++++++++++++++-------------------- seclai/auth.py | 10 +++++--- seclai/seclai.py | 9 ++++--- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index e4fa919..1b40648 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -7187,7 +7187,7 @@ ] }, "post": { - "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Agent is created in the API key's account.", + "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Agent is created in the authenticated account.", "operationId": "create_agent_api_agents_post", "parameters": [ { @@ -9238,7 +9238,7 @@ }, "/ai-assistant/feedback": { "post": { - "description": "Submit thumbs-up/down feedback on any AI assistant interaction. Negative feedback with a comment is analyzed for concerning issues.\n\nAuth: requires ``X-API-Key``.", + "description": "Submit thumbs-up/down feedback on any AI assistant interaction. Negative feedback with a comment is analyzed for concerning issues.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_feedback_api_ai_assistant_feedback_post", "parameters": [ { @@ -9285,7 +9285,7 @@ }, "/ai-assistant/knowledge-base": { "post": { - "description": "Generate a knowledge base creation/modification plan without requiring an existing solution. May also propose prerequisite source creation actions.\n\nAuth: requires ``X-API-Key``.", + "description": "Generate a knowledge base creation/modification plan without requiring an existing solution. May also propose prerequisite source creation actions.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_knowledge_base_api_ai_assistant_knowledge_base_post", "parameters": [ { @@ -9332,7 +9332,7 @@ }, "/ai-assistant/memory-bank": { "post": { - "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings.\n\nAuth: requires ``X-API-Key``.", + "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_memory_bank_api_ai_assistant_memory_bank_post", "parameters": [ { @@ -9379,7 +9379,7 @@ }, "/ai-assistant/memory-bank/last-conversation": { "get": { - "description": "Fetch the most recent memory bank AI assistant conversation turns for the authenticated user. Returns turns in oldest-first order with a total count for pagination via limit/offset query parameters.\n\nAuth: requires ``X-API-Key``.", + "description": "Fetch the most recent memory bank AI assistant conversation turns for the authenticated user. Returns turns in oldest-first order with a total count for pagination via limit/offset query parameters.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_memory_bank_history_api_ai_assistant_memory_bank_last_conversation_get", "parameters": [ { @@ -9443,7 +9443,7 @@ }, "/ai-assistant/memory-bank/{conversation_id}": { "patch": { - "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires ``X-API-Key``.", + "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_memory_bank_accept_api_ai_assistant_memory_bank__conversation_id__patch", "parameters": [ { @@ -9504,7 +9504,7 @@ }, "/ai-assistant/solution": { "post": { - "description": "Generate a complete solution plan covering sources, knowledge bases, and agents without requiring an existing solution. Supports SSE streaming when ``Accept: text/event-stream`` is set.\n\nAuth: requires ``X-API-Key``.", + "description": "Generate a complete solution plan covering sources, knowledge bases, and agents without requiring an existing solution. Supports SSE streaming when ``Accept: text/event-stream`` is set.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_solution_api_ai_assistant_solution_post", "parameters": [ { @@ -9551,7 +9551,7 @@ }, "/ai-assistant/source": { "post": { - "description": "Generate a content source creation/modification plan without requiring an existing solution. The AI proposes actions for the user to review before any changes are made.\n\nAuth: requires ``X-API-Key``.", + "description": "Generate a content source creation/modification plan without requiring an existing solution. The AI proposes actions for the user to review before any changes are made.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_source_api_ai_assistant_source_post", "parameters": [ { @@ -9598,7 +9598,7 @@ }, "/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Accept and execute a previously proposed standalone plan. If the plan contains destructive actions (deletions), ``confirm_deletions`` must be set to true.\n\nAuth: requires ``X-API-Key``.", + "description": "Accept and execute a previously proposed standalone plan. If the plan contains destructive actions (deletions), ``confirm_deletions`` must be set to true.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_accept_api_ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -9655,7 +9655,7 @@ }, "/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Decline a previously proposed standalone plan. No resources are modified. The conversation is marked as declined.\n\nAuth: requires ``X-API-Key``.", + "description": "Decline a previously proposed standalone plan. No resources are modified. The conversation is marked as declined.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "api_ai_decline_api_ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -10807,7 +10807,7 @@ }, "/governance/ai-assistant": { "post": { - "description": "Send a natural-language request to the governance AI assistant to generate a plan of policy changes. Returns a conversation with proposed actions that can be accepted or declined.\n\nAuth: requires `X-API-Key` with governance access.", + "description": "Send a natural-language request to the governance AI assistant to generate a plan of policy changes. Returns a conversation with proposed actions that can be accepted or declined.\n\nAuth: requires authentication (API key or bearer token) with governance access.", "operationId": "governance_ai_generate_api_governance_ai_assistant_post", "parameters": [ { @@ -10860,7 +10860,7 @@ }, "/governance/ai-assistant/conversations": { "get": { - "description": "Return recent governance AI assistant conversations for the account, ordered by most recent first.\n\nAuth: requires `X-API-Key` with governance access.", + "description": "Return recent governance AI assistant conversations for the account, ordered by most recent first.\n\nAuth: requires authentication (API key or bearer token) with governance access.", "operationId": "list_governance_ai_conversations_api_governance_ai_assistant_conversations_get", "parameters": [ { @@ -10918,7 +10918,7 @@ }, "/governance/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Execute the proposed policy changes from a governance AI assistant conversation. Each action is applied in order and results are returned.\n\nAuth: requires `X-API-Key` with governance access.", + "description": "Execute the proposed policy changes from a governance AI assistant conversation. Each action is applied in order and results are returned.\n\nAuth: requires authentication (API key or bearer token) with governance access.", "operationId": "governance_ai_accept_api_governance_ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -10971,7 +10971,7 @@ }, "/governance/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Reject a previously proposed governance AI assistant plan without applying any changes. The conversation is marked as declined and no policy modifications are made.\n\nAuth: requires `X-API-Key` with governance access.", + "description": "Reject a previously proposed governance AI assistant plan without applying any changes. The conversation is marked as declined and no policy modifications are made.\n\nAuth: requires authentication (API key or bearer token) with governance access.", "operationId": "governance_ai_decline_api_governance_ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -11290,13 +11290,8 @@ }, "/me": { "get": { - "description": "Returns the authenticated user's personal account ID and a list of organisations they belong to. Each organisation entry includes the organisation's own id, display name, and account_id. Useful for CLI tooling that needs to let the user pick an org context.", + "description": "Returns the authenticated user's personal account ID and a list of organizations they belong to. Each organization entry includes the organization's own id, name, and account_id. Useful for CLI tooling that needs to let the user pick an organization context.", "operationId": "get_me_api_me_get", - "parameters": [ - { - "$ref": "#/components/parameters/X-Account-Id" - } - ], "responses": { "200": { "content": { @@ -11468,7 +11463,7 @@ }, "/memory_banks/ai-assistant": { "post": { - "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings based on the user's description.\n\nAuth: requires `X-API-Key`.", + "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings based on the user's description.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "memory_bank_ai_generate_api_memory_banks_ai_assistant_post", "parameters": [ { @@ -11518,7 +11513,7 @@ }, "/memory_banks/ai-assistant/last-conversation": { "get": { - "description": "Fetch the most recent memory bank AI assistant conversation turns for the current user. Supports pagination via limit/offset.\n\nAuth: requires `X-API-Key`.", + "description": "Fetch the most recent memory bank AI assistant conversation turns for the current user. Supports pagination via limit/offset.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "memory_bank_ai_last_conversation_api_memory_banks_ai_assistant_last_conversation_get", "parameters": [ { @@ -11582,7 +11577,7 @@ }, "/memory_banks/ai-assistant/{conversation_id}": { "patch": { - "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires `X-API-Key`.", + "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires authentication (API key or bearer token).", "operationId": "memory_bank_ai_accept_api_memory_banks_ai_assistant__conversation_id__patch", "parameters": [ { @@ -12245,7 +12240,7 @@ }, "/models/alerts/mark-all-read": { "post": { - "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Scoped to the API key's account.", + "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Scoped to the authenticated account.", "operationId": "mark_all_read_api_models_alerts_mark_all_read_post", "parameters": [ { @@ -12294,7 +12289,7 @@ }, "/models/alerts/{alert_id}/read": { "patch": { - "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The alert must belong to the API key's account.", + "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The alert must belong to the authenticated account.", "operationId": "mark_read_api_models_alerts__alert_id__read_patch", "parameters": [ { @@ -12661,7 +12656,7 @@ ] }, "post": { - "description": "Create a new solution for the API key's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires authentication (API key or bearer token).", + "description": "Create a new solution for the authenticated account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires authentication (API key or bearer token).", "operationId": "create_solution_api_solutions_post", "parameters": [ { @@ -13688,7 +13683,7 @@ }, "/sources/": { "get": { - "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.\n- The optional `account_id` query param is only allowed when it matches the API key's account.", + "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.\n- The optional `account_id` query param is only allowed when it matches the authenticated account.", "operationId": "list_sources_api_sources__get", "parameters": [ { diff --git a/seclai/auth.py b/seclai/auth.py index 80de15c..c8444c4 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -422,20 +422,22 @@ def resolve_credential_chain( RuntimeError: If no credentials are found. """ # 1. Explicit API key - if api_key: + stripped_api_key = api_key.strip() if api_key else "" + if stripped_api_key: return AuthState( mode="api_key", - api_key=api_key.strip(), + api_key=stripped_api_key, api_key_header=api_key_header, account_id=account_id, auto_refresh=False, ) # 2. Static access token - if access_token: + stripped_access_token = access_token.strip() if access_token else "" + if stripped_access_token: return AuthState( mode="bearer_static", - access_token=access_token, + access_token=stripped_access_token, api_key_header=api_key_header, account_id=account_id, auto_refresh=False, diff --git a/seclai/seclai.py b/seclai/seclai.py index c10ef98..b218585 100644 --- a/seclai/seclai.py +++ b/seclai/seclai.py @@ -1367,7 +1367,7 @@ def upload_file_to_source( # Note: openapi-python-client currently struggles with Seclai's spec for this endpoint # due to duplicate schema names, so we send the multipart request directly and parse # into our SDK model types. - http = self._generated_client().get_httpx_client() + http = self._sync_generated_client().get_httpx_client() raw = http.request( "POST", endpoint_path, @@ -4538,7 +4538,7 @@ async def upload_file_to_source( endpoint_path = f"/sources/{source_connection_id}/upload" - http = self._generated_client().get_async_httpx_client() + http = (await self._async_generated_client()).get_async_httpx_client() raw = await http.request( "POST", endpoint_path, @@ -5760,10 +5760,13 @@ async def download_source_export( Returns: A streaming ``httpx.Response``. Must be closed by the caller. """ + headers = await _merge_request_headers_async( + options=self._options, request_headers=None + ) request = self._client.build_request( "GET", f"/sources/{source_id}/exports/{export_id}/download", - headers=_merge_request_headers(options=self._options, request_headers=None), + headers=headers, ) response = await self._client.send(request, stream=True) _raise_for_status(response) From ccc798c7e6c1100769601f79f1ef9ffce6bbecfa Mon Sep 17 00:00:00 2001 From: Kim Burgaard Date: Fri, 27 Mar 2026 15:25:26 -0700 Subject: [PATCH 7/7] Addressed review comments --- openapi/seclai.openapi.json | 177 +++++++++++++++++---------------- seclai/auth.py | 16 +-- tests/test_auth_and_headers.py | 7 +- tests/test_new_methods.py | 21 ++++ 4 files changed, 126 insertions(+), 95 deletions(-) diff --git a/openapi/seclai.openapi.json b/openapi/seclai.openapi.json index 1b40648..a20fe19 100644 --- a/openapi/seclai.openapi.json +++ b/openapi/seclai.openapi.json @@ -7125,7 +7125,7 @@ "paths": { "/agents": { "get": { - "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List agents for the account with pagination.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "list_agents_api_agents_get", "parameters": [ { @@ -7187,7 +7187,7 @@ ] }, "post": { - "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Agent is created in the authenticated account.", + "description": "Create a new agent.\n\nTrigger types:\n- `dynamic_input`: triggered via API with user-provided input\n- `template_input`: triggered via API with a predefined template\n- `schedule`: triggered on a schedule\n- `new_content`: triggered when new content arrives\n\nTemplates: `blank`, `retrieval_example`, `simple_qa`, `summarizer`, `json_extractor`, `content_change_notifier`, `scheduled_report`, `webhook_pipeline`\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Agent is created in the caller's account.", "operationId": "create_agent_api_agents_post", "parameters": [ { @@ -7768,7 +7768,7 @@ }, "/agents/runs/search": { "post": { - "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Searches only within your account's traces.", + "description": "Search agent traces using semantic similarity.\n\nFinds step-run outputs that are most semantically similar to the query.\nResults include the matching text, agent/step metadata, and a similarity score.\n\nAgent traces are automatically indexed when runs complete. The first 7 days of storage are free; extended retention is billed.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Searches only within your account's traces.", "operationId": "search_agent_runs_api_agents_runs_search_post", "parameters": [ { @@ -7815,7 +7815,7 @@ }, "/agents/runs/{run_id}": { "delete": { - "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only cancel runs belonging to your account.", + "description": "Cancel a running agent run.\n\nIf the run is already in a terminal state (`completed` or `failed`), cancellation will be rejected.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only cancel runs belonging to your account.", "operationId": "delete_agent_run_api_agents_runs__run_id__delete", "parameters": [ { @@ -7859,7 +7859,7 @@ ] }, "get": { - "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access runs belonging to your account.", + "description": "Fetch the latest snapshot for an agent run created by `POST /agents/{agent_id}/runs` or `POST /agents/{agent_id}/runs/stream`.\n\nThe response includes `status`, `error_count`, and `output` once the run completes. Use `include_step_outputs=true` to include per-step outputs, timing, durations, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access runs belonging to your account.", "operationId": "get_agent_run_api_agents_runs__run_id__get", "parameters": [ { @@ -7917,7 +7917,7 @@ }, "/agents/{agent_id}": { "delete": { - "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete agents belonging to your account.", + "description": "Soft-delete an agent. The agent will no longer appear in listings or be accessible via the API.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only delete agents belonging to your account.", "operationId": "delete_agent_api_agents__agent_id__delete", "parameters": [ { @@ -7954,7 +7954,7 @@ ] }, "get": { - "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", + "description": "Fetch an agent's metadata (name, description, trigger type, timestamps).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your account.", "operationId": "get_agent_metadata_api_agents__agent_id__get", "parameters": [ { @@ -7998,7 +7998,7 @@ ] }, "put": { - "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", + "description": "Update an agent's name, description, evaluation settings, and model lifecycle settings.\n\nEvaluation settings: `evaluation_mode` ('output_expectation', 'eval_and_retry', 'sample_and_flag'), `default_evaluation_tier` ('fast', 'balanced', 'thorough'), `max_retries`, `retry_on_failure`, `sampling_config`.\n\nModel lifecycle settings: `prompt_model_auto_upgrade_strategy` ('none', 'early_adopter', 'middle_of_road', 'cautious_adopter'), `prompt_model_auto_rollback_enabled`, `prompt_model_auto_rollback_triggers` (list of 'agent_eval_fail', 'governance_flag', 'governance_block', 'agent_run_failed').\n\nAt least one field must be provided.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account.", "operationId": "update_agent_api_agents__agent_id__put", "parameters": [ { @@ -8054,7 +8054,7 @@ }, "/agents/{agent_id}/ai-assistant/conversations": { "get": { - "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be queried.", + "description": "Fetch the AI assistant conversation history for a specific step of an agent.\n\nReturns past conversation turns (user inputs, AI responses, accept/decline status) ordered oldest first. Use `step_type` to filter by step type, and optionally `step_id` to narrow to a specific step instance.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Only agents belonging to your account can be queried.", "operationId": "get_ai_conversation_history_api_agents__agent_id__ai_assistant_conversations_get", "parameters": [ { @@ -8156,7 +8156,7 @@ }, "/agents/{agent_id}/ai-assistant/generate-steps": { "post": { - "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate a full agent step workflow from a natural language description.\n\nProvide a description of what the agent should do, along with optional context (current steps, trigger type). The AI produces a complete set of agent steps.\nUse mode 'generate_full' for new workflows or 'modify_workflow' to refine existing ones.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Only agents belonging to your account can be used.", "operationId": "generate_agent_steps_api_agents__agent_id__ai_assistant_generate_steps_post", "parameters": [ { @@ -8212,7 +8212,7 @@ }, "/agents/{agent_id}/ai-assistant/step-config": { "post": { - "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Only agents belonging to your account can be used.", + "description": "Use the AI assistant to generate or refine a single step's configuration.\n\nProvide the step type, a natural language instruction, and optionally the current configuration. The AI will produce a proposed configuration along with an explanation. The suggestion is stored as a conversation turn that can be accepted or declined separately via the mark endpoint.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Only agents belonging to your account can be used.", "operationId": "generate_step_config_api_agents__agent_id__ai_assistant_step_config_post", "parameters": [ { @@ -8268,7 +8268,7 @@ }, "/agents/{agent_id}/ai-assistant/{conversation_id}": { "patch": { - "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The conversation must belong to one of your agents.", + "description": "Accept or decline a proposed AI assistant configuration for a conversation turn.\n\nThis only updates the tracking status on the conversation record. To actually apply the proposed configuration, use the agent definition update endpoint separately.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. The conversation must belong to one of your agents.", "operationId": "mark_ai_suggestion_api_agents__agent_id__ai_assistant__conversation_id__patch", "parameters": [ { @@ -8337,7 +8337,7 @@ }, "/agents/{agent_id}/definition": { "get": { - "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access agents belonging to your account.", + "description": "Fetch the current agent definition from the main branch.\n\nThe response includes `change_id` which must be provided when updating the definition (optimistic locking).\n\nThe definition contains the agent's step workflow. Available step types:\n- `prompt_call`: Call an LLM with a prompt template\n- `retrieval`: Search a knowledge base\n- `transform`: Reshape data with a Liquid template\n- `gate`: Evaluate conditions, stop or continue child execution\n- `retry`: Re-execute from a target ancestor step (for quality-control loops; pair with a `gate` step for conditional retrying. Fields: `target_step_id` (ancestor step ID), `max_retries` (1\u201310))\n- `evaluate_step`: Score a selected previous step output and emit JSON with `score`, `passed`, and `pass_threshold` (fields: `target_step_id`, `evaluation_prompt`, `pass_threshold`, optional `evaluation_tier`, optional `expectation_config`)\n- `insight`: Progressively read and analyze large input\n- `extract_json` / `extract_html` / `extract_xml`: Extract structured data\n- `send_email`: Send email with step output\n- `webhook_call`: POST data to an external URL\n- `write_aws_s3_object`: Write output to S3\n- `call_agent`: Invoke another agent\n- `write_metadata`: Write a value to content metadata (for filtering/gates; content-triggered agents only. Fields: `metadata_key`, `content`)\n- `write_content_attachment`: Write a file-backed attachment to content (optionally indexed for retrieval; content-triggered agents only. Fields: `attachment_key`, `content`, `content_type`, `indexed`)\n- `load_content_attachment`: Load a previously written attachment (content-triggered agents only. Fields: `attachment_key`)\n- `load_content`: Load the full text body of a source document (typically used with content-triggered agents; can also load by explicit `content_version_id`. Fields: `content_version_id` optional)\n- `display_result`: Show output to the user\n- `join`: Merge parallel branches\n- `combinator`: Combine multiple inputs\n- `text`: Static text literal\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access agents belonging to your account.", "operationId": "get_agent_definition_api_agents__agent_id__definition_get", "parameters": [ { @@ -8381,7 +8381,7 @@ ] }, "put": { - "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update agents belonging to your account.", + "description": "Update the agent's definition on the main branch.\n\nUses **optimistic locking**: provide `expected_change_id` from the last `GET /api/agents/{agent_id}/definition`. Returns `409 Conflict` if the definition was modified since your last read.\n\nThe definition contains the agent's step workflow. Step types include `prompt_call`, `retrieval`, `transform`, `gate`, `retry`, `evaluate_step`, `insight`, `extract_json`, `extract_html`, `extract_xml`, `send_email`, `webhook_call`, `write_aws_s3_object`, `call_agent`, `write_metadata`, `write_content_attachment`, `load_content_attachment`, `load_content`, `display_result`, `join`, `combinator`, and `text`. Non-composite step types (`display_result`, `join`, `retry`, `evaluate_step`) cannot contain child steps.\n\n**Retry steps** re-execute from a target ancestor step for quality-control loops. Configure with `target_step_id` (ancestor step ID) and `max_retries` (1\u201310). Best practice: place a `gate` step before the retry to make retries conditional.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only update agents belonging to your account.", "operationId": "update_agent_definition_api_agents__agent_id__definition_put", "parameters": [ { @@ -8873,7 +8873,7 @@ }, "/agents/{agent_id}/input-uploads/{upload_id}": { "get": { - "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Poll the processing status of a file upload created via `POST /agents/{agent_id}/upload-input`.\n\nPossible `status` values: `processing`, `ready`, `failed`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "api_get_agent_input_upload_status_api_agents__agent_id__input_uploads__upload_id__get", "parameters": [ { @@ -8928,7 +8928,7 @@ }, "/agents/{agent_id}/runs": { "get": { - "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only list runs for agents in your account.", + "description": "List runs for a specific agent (most recent first), with pagination.\n\nTypical use cases:\n- Build a traces UI for an agent.\n- Debug recent executions and inspect terminal statuses.\n\nNotes:\n- This endpoint returns a summary list. Fetch full details with `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only list runs for agents in your account.", "operationId": "list_agent_runs_api_agents__agent_id__runs_get", "parameters": [ { @@ -9017,7 +9017,7 @@ ] }, "post": { - "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Start an agent run.\n\nAn *agent* is an automated workflow that can monitor content from your sources, process it with AI, and trigger actions. This endpoint creates a new run and returns a `run_id` you can poll to retrieve status and output.\n\nWhen to use:\n- Use this endpoint for request/response style integrations where polling is acceptable.\n- Use `POST /agents/{agent_id}/runs/stream` if you need real-time progress via SSE.\n\nKey fields:\n- `input`: text input for agents with a `dynamic_input` trigger.\n- `input_upload_id`: alternatively, reference a file previously uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n- `priority`: set true for latency-sensitive, user-facing work.\n- `metadata`: a JSON object that becomes available to agent steps for string substitution.\n\nAfter starting:\n- Poll `GET /agents/runs/{run_id}` until `status` is `completed` or `failed`.\n- Use `include_step_outputs=true` to include per-step outputs, timing, and credits.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "run_agent_api_agents__agent_id__runs_post", "parameters": [ { @@ -9073,7 +9073,7 @@ }, "/agents/{agent_id}/runs/stream": { "post": { - "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Start a **priority** agent run and stream run events using Server-Sent Events (SSE).\n\nThis is the best option for interactive UIs where you want progress updates as the run executes.\n\nHow it works:\n- The first `init` event contains an `AgentRunResponse` snapshot, including the `run_id`.\n- Subsequent events are forwarded from the run event stream (status changes, step events, etc).\n- The final `done` event contains the terminal snapshot (including `output` and `credits` when available).\n\nInput options (for `dynamic_input` triggers):\n- `input`: text input passed directly.\n- `input_upload_id`: reference a file uploaded via `POST /agents/{agent_id}/upload-input` (mutually exclusive with `input`).\n\nClient guidance:\n- Keep the connection open and handle keepalive comments.\n- On `timeout` or `error`, the payload includes `run_id` so clients can resume by polling `GET /agents/runs/{run_id}`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "run_streaming_agent_api_agents__agent_id__runs_stream_post", "parameters": [ { @@ -9192,7 +9192,7 @@ }, "/agents/{agent_id}/upload-input": { "post": { - "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "Upload a file to use as input for a `dynamic_input` agent run.\n\nSupports the same file types as content source uploads: text, PDF, DOCX, audio, video, images, etc. Text and document files are processed synchronously; audio/video are submitted for asynchronous transcription.\n\n**Size limit:** 200 MB per file.\n\n**Supported extensions:** txt, html, md, csv, xml, json, pdf, msg, docx, doc, pptx, ppt, xlsx, xls, zip, epub, png, jpg, gif, bmp, tiff, webp, mp3, wav, m4a, flac, ogg, mp4, mov, avi.\n\nAfter uploading, poll `GET /agents/{agent_id}/input-uploads/{upload_id}` until `status` is `ready`, then pass `input_upload_id` to `POST /agents/{agent_id}/runs`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "api_upload_agent_input_api_agents__agent_id__upload_input_post", "parameters": [ { @@ -9238,7 +9238,7 @@ }, "/ai-assistant/feedback": { "post": { - "description": "Submit thumbs-up/down feedback on any AI assistant interaction. Negative feedback with a comment is analyzed for concerning issues.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Submit thumbs-up/down feedback on any AI assistant interaction. Negative feedback with a comment is analyzed for concerning issues.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_feedback_api_ai_assistant_feedback_post", "parameters": [ { @@ -9285,7 +9285,7 @@ }, "/ai-assistant/knowledge-base": { "post": { - "description": "Generate a knowledge base creation/modification plan without requiring an existing solution. May also propose prerequisite source creation actions.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Generate a knowledge base creation/modification plan without requiring an existing solution. May also propose prerequisite source creation actions.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_knowledge_base_api_ai_assistant_knowledge_base_post", "parameters": [ { @@ -9332,7 +9332,7 @@ }, "/ai-assistant/memory-bank": { "post": { - "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_memory_bank_api_ai_assistant_memory_bank_post", "parameters": [ { @@ -9379,7 +9379,7 @@ }, "/ai-assistant/memory-bank/last-conversation": { "get": { - "description": "Fetch the most recent memory bank AI assistant conversation turns for the authenticated user. Returns turns in oldest-first order with a total count for pagination via limit/offset query parameters.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Fetch the most recent memory bank AI assistant conversation turns for the authenticated user. Returns turns in oldest-first order with a total count for pagination via limit/offset query parameters.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_memory_bank_history_api_ai_assistant_memory_bank_last_conversation_get", "parameters": [ { @@ -9443,7 +9443,7 @@ }, "/ai-assistant/memory-bank/{conversation_id}": { "patch": { - "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_memory_bank_accept_api_ai_assistant_memory_bank__conversation_id__patch", "parameters": [ { @@ -9504,7 +9504,7 @@ }, "/ai-assistant/solution": { "post": { - "description": "Generate a complete solution plan covering sources, knowledge bases, and agents without requiring an existing solution. Supports SSE streaming when ``Accept: text/event-stream`` is set.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Generate a complete solution plan covering sources, knowledge bases, and agents without requiring an existing solution. Supports SSE streaming when ``Accept: text/event-stream`` is set.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_solution_api_ai_assistant_solution_post", "parameters": [ { @@ -9551,7 +9551,7 @@ }, "/ai-assistant/source": { "post": { - "description": "Generate a content source creation/modification plan without requiring an existing solution. The AI proposes actions for the user to review before any changes are made.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Generate a content source creation/modification plan without requiring an existing solution. The AI proposes actions for the user to review before any changes are made.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_source_api_ai_assistant_source_post", "parameters": [ { @@ -9598,7 +9598,7 @@ }, "/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Accept and execute a previously proposed standalone plan. If the plan contains destructive actions (deletions), ``confirm_deletions`` must be set to true.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Accept and execute a previously proposed standalone plan. If the plan contains destructive actions (deletions), ``confirm_deletions`` must be set to true.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_accept_api_ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -9655,7 +9655,7 @@ }, "/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Decline a previously proposed standalone plan. No resources are modified. The conversation is marked as declined.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Decline a previously proposed standalone plan. No resources are modified. The conversation is marked as declined.\n\nAuth: requires ``X-API-Key`` header or OAuth Bearer token.", "operationId": "api_ai_decline_api_ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -9695,7 +9695,7 @@ }, "/alerts": { "get": { - "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association. Results are scoped to the authenticated account.", + "description": "List alerts for the account with optional filters.\n\nFilters:\n- `status`: triggered, acknowledged, resolved, dismissed\n- `agent_id`: filter by agent\n- `source_connection_id`: filter by source\n- `time_from` / `time_to`: ISO 8601 date range\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Results are scoped to the caller's account.", "operationId": "list_alerts_api_alerts_get", "parameters": [ { @@ -9853,7 +9853,7 @@ }, "/alerts/configs": { "get": { - "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "List alert configurations.\n\nFilters:\n- `agent_id`: list configs for a specific agent\n- `source_connection_id`: list configs for a specific source\n- Neither: list account-level agent alert configs\n- `scope=source`: list account-level source alert configs\n\nCredits alerts (`credits_low_threshold`, `credits_runout_prediction`, `credits_usage_spike`) are account-level alert configs. They are evaluated by the credits alert sweep and default-enabled configs may be auto-created for active accounts at runtime.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "list_alert_configs_api_alerts_configs_get", "parameters": [ { @@ -9944,7 +9944,7 @@ ] }, "post": { - "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Create a new alert configuration.\n\nAgent alert types: run_failed, consecutive_failures, error_rate_spike, run_burst, slow_run, credits_low_threshold, credits_runout_prediction, credits_usage_spike, non_manual_eval_failed, non_manual_eval_flagged, governance_flagged, governance_blocked, model_newer_available, model_deprecated, model_sunset.\nSource alert types: pull_failed, consecutive_pull_failures, pull_error_rate_spike.\n\nDistribution types: owner, owner_admins, selected_members. Organization accounts are normalized to owner_admins.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "create_alert_config_api_alerts_configs_post", "parameters": [ { @@ -9993,7 +9993,7 @@ }, "/alerts/configs/{config_id}": { "delete": { - "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Delete an alert configuration. This permanently removes the config and stops any future alerts of this type from being triggered.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "delete_alert_config_api_alerts_configs__config_id__delete", "parameters": [ { @@ -10030,7 +10030,7 @@ ] }, "get": { - "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Get a specific alert configuration by ID.\n\nReturns all fields including type, enabled state, threshold, cooldown, distribution type, and recipient list.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "get_alert_config_api_alerts_configs__config_id__get", "parameters": [ { @@ -10076,7 +10076,7 @@ ] }, "patch": { - "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Update an alert configuration. Only provided fields are updated.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "update_alert_config_api_alerts_configs__config_id__patch", "parameters": [ { @@ -10134,7 +10134,7 @@ }, "/alerts/organization-preferences/list": { "get": { - "description": "List per-organization alert delivery preferences for the API key's associated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only organizations where the user is an owner or administrator are included.", + "description": "List per-organization alert delivery preferences for the authenticated user.\n\nBy default, only explicit override rows are returned. Set `include_defaults=true` to return the effective subscribed state for every alert type in every organization the user can manage.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.\n- Only organizations where the user is an owner or administrator are included.", "operationId": "list_organization_preferences_api_alerts_organization_preferences_list_get", "parameters": [ { @@ -10202,7 +10202,7 @@ }, "/alerts/organization-preferences/{organization_id}/{alert_type}": { "patch": { - "description": "Update the API key user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.\n- Only owners and administrators can update preferences for an organization.", + "description": "Update the authenticated user's personal delivery preference for one alert type in one organization.\n\nSetting `subscribed=false` stores an explicit opt-out override. Setting `subscribed=true` removes the override and restores the default subscribed behavior.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.\n- Only owners and administrators can update preferences for an organization.", "operationId": "update_organization_preference_api_alerts_organization_preferences__organization_id___alert_type__patch", "parameters": [ { @@ -10268,7 +10268,7 @@ }, "/alerts/{alert_id}": { "get": { - "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Get full alert detail including history, comments, and subscribers.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "get_alert_detail_api_alerts__alert_id__get", "parameters": [ { @@ -10316,7 +10316,7 @@ }, "/alerts/{alert_id}/comments": { "post": { - "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Add a comment to an alert. Comments are visible to all subscribers and are included in the alert detail response.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "add_alert_comment_api_alerts__alert_id__comments_post", "parameters": [ { @@ -10374,7 +10374,7 @@ }, "/alerts/{alert_id}/status": { "post": { - "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Change the status of an alert. Valid statuses: triggered, acknowledged, resolved, dismissed.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "change_alert_status_api_alerts__alert_id__status_post", "parameters": [ { @@ -10432,7 +10432,7 @@ }, "/alerts/{alert_id}/subscribe": { "post": { - "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Subscribe the current user to an alert. Subscribed users receive email notifications when the alert status changes or new comments are added.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "subscribe_to_alert_api_alerts__alert_id__subscribe_post", "parameters": [ { @@ -10480,7 +10480,7 @@ }, "/alerts/{alert_id}/unsubscribe": { "post": { - "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token) with user association.", + "description": "Unsubscribe the current user from an alert. The user will no longer receive email notifications for status changes or new comments on this alert.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "unsubscribe_from_alert_api_alerts__alert_id__unsubscribe_post", "parameters": [ { @@ -10528,7 +10528,7 @@ }, "/contents/{source_connection_content_version}": { "delete": { - "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only delete content belonging to your account.", + "description": "Delete a content item (a `SourceConnectionContentVersion`).\n\nUse this to remove an uploaded or indexed item from your account. Deleting content can affect agents and knowledge base workflows that reference this item.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only delete content belonging to your account.", "operationId": "delete_content_api_contents__source_connection_content_version__delete", "parameters": [ { @@ -10565,7 +10565,7 @@ ] }, "get": { - "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access content belonging to your account.", + "description": "Get detailed information about a specific content item (a `SourceConnectionContentVersion`).\n\nThis is useful when you want to:\n- Inspect the extracted text for debugging or review.\n- Display content details in a UI.\n\nText range:\n- `start` and `end` control the character range returned in `text_content` so clients can page through large documents.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access content belonging to your account.", "operationId": "get_content_detail_api_contents__source_connection_content_version__get", "parameters": [ { @@ -10685,7 +10685,7 @@ }, "/contents/{source_connection_content_version}/embeddings": { "get": { - "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access embeddings for content belonging to your account.", + "description": "List the embeddings (chunk vectors) for a content item, with pagination.\n\nEmbeddings are used for semantic search and retrieval in knowledge base workflows. This endpoint is primarily useful for debugging chunking, indexing, and vector contents.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access embeddings for content belonging to your account.", "operationId": "list_content_embeddings_api_contents__source_connection_content_version__embeddings_get", "parameters": [ { @@ -10751,7 +10751,7 @@ }, "/contents/{source_connection_content_version}/upload": { "post": { - "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only replace content belonging to your account.", + "description": "Upload a new file and replace the content backing an existing `SourceConnectionContentVersion`.\n\nThis behaves like a source file upload, but it targets an existing content version ID. This is useful when you want to correct or update an uploaded document while keeping references stable.\n\n**Maximum file size:** 209715200 bytes.\n\n**Supported MIME types:**\n- `application/epub+zip`\n- `application/json`\n- `application/msword`\n- `application/pdf`\n- `application/vnd.ms-excel`\n- `application/vnd.ms-outlook`\n- `application/vnd.ms-powerpoint`\n- `application/vnd.openxmlformats-officedocument.presentationml.presentation`\n- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n- `application/vnd.openxmlformats-officedocument.wordprocessingml.document`\n- `application/xml`\n- `application/zip`\n- `audio/flac`\n- `audio/mp4`\n- `audio/mpeg`\n- `audio/ogg`\n- `audio/wav`\n- `image/bmp`\n- `image/gif`\n- `image/jpeg`\n- `image/png`\n- `image/tiff`\n- `image/webp`\n- `text/csv`\n- `text/html`\n- `text/markdown`\n- `text/plain`\n- `text/x-markdown`\n- `text/xml`\n- `video/mp4`\n- `video/quicktime`\n- `video/x-msvideo`\n\nNotes:\n- If the uploaded file's content type is `application/octet-stream`, the server attempts to infer the type from the file extension.\n- Use `metadata` to attach an arbitrary JSON object of metadata (for example `metadata={\"category\":\"docs\"}`).\n- `title` is a convenience field and is merged into the metadata as `metadata.title` (it does not override an existing `metadata.title`).\n- For backwards compatibility, you can also pass form fields named `metadata_` (for example `metadata_author=...`). These override keys from `metadata`.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only replace content belonging to your account.", "operationId": "upload_file_to_content_api_contents__source_connection_content_version__upload_post", "parameters": [ { @@ -10807,7 +10807,7 @@ }, "/governance/ai-assistant": { "post": { - "description": "Send a natural-language request to the governance AI assistant to generate a plan of policy changes. Returns a conversation with proposed actions that can be accepted or declined.\n\nAuth: requires authentication (API key or bearer token) with governance access.", + "description": "Send a natural-language request to the governance AI assistant to generate a plan of policy changes. Returns a conversation with proposed actions that can be accepted or declined.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token with governance access.", "operationId": "governance_ai_generate_api_governance_ai_assistant_post", "parameters": [ { @@ -10860,7 +10860,7 @@ }, "/governance/ai-assistant/conversations": { "get": { - "description": "Return recent governance AI assistant conversations for the account, ordered by most recent first.\n\nAuth: requires authentication (API key or bearer token) with governance access.", + "description": "Return recent governance AI assistant conversations for the account, ordered by most recent first.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token with governance access.", "operationId": "list_governance_ai_conversations_api_governance_ai_assistant_conversations_get", "parameters": [ { @@ -10918,7 +10918,7 @@ }, "/governance/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Execute the proposed policy changes from a governance AI assistant conversation. Each action is applied in order and results are returned.\n\nAuth: requires authentication (API key or bearer token) with governance access.", + "description": "Execute the proposed policy changes from a governance AI assistant conversation. Each action is applied in order and results are returned.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token with governance access.", "operationId": "governance_ai_accept_api_governance_ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -10971,7 +10971,7 @@ }, "/governance/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Reject a previously proposed governance AI assistant plan without applying any changes. The conversation is marked as declined and no policy modifications are made.\n\nAuth: requires authentication (API key or bearer token) with governance access.", + "description": "Reject a previously proposed governance AI assistant plan without applying any changes. The conversation is marked as declined and no policy modifications are made.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token with governance access.", "operationId": "governance_ai_decline_api_governance_ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -11017,7 +11017,7 @@ }, "/knowledge_bases": { "get": { - "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List knowledge bases for the account.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "list_knowledge_bases_api_knowledge_bases_get", "parameters": [ { @@ -11190,7 +11190,7 @@ ] }, "get": { - "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access knowledge bases belonging to your account.", + "description": "Fetch a knowledge base by ID.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access knowledge bases belonging to your account.", "operationId": "get_knowledge_base_api_knowledge_bases__knowledge_base_id__get", "parameters": [ { @@ -11234,7 +11234,7 @@ ] }, "put": { - "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only update knowledge bases belonging to your account.", + "description": "Update a knowledge base's configuration. Only provided fields are changed; omitted fields are left unchanged.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only update knowledge bases belonging to your account.", "operationId": "update_knowledge_base_api_knowledge_bases__knowledge_base_id__put", "parameters": [ { @@ -11290,8 +11290,13 @@ }, "/me": { "get": { - "description": "Returns the authenticated user's personal account ID and a list of organizations they belong to. Each organization entry includes the organization's own id, name, and account_id. Useful for CLI tooling that needs to let the user pick an organization context.", + "description": "Returns the authenticated user's personal account ID and a list of organisations they belong to. Each organisation entry includes the organisation's own id, display name, and account_id. Useful for CLI tooling that needs to let the user pick an org context.", "operationId": "get_me_api_me_get", + "parameters": [ + { + "$ref": "#/components/parameters/X-Account-Id" + } + ], "responses": { "200": { "content": { @@ -11312,7 +11317,7 @@ }, "/memory_banks": { "get": { - "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). All resources are scoped to the authenticated account.", + "description": "List memory banks for the account.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. All resources are scoped to the caller's account.", "operationId": "list_memory_banks_api_memory_banks_get", "parameters": [ { @@ -11463,7 +11468,7 @@ }, "/memory_banks/ai-assistant": { "post": { - "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings based on the user's description.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Generate a memory bank configuration suggestion via the AI assistant. The AI proposes name, type, mode, compaction prompt, and retention settings based on the user's description.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token.", "operationId": "memory_bank_ai_generate_api_memory_banks_ai_assistant_post", "parameters": [ { @@ -11513,7 +11518,7 @@ }, "/memory_banks/ai-assistant/last-conversation": { "get": { - "description": "Fetch the most recent memory bank AI assistant conversation turns for the current user. Supports pagination via limit/offset.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Fetch the most recent memory bank AI assistant conversation turns for the current user. Supports pagination via limit/offset.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token.", "operationId": "memory_bank_ai_last_conversation_api_memory_banks_ai_assistant_last_conversation_get", "parameters": [ { @@ -11577,7 +11582,7 @@ }, "/memory_banks/ai-assistant/{conversation_id}": { "patch": { - "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires authentication (API key or bearer token).", + "description": "Update the acceptance status of a memory bank AI assistant conversation turn. Set ``accepted`` to true to accept the proposed configuration, or false to decline it. The accepted status is recorded for audit purposes.\n\nAuth: requires `X-API-Key` header or OAuth Bearer token.", "operationId": "memory_bank_ai_accept_api_memory_banks_ai_assistant__conversation_id__patch", "parameters": [ { @@ -11754,7 +11759,7 @@ ] }, "get": { - "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access memory banks belonging to your account.", + "description": "Fetch a memory bank by ID.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access memory banks belonging to your account.", "operationId": "get_memory_bank_api_memory_banks__memory_bank_id__get", "parameters": [ { @@ -12144,7 +12149,7 @@ }, "/models/alerts": { "get": { - "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Alerts are scoped to the authenticated account.", + "description": "List model lifecycle alerts for the account.\n\nReturns in-app notifications about model deprecations, sunsets, and newer model availability. Supports filtering by agent, unread-only, and pagination.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Alerts are scoped to the caller's account.", "operationId": "list_alerts_api_models_alerts_get", "parameters": [ { @@ -12240,7 +12245,7 @@ }, "/models/alerts/mark-all-read": { "post": { - "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Scoped to the authenticated account.", + "description": "Mark all model lifecycle alerts as read for the account.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Scoped to the caller's account.", "operationId": "mark_all_read_api_models_alerts_mark_all_read_post", "parameters": [ { @@ -12260,7 +12265,7 @@ }, "/models/alerts/unread-count": { "get": { - "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Count is scoped to the authenticated account.", + "description": "Get the count of unread model lifecycle alerts.\n\nUseful for badge indicators in UIs and dashboards.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Count is scoped to the caller's account.", "operationId": "get_alert_unread_count_api_models_alerts_unread_count_get", "parameters": [ { @@ -12289,7 +12294,7 @@ }, "/models/alerts/{alert_id}/read": { "patch": { - "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). The alert must belong to the authenticated account.", + "description": "Mark a single model lifecycle alert as read (dismissed).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. The alert must belong to the caller's account.", "operationId": "mark_read_api_models_alerts__alert_id__read_patch", "parameters": [ { @@ -12329,7 +12334,7 @@ }, "/models/{model_id}/recommendations": { "get": { - "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token).", + "description": "Get replacement/upgrade recommendations for a model.\n\nReturns a designated successor (if any), same-family upgrades, and cross-provider/cross-family alternatives.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token.", "operationId": "get_recommendations_api_models__model_id__recommendations_get", "parameters": [ { @@ -12551,7 +12556,7 @@ }, "/solutions": { "get": { - "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.", + "description": "List solutions for your account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Use solutions to organise related resources and leverage AI assistants for automated setup.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n- Filtering: `search` to filter by solution name (case-insensitive partial match).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Results are scoped to the caller's account.", "operationId": "list_solutions_api_solutions_get", "parameters": [ { @@ -12656,7 +12661,7 @@ ] }, "post": { - "description": "Create a new solution for the authenticated account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body. Requires authentication (API key or bearer token).", + "description": "Create a new solution for the caller's account.\n\nA *solution* groups agents, knowledge bases, and content sources into a cohesive unit. Provide a `name` and optional `description` in the request body.", "operationId": "create_solution_api_solutions_post", "parameters": [ { @@ -12703,7 +12708,7 @@ }, "/solutions/{solution_id}": { "delete": { - "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted. Requires authentication (API key or bearer token).", + "description": "Delete a solution by its ID.\n\nThis permanently removes the solution and all its resource associations (agent links, knowledge base links, source connection links). The underlying resources themselves are not deleted.", "operationId": "delete_solution_api_solutions__solution_id__delete", "parameters": [ { @@ -12741,7 +12746,7 @@ ] }, "get": { - "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information. Requires authentication (API key or bearer token).", + "description": "Retrieve a solution by its ID, including all linked agents, knowledge bases, and source connections.\n\nReturns the full solution detail with nested resource information.", "operationId": "get_solution_api_solutions__solution_id__get", "parameters": [ { @@ -12786,7 +12791,7 @@ ] }, "patch": { - "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged. Requires authentication (API key or bearer token).", + "description": "Update an existing solution's name or description.\n\nPass the fields you wish to change in the request body. Fields not included remain unchanged.", "operationId": "update_solution_api_solutions__solution_id__patch", "parameters": [ { @@ -12843,7 +12848,7 @@ }, "/solutions/{solution_id}/agents": { "delete": { - "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources. Requires authentication (API key or bearer token).", + "description": "Unlink one or more agents from a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs to remove. Agents not currently linked are silently ignored. Returns the updated solution with remaining linked resources.", "operationId": "unlink_agents_api_solutions__solution_id__agents_delete", "parameters": [ { @@ -12898,7 +12903,7 @@ ] }, "post": { - "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more agents to a solution by their IDs.\n\nPass a JSON body with an `ids` array of agent UUIDs. Already-linked agents are silently ignored. Returns the updated solution with all linked resources.", "operationId": "link_agents_api_solutions__solution_id__agents_post", "parameters": [ { @@ -12955,7 +12960,7 @@ }, "/solutions/{solution_id}/ai-assistant/generate": { "post": { - "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", + "description": "Generate a comprehensive solution management plan via the solution AI assistant.\n\nThis is the most powerful assistant \u2014 it can propose changes across sources, knowledge bases, and agents. Describe your goal in natural language and the assistant will create a multi-step plan. Review the proposed actions and use the accept or decline endpoint.\n\nSupports SSE streaming when `Accept: text/event-stream` is set.", "operationId": "ai_assistant_generate_api_solutions__solution_id__ai_assistant_generate_post", "parameters": [ { @@ -13012,7 +13017,7 @@ }, "/solutions/{solution_id}/ai-assistant/knowledge-base": { "post": { - "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint. Requires authentication (API key or bearer token).", + "description": "Generate a knowledge base plan via the KB AI assistant.\n\nDescribe what knowledge bases you need in natural language and the assistant will propose a plan with create, update, or delete actions. The assistant may also propose creating new sources if needed. Review the proposed actions and use the accept or decline endpoint.", "operationId": "ai_assistant_knowledge_base_api_solutions__solution_id__ai_assistant_knowledge_base_post", "parameters": [ { @@ -13069,7 +13074,7 @@ }, "/solutions/{solution_id}/ai-assistant/source": { "post": { - "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan. Requires authentication (API key or bearer token).", + "description": "Generate a content source plan via the source AI assistant.\n\nDescribe what sources you need in natural language and the assistant will propose a plan with create, update, or delete actions. Review the proposed actions and use the accept or decline endpoint to execute or discard the plan.", "operationId": "ai_assistant_source_api_solutions__solution_id__ai_assistant_source_post", "parameters": [ { @@ -13126,7 +13131,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/accept": { "post": { - "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status. Requires authentication (API key or bearer token).", + "description": "Accept and execute a proposed plan generated by one of the AI assistant endpoints.\n\nExecutes all proposed actions in the plan and returns the results of each action. If the plan contains destructive actions (e.g. deletions), you must set `confirm_deletions` to `true` in the request body. Returns a summary of executed actions with success/failure status.", "operationId": "ai_assistant_accept_api_solutions__solution_id__ai_assistant__conversation_id__accept_post", "parameters": [ { @@ -13193,7 +13198,7 @@ }, "/solutions/{solution_id}/ai-assistant/{conversation_id}/decline": { "post": { - "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed. Requires authentication (API key or bearer token).", + "description": "Decline a proposed plan generated by one of the AI assistant endpoints.\n\nMarks the conversation as declined without executing any actions. The conversation history is preserved for reference. You can generate a new plan afterwards if needed.", "operationId": "ai_assistant_decline_api_solutions__solution_id__ai_assistant__conversation_id__decline_post", "parameters": [ { @@ -13243,7 +13248,7 @@ }, "/solutions/{solution_id}/conversations": { "get": { - "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status. Requires authentication (API key or bearer token).", + "description": "List AI assistant conversation history for a solution.\n\nReturns all conversation turns for the given solution, including user inputs, AI responses, proposed actions, and acceptance status.", "operationId": "list_conversations_api_solutions__solution_id__conversations_get", "parameters": [ { @@ -13292,7 +13297,7 @@ ] }, "post": { - "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions. Requires authentication (API key or bearer token).", + "description": "Add a conversation turn to a solution's AI assistant history.\n\nRecords a user input and optional AI response and actions taken. This is typically called internally by AI assistant endpoints, but can also be used to manually log interactions.", "operationId": "add_conversation_turn_api_solutions__solution_id__conversations_post", "parameters": [ { @@ -13349,7 +13354,7 @@ }, "/solutions/{solution_id}/conversations/{conversation_id}": { "patch": { - "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user. Requires authentication (API key or bearer token).", + "description": "Mark a conversation turn as accepted or declined.\n\nUpdates the `accepted` field on an existing conversation turn. Use this after reviewing a proposed plan to record whether it was accepted or declined by the user.", "operationId": "mark_conversation_turn_api_solutions__solution_id__conversations__conversation_id__patch", "parameters": [ { @@ -13409,7 +13414,7 @@ }, "/solutions/{solution_id}/knowledge-bases": { "delete": { - "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", + "description": "Unlink one or more knowledge bases from a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs to remove. Knowledge bases not currently linked are silently ignored. Returns the updated solution.", "operationId": "unlink_knowledge_bases_api_solutions__solution_id__knowledge_bases_delete", "parameters": [ { @@ -13464,7 +13469,7 @@ ] }, "post": { - "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more knowledge bases to a solution by their IDs.\n\nPass a JSON body with an `ids` array of knowledge base UUIDs. Already-linked knowledge bases are silently ignored. Returns the updated solution with all linked resources.", "operationId": "link_knowledge_bases_api_solutions__solution_id__knowledge_bases_post", "parameters": [ { @@ -13521,7 +13526,7 @@ }, "/solutions/{solution_id}/source-connections": { "delete": { - "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution. Requires authentication (API key or bearer token).", + "description": "Unlink one or more source connections from a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs to remove. Sources not currently linked are silently ignored. Returns the updated solution.", "operationId": "unlink_source_connections_api_solutions__solution_id__source_connections_delete", "parameters": [ { @@ -13576,7 +13581,7 @@ ] }, "post": { - "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources. Requires authentication (API key or bearer token).", + "description": "Link one or more source connections to a solution by their IDs.\n\nPass a JSON body with an `ids` array of source connection UUIDs. Already-linked sources are silently ignored. Returns the updated solution with all linked resources.", "operationId": "link_source_connections_api_solutions__solution_id__source_connections_post", "parameters": [ { @@ -13683,7 +13688,7 @@ }, "/sources/": { "get": { - "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). Results are scoped to the authenticated account.\n- The optional `account_id` query param is only allowed when it matches the authenticated account.", + "description": "List content sources for your account.\n\nA *source* is where Seclai pulls or receives content from (for example RSS feeds, websites, file uploads, or custom indexes). Sources are the inputs that power your agents and knowledge base workflows.\n\nParameters:\n- Pagination: `page` and `limit`.\n- Sorting: `sort` (created_at/updated_at/name) and `order` (asc/desc).\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. Results are scoped to the caller's account.\n- The optional `account_id` query param is only allowed when it matches the caller's account.", "operationId": "list_sources_api_sources__get", "parameters": [ { @@ -13739,7 +13744,7 @@ } }, { - "description": "List sources for the given account. Defaults to the api key's account.", + "description": "List sources for the given account. Defaults to the caller's account.", "in": "query", "name": "account_id", "required": false, @@ -13752,7 +13757,7 @@ "type": "null" } ], - "description": "List sources for the given account. Defaults to the api key's account.", + "description": "List sources for the given account. Defaults to the caller's account.", "title": "Account Id" } }, @@ -13828,7 +13833,7 @@ ] }, "get": { - "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires authentication (API key or bearer token). You can only access sources belonging to your account.", + "description": "Fetch a content source by ID.\n\nAuth & scoping:\n- Requires `X-API-Key` header or OAuth Bearer token. You can only access sources belonging to your account.", "operationId": "get_source_api_sources__source_connection_id__get", "parameters": [ { diff --git a/seclai/auth.py b/seclai/auth.py index c8444c4..34b0078 100644 --- a/seclai/auth.py +++ b/seclai/auth.py @@ -14,6 +14,7 @@ import configparser import hashlib import json +import logging import os import tempfile import threading @@ -30,7 +31,9 @@ _CONFIG_FILE = "config" _EXPIRY_BUFFER_SECONDS = 30 -_SSO_EXPIRED_MSG = "SSO token expired. Run `seclai auth login` to re-authenticate." +_SSO_EXPIRED_MSG = ( + "SSO token is missing or has expired. Run `seclai auth login` to authenticate." +) #: Default SSO domain (production Cognito). Override with ``SECLAI_SSO_DOMAIN`` or config file. DEFAULT_SSO_DOMAIN = "auth.seclai.com" @@ -196,6 +199,11 @@ def load_sso_profile(config_dir: Path, profile_name: str) -> SsoProfile: section_name = f"profile {profile_name}" if cp.has_section(section_name): merged = {**default_section, **dict(cp.items(section_name))} + else: + logging.getLogger(__name__).warning( + "SSO profile '%s' not found in config; using defaults", + profile_name, + ) # Environment variables override config file values sso_domain = ( @@ -415,11 +423,7 @@ def resolve_credential_chain( 2. Explicit ``access_token`` (static string) 3. Explicit ``access_token_provider`` (callback) 4. ``SECLAI_API_KEY`` environment variable - 5. SSO profile from config file + cached tokens - 6. Error - - Raises: - RuntimeError: If no credentials are found. + 5. SSO profile from config file + cached tokens (always available via built-in defaults) """ # 1. Explicit API key stripped_api_key = api_key.strip() if api_key else "" diff --git a/tests/test_auth_and_headers.py b/tests/test_auth_and_headers.py index 5391263..e5efb21 100644 --- a/tests/test_auth_and_headers.py +++ b/tests/test_auth_and_headers.py @@ -16,9 +16,11 @@ def test_api_key_param_takes_precedence(monkeypatch: pytest.MonkeyPatch) -> None assert client.api_key == "param-key" -def test_missing_api_key_falls_back_to_sso(monkeypatch: pytest.MonkeyPatch) -> None: +def test_missing_api_key_falls_back_to_sso( + monkeypatch: pytest.MonkeyPatch, tmp_path: pytest.TempPathFactory +) -> None: monkeypatch.delenv("SECLAI_API_KEY", raising=False) - monkeypatch.setenv("SECLAI_CONFIG_DIR", "/nonexistent-seclai-dir") + monkeypatch.setenv("SECLAI_CONFIG_DIR", str(tmp_path)) client = Seclai() # With built-in SSO defaults, client succeeds and falls back to SSO mode assert client._options.auth_state.mode == "sso" @@ -28,7 +30,6 @@ def test_both_api_key_and_access_token_raises(monkeypatch: pytest.MonkeyPatch) - monkeypatch.delenv("SECLAI_API_KEY", raising=False) with pytest.raises(SeclaiConfigurationError, match="not both"): _ = Seclai(api_key="k", access_token="t") - _ = Seclai(api_key="k", access_token="t") def test_header_injected_sync(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/test_new_methods.py b/tests/test_new_methods.py index f708c26..bb98490 100644 --- a/tests/test_new_methods.py +++ b/tests/test_new_methods.py @@ -1599,6 +1599,27 @@ async def handler(req: httpx.Request) -> httpx.Response: assert events[0][0] == "init" assert events[1][0] == "done" + @pytest.mark.asyncio + async def test_async_download_source_export(self) -> None: + seen: dict[str, Any] = {} + + async def handler(req: httpx.Request) -> httpx.Response: + seen["method"] = req.method + seen["path"] = req.url.path + seen["has_api_key"] = "x-api-key" in req.headers + return httpx.Response(status_code=200, content=b"csv-data") + + client = _async_client(handler) + resp = await client.download_source_export("s1", "e1") + assert seen == { + "method": "GET", + "path": "/sources/s1/exports/e1/download", + "has_api_key": True, + } + await resp.aread() + assert resp.content == b"csv-data" + await resp.aclose() + # --------------------------------------------------------------------------- # Top-level AI Assistant