# Config Manager Source: https://docs.sinas.co/admin/config-manager YAML configuration import and export The config manager supports GitOps-style declarative configuration. Define all your resources in a YAML file and apply it idempotently. **YAML structure:** ```yaml theme={null} apiVersion: sinas.co/v1 kind: SinasConfig metadata: name: my-config description: Production configuration spec: roles: # Roles and permissions users: # User provisioning llmProviders: # LLM provider connections databaseConnections: # External database credentials dependencies: # Python packages (pip) secrets: # Encrypted credentials (values omitted on export) connectors: # HTTP connectors with typed operations skills: # Instruction documents components: # UI components functions: # Python functions queries: # Saved SQL templates collections: # File storage collections templates: # Jinja2 templates stores: # State store definitions manifests: # Application manifests agents: # AI agent configurations webhooks: # HTTP triggers for functions schedules: # Cron-based triggers databaseTriggers: # CDC polling triggers ``` All sections are optional — include only what you need. **Key behaviors:** * **Idempotent** — Applying the same config twice does nothing. Unchanged resources are skipped (SHA256 checksum comparison). * **Config-managed tracking** — Resources created via config are tagged with `managed_by: "config"`. The system won't overwrite resources that were created manually (it warns instead). * **Environment variable interpolation** — Use `${VAR_NAME}` in values (e.g., `apiKey: "${OPENAI_API_KEY}"`). * **Reference validation** — Cross-references (e.g., an agent referencing a function) are validated before applying. * **Dry run** — Set `dryRun: true` to preview changes without applying. **Endpoints (admin only):** ``` POST /api/v1/config/validate # Validate YAML syntax and references POST /api/v1/config/apply # Apply config (supports dryRun and force flags) GET /api/v1/config/export # Export current configuration as YAML ``` **Auto-apply on startup:** ```bash theme={null} # In .env CONFIG_FILE=config/production.yaml AUTO_APPLY_CONFIG=true ``` **Apply via API:** ```bash theme={null} curl -X POST https://yourdomain.com/api/v1/config/apply \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"config\": \"$(cat config.yaml)\", \"dryRun\": false}" ``` # Icons Source: https://docs.sinas.co/admin/icons Icon references for agents and resources Agents and functions support configurable icons via the `icon` field. Two formats are supported: | Format | Example | Description | | ------------------------------- | ---------------------------------- | --------------------------------- | | `url:` | `url:https://example.com/icon.png` | Direct URL to an image | | `collection://` | `collection:assets/icons/bot.png` | File stored in a Sinas collection | Collection-based icons generate signed JWT URLs for private files and direct URLs for public collection files. Icons are resolved at read time via the icon resolver service. # Manifests Source: https://docs.sinas.co/admin/manifests Application declarations and dependencies Manifests are application declarations that describe what resources, permissions, and stores an application built on Sinas requires. They enable a single API call to validate whether a user has everything needed to run the application. **Key properties:** | Property | Description | | ---------------------- | ------------------------------------------------------------------------------- | | `namespace` / `name` | Unique identifier | | `description` | What this manifest declares | | `required_resources` | Resource references: `[{"type": "agent", "namespace": "...", "name": "..."}]` | | `required_permissions` | Permissions the application needs | | `optional_permissions` | Optional permissions for extended features | | `exposed_namespaces` | Namespace filter per resource type (e.g., `{"agents": ["support"]}`) | | `store_dependencies` | Stores the application expects: `[{"store": "ns/name", "key": "optional_key"}]` | **Endpoints:** ``` POST /api/v1/manifests # Create manifest GET /api/v1/manifests # List manifests GET /api/v1/manifests/{namespace}/{name} # Get manifest PUT /api/v1/manifests/{namespace}/{name} # Update DELETE /api/v1/manifests/{namespace}/{name} # Delete ``` **Runtime status validation:** ``` GET /api/runtime/manifests/{namespace}/{name}/status # Validate dependencies ``` Returns `ready: true/false` with details on satisfied/missing resources, granted/missing permissions, and existing/missing store dependencies. # Packages Source: https://docs.sinas.co/admin/packages Install, create, and manage integration packages Integration packages bundle agents, functions, skills, components, templates, and other resources into a shareable YAML file that can be installed with one click. **How packages work:** 1. **Create**: Select resources from your Sinas instance → export as `SinasPackage` YAML 2. **Share**: Distribute the YAML file (GitHub, email, package registry) 3. **Install**: Paste/upload the YAML → preview changes → confirm install 4. **Uninstall**: Removes all resources created by the package in one operation **Package YAML format:** ```yaml theme={null} apiVersion: sinas.co/v1 kind: SinasPackage package: name: crm-integration version: "1.0.0" description: "CRM support agents and functions" author: "team@company.com" url: "https://github.com/company/sinas-crm" spec: variables: [...] # Install-time configuration (optional) agents: [...] functions: [...] skills: [...] connectors: [...] components: [...] templates: [...] queries: [...] collections: [...] stores: [...] webhooks: [...] schedules: [...] manifests: [...] databaseTriggers: [...] dependencies: [...] ``` **Key behaviors:** * Resources created by packages are tagged with `managed_by: "pkg:"` * **Detach-on-edit**: Editing a package-managed resource clears `managed_by` — the resource survives uninstall * **Uninstall**: Deletes all resources where `managed_by = "pkg:"` + the package record * **Upgrade**: Re-installing an existing package updates its resources in place (idempotent apply) * **Excluded types**: Packages cannot include roles, users, LLM providers, or database connections (these are environment-specific) * **Dependencies**: Packages can declare Python dependencies — these are recorded in the database and installed in containers on worker restart ## Install-time Variables Packages can declare typed variables that are prompted during installation. Variables are substituted into the YAML before resources are persisted — the resulting resources contain literal values, no runtime template evaluation. **Declaring variables:** ```yaml theme={null} spec: variables: - name: APP_URL type: text description: Base URL where the app is reachable from containers example: http://host.docker.internal:8080 required: true - name: PRIMARY_LLM type: resource_ref resource: llm_providers description: LLM provider for capable agents required: true - name: CHEAP_LLM type: resource_ref resource: llm_providers description: LLM provider for filtering/extraction agents required: true - name: ENABLE_LOGGING type: boolean default: false description: Enable verbose logging in functions - name: API_KEY type: secret description: External service API key required: true ``` **Variable types:** | Type | Description | Validation | | -------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- | | `text` | Free text input | Optional `pattern` (regex) | | `boolean` | True/false toggle | — | | `enum` | Fixed choices | `choices: [a, b, c]` | | `resource_ref` | Pointer to existing resource | `resource: llm_providers\|database_connections\|collections\|secrets\|roles` — validated at install | | `secret` | Masked input, stored encrypted | Creates/upserts a Secret. Not re-prompted on upgrade if already exists. | **Substitution syntax:** Use `${{ vars.NAME }}` anywhere in the package spec. Resolved by simple string replacement before parsing — not Jinja2, so no conflicts with system prompt templates. ```yaml theme={null} connectors: - baseUrl: "${{ vars.APP_URL }}" agents: - llmProviderName: "${{ vars.PRIMARY_LLM }}" ``` **API:** * `POST /api/v1/packages/preview` — response includes `variables` (declarations) and `requires_input` (bool). Pass `variables: {NAME: value}` in the request to preview with substitution. * `POST /api/v1/packages/install` — pass `variables: {NAME: value}` in the request. Required variables must be present. **Stored values:** Variable values from the last install are stored in `Package.values`. On upgrade, the console pre-fills the form with previous values. Secret values are stored as `"***"` — the actual credential lives in the Secrets table. **Endpoints:** ``` POST /api/v1/packages/install # Install package from YAML POST /api/v1/packages/preview # Preview install (dry run) POST /api/v1/packages/create # Create package YAML from selected resources GET /api/v1/packages # List installed packages GET /api/v1/packages/{name} # Get package details DELETE /api/v1/packages/{name} # Uninstall package GET /api/v1/packages/{name}/export # Export original YAML ``` **Creating a package from existing resources:** ```bash theme={null} curl -X POST https://yourdomain.com/api/v1/packages/create \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "my-package", "version": "1.0.0", "description": "My integration package", "resources": [ {"type": "agent", "namespace": "support", "name": "ticket-bot"}, {"type": "function", "namespace": "support", "name": "lookup-customer"}, {"type": "template", "namespace": "support", "name": "ticket-reply"}, {"type": "schedule", "namespace": "default", "name": "daily-digest"} ] }' ``` Supported resource types: `agent`, `function`, `skill`, `connector`, `manifest`, `component`, `query`, `collection`, `store`, `template`, `webhook`, `schedule`, `database_trigger`. # Permissions Source: https://docs.sinas.co/admin/permissions Permission reference and management See [Role-Based Access Control (RBAC)](#role-based-access-control-rbac) for the full permission system documentation, including format, matching rules, custom permissions, and the check-permissions endpoint. **Quick reference of action verbs:** | Verb | Usage | | ---------- | ------------------------ | | `create` | Create a resource | | `read` | View/list a resource | | `update` | Modify a resource | | `delete` | Remove a resource | | `execute` | Run a function or query | | `chat` | Chat with an agent | | `render` | Render a template | | `send` | Send a rendered template | | `upload` | Upload a file | | `download` | Download a file | | `install` | Approve a package | # System Source: https://docs.sinas.co/admin/system Workers, sandbox containers, and dependencies Sinas has a dual-execution model for functions, plus dedicated queue workers for async job processing. #### Sandbox Containers The sandbox container pool is a set of **pre-warmed, generic Docker containers** for executing untrusted user code. This is the default execution mode for all functions (`shared_pool=false`). **How it works:** * On startup, the pool creates `sandbox_min_size` containers (default: 4) ready to accept work. * When a function executes, a container is acquired from the idle pool, used, and returned. * Containers are recycled (destroyed and replaced) after `sandbox_max_executions` uses (default: 100) to prevent state leakage between executions. * If a container errors during execution, it's marked as tainted and destroyed immediately. * A background replenishment loop monitors the idle count and creates new containers whenever it drops below `sandbox_min_idle` (default: 2), up to `sandbox_max_size` (default: 20). * Health checks run every 60 seconds to detect and replace dead containers. **Isolation guarantees:** Each container runs with strict resource limits and security hardening: | Constraint | Default | | -------------------- | ------------------------------------------------- | | Memory | 512 MB (`MAX_FUNCTION_MEMORY`) | | CPU | 1.0 cores (`MAX_FUNCTION_CPU`) | | Disk | 1 GB (`MAX_FUNCTION_STORAGE`) | | Execution time | 300 seconds (`FUNCTION_TIMEOUT`) | | Temp storage | 100 MB tmpfs at `/tmp` | | Capabilities | All dropped, only `CHOWN`/`SETUID`/`SETGID` added | | Privilege escalation | Disabled (`no-new-privileges`) | **Runtime scaling:** The pool can be scaled up or down at runtime without restarting the application: ```bash theme={null} # Check current pool state GET /api/v1/containers/stats # → {"idle": 2, "in_use": 3, "total": 5, "max_size": 20, ...} # Scale up for high load POST /api/v1/containers/scale {"target": 15} # → {"action": "scale_up", "previous": 5, "current": 15, "added": 10} # Scale back down (only removes idle containers — never interrupts running executions) POST /api/v1/containers/scale {"target": 4} # → {"action": "scale_down", "previous": 15, "current": 4, "removed": 11} ``` **Package installation:** When new packages are approved, existing containers don't have them yet. Use the reload endpoint to install approved packages into all idle containers: ```bash theme={null} POST /api/v1/containers/reload # → {"status": "completed", "idle_containers": 4, "success": 4, "failed": 0} ``` Containers that are currently executing are unaffected. New containers created by the replenishment loop automatically include all approved packages. #### Shared Containers Functions marked `shared_pool=true` run in **persistent shared containers** instead of sandbox containers. This is an admin-only option for trusted code that benefits from longer-lived containers. **Differences from sandbox:** | | Sandbox Containers | Shared Containers | | ------------------ | ----------------------------------- | --------------------------------------- | | **Trust level** | Untrusted user code | Trusted admin code only | | **Isolation** | Per-request (recycled after N uses) | Shared (persistent containers) | | **Lifecycle** | Created/destroyed automatically | Persist until explicitly scaled down | | **Scaling** | Auto-replenishment + manual | Manual via API only | | **Load balancing** | First available idle container | Round-robin across workers | | **Best for** | User-submitted functions | Admin functions, long-startup libraries | **When to use `shared_pool=true` (shared containers):** * Functions created and maintained by admins (not user-submitted code) * Functions that import heavy libraries (pandas, scikit-learn) where container startup cost matters * Performance-critical functions that benefit from warm containers **Management:** ```bash theme={null} # List workers GET /api/v1/workers # Check count GET /api/v1/workers/count # → {"count": 4} # Scale workers POST /api/v1/workers/scale {"target_count": 6} # → {"action": "scale_up", "previous_count": 4, "current_count": 6, "added": 2} # Reload packages in all workers POST /api/v1/workers/reload # → {"status": "completed", "total_workers": 6, "success": 6, "failed": 0} ``` #### Queue Workers All function and agent executions are processed asynchronously through Redis-based queues (arq). Two separate worker types handle different workloads: | Worker | Docker service | Queue | Concurrency | Retries | | -------------------- | -------------- | ----------------------- | -------------- | --------------------- | | **Function workers** | `queue-worker` | `sinas:queue:functions` | 10 jobs/worker | Up to 3 | | **Agent workers** | `queue-agent` | `sinas:queue:agents` | 5 jobs/worker | None (not idempotent) | **Function workers** dequeue function execution jobs, route them to either sandbox or shared containers, track results in Redis, and handle retries. Failed jobs that exhaust retries are moved to a **dead letter queue** (DLQ) for inspection and manual retry. **Agent workers** handle chat message processing — they call the LLM, execute tool calls, and stream responses back via Redis Streams. Agent jobs don't retry because LLM calls with tool execution have side effects. **Scaling** is controlled via Docker Compose replicas: ```yaml theme={null} # docker-compose.yml queue-worker: command: python -m arq app.queue.worker.WorkerSettings deploy: replicas: ${QUEUE_WORKER_REPLICAS:-2} queue-agent: command: python -m arq app.queue.worker.AgentWorkerSettings deploy: replicas: ${QUEUE_AGENT_REPLICAS:-2} ``` Each worker sends a **heartbeat** to Redis every 10 seconds (TTL: 30 seconds). If a worker dies, its heartbeat key auto-expires, making it easy to detect dead workers. **Job status tracking:** ```bash theme={null} # Check job status GET /jobs/{job_id} # → {"status": "completed", "execution_id": "...", ...} # Get job result GET /jobs/{job_id}/result # → {function output} ``` Jobs go through states: `queued` → `running` → `completed` or `failed`. Stale or orphaned jobs can be cancelled via the admin API: ```bash theme={null} # Cancel a running or queued job POST /api/v1/queue/jobs/{job_id}/cancel # → {"status": "cancelled", "job_id": "..."} ``` Cancellation updates the Redis status to `cancelled` and marks the DB execution record as `CANCELLED`. It also publishes to the done channel so any waiters unblock. This is a soft cancel — it does not kill running containers. Results are stored in Redis with a 24-hour TTL. #### System Endpoints Admin endpoints for monitoring and managing the Sinas deployment. All require `sinas.system.read:all` or `sinas.system.update:all` permissions. **Health check:** ```bash theme={null} GET /api/v1/system/health ``` Returns a comprehensive health report: * **`services`** — All Docker Compose containers with status, health, uptime, CPU %, and memory usage. Infrastructure containers (redis, postgres, pgbouncer) are listed first, followed by application containers sorted alphabetically. Sandbox and shared worker containers are included. * **`host`** — Host-level CPU, memory, and disk usage (read from `/proc` on Linux). * **`warnings`** — Auto-generated alerts at three levels: * `critical` — No queue workers running, or infrastructure services (redis, postgres, pgbouncer) down * `warning` — Non-infrastructure services down, unhealthy containers, DLQ items, queue backlog >50, disk/memory >90% * `info` — Disk/memory >75% **Container restart:** ```bash theme={null} POST /api/v1/system/containers/{container_name}/restart # → {"status": "restarted", "container": "sinas-backend"} ``` Restarts any Docker container by name (15-second timeout). Returns 404 if the container doesn't exist. **Flush stuck jobs:** ```bash theme={null} POST /api/v1/system/flush-stuck-jobs ``` Cancels all jobs that have been stuck in `running` state for over 2 hours. Useful for recovering from worker crashes or orphaned jobs. #### Dependencies (Python Packages) Functions can only use Python packages that have been approved by an admin. This prevents untrusted code from installing arbitrary dependencies. **Approval flow:** 1. Admin approves a dependency (optionally pinning a version) 2. Package becomes available in newly created containers and workers 3. Use `POST /containers/reload` or `POST /workers/reload` to install into existing containers ``` POST /api/v1/dependencies # Approve dependency (admin) GET /api/v1/dependencies # List approved dependencies DELETE /api/v1/dependencies/{id} # Remove approval (admin) ``` Optionally restrict which packages can be approved with a whitelist: ```bash theme={null} # In .env — only these packages can be approved ALLOWED_PACKAGES=requests,pandas,numpy,redis,boto3 ``` #### Configuration Reference **Container pool:** | Variable | Default | Description | | ---------------------- | ------- | ------------------------------------------ | | `POOL_MIN_SIZE` | 4 | Containers created on startup | | `POOL_MAX_SIZE` | 20 | Maximum total containers | | `POOL_MIN_IDLE` | 2 | Replenish when idle count drops below this | | `POOL_MAX_EXECUTIONS` | 100 | Recycle container after this many uses | | `POOL_ACQUIRE_TIMEOUT` | 30 | Seconds to wait for an available container | **Function execution:** | Variable | Default | Description | | --------------------------------- | ------- | -------------------------------- | | `FUNCTION_TIMEOUT` | 300 | Max execution time in seconds | | `MAX_FUNCTION_MEMORY` | 512 | Memory limit per container (MB) | | `MAX_FUNCTION_CPU` | 1.0 | CPU cores per container | | `MAX_FUNCTION_STORAGE` | 1g | Disk storage limit | | `FUNCTION_CONTAINER_IDLE_TIMEOUT` | 3600 | Idle container cleanup (seconds) | **Workers and queues:** | Variable | Default | Description | | ---------------------------- | ------- | ----------------------------------- | | `DEFAULT_WORKER_COUNT` | 4 | Shared workers created on startup | | `QUEUE_WORKER_REPLICAS` | 2 | Function queue worker processes | | `QUEUE_AGENT_REPLICAS` | 2 | Agent queue worker processes | | `QUEUE_FUNCTION_CONCURRENCY` | 10 | Concurrent jobs per function worker | | `QUEUE_AGENT_CONCURRENCY` | 5 | Concurrent jobs per agent worker | | `QUEUE_MAX_RETRIES` | 3 | Retry attempts before DLQ | | `QUEUE_RETRY_DELAY` | 10 | Seconds between retries | **Packages:** | Variable | Default | Description | | ---------------------------- | --------- | ----------------------------------------------- | | `ALLOW_PACKAGE_INSTALLATION` | true | Enable pip in containers | | `ALLOWED_PACKAGES` | *(empty)* | Comma-separated whitelist (empty = all allowed) | # Users & Roles Source: https://docs.sinas.co/admin/users-roles User management, external identities, custom fields, and role assignments **Users** are identified by email, internal id, or a linked external identity. They can be created via the management API, declarative config, or the console. Authentication is via OTP (email one-time password) or password, depending on `AUTH_MODE`. **Roles** group permissions together. Users can belong to multiple roles. All permissions across all of a user's roles are combined. **User endpoints:** ``` GET /api/v1/users # List users (admin) POST /api/v1/users # Create user (admin) GET /api/v1/users/by-identity # Look up user by external identity (admin) GET /api/v1/users/{id} # Get user PATCH /api/v1/users/{id} # Update user (custom_fields) DELETE /api/v1/users/{id} # Delete user (admin) POST /api/v1/users/{id}/identities # Link external identity (admin) DELETE /api/v1/users/{id}/identities # Unlink external identity (admin) ``` ## External identities If your application or organization already has its own auth system or identity provider, link those identities to Sinas users. An identity is a `(provider, subject)` pair — e.g. provider `acme-app` with your application's stable user id as subject. A user can hold multiple identities. ```json theme={null} POST /api/v1/users/{id}/identities { "provider": "acme-app", "subject": "usr_8f3a2", "metadata": {"plan": "enterprise"} } ``` Look up users by identity with `GET /api/v1/users/by-identity?provider=acme-app&subject=usr_8f3a2`. Use stable identifiers as subjects (an internal user id, an OIDC `sub` claim) — not email addresses, which can change at the provider. ## Custom fields Attach org-specific profile data to users with the free-form `custom_fields` object: ```json theme={null} PATCH /api/v1/users/{id} { "custom_fields": {"department": "marketing", "locale": "nl-NL"} } ``` `custom_fields` is owned by Sinas admins and is never overwritten by identity sync (identity `metadata` is, on every sync). Custom fields are also returned by `GET /auth/me`, so an external service holding a user's Sinas token can read them without admin permissions — treat `/auth/me` as Sinas's userinfo endpoint. Don't store values in custom fields that the user themselves shouldn't see. ## Declarative config Users, identities, and custom fields can be provisioned via YAML: ```yaml theme={null} users: - email: jane@acme.com roles: [Users] customFields: department: marketing locale: nl-NL identities: - provider: acme-app subject: usr_8f3a2 ``` For config-managed users the `identities` list is the full desired set: identities removed from the config are unlinked on apply. **Role endpoints:** See [Managing Roles](#managing-roles). # Agents Source: https://docs.sinas.co/build-resources/agents Configurable AI assistants with tools, prompts, and hooks Agents are configurable AI assistants. Each agent has an LLM provider, a system prompt, and a set of enabled tools. **Key properties:** | Property | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `namespace` / `name` | Unique identifier (e.g., `support/ticket-agent`) | | `llm_provider_id` | LLM provider to use (null = system default) | | `model` | Model override (null = provider's default) | | `system_prompt` | Jinja2 template for the system message | | `temperature` | Sampling temperature (default: 0.7) | | `max_tokens` | Max token limit for responses | | `input_schema` | JSON Schema for validating chat input variables | | `output_schema` | JSON Schema for validating agent output | | `initial_messages` | Few-shot example messages | | `enabled_functions` | Functions available as tools. Supports wildcards: `namespace/*` or `*/*`. | | `function_parameters` | Default parameter values per function (supports Jinja2) | | `enabled_agents` | Other agents callable as sub-agents. Supports wildcards. | | `enabled_skills` | Skills available to the agent. Supports wildcards. | | `enabled_queries` | Database queries available as tools. Supports wildcards. | | `query_parameters` | Default query parameter values | | `enabled_collections` | File collections the agent can access. Plain string (readonly) or `{"collection": "namespace/name", "access": "readonly\|readwrite"}`. Supports wildcards. Readwrite enables write/edit/delete file tools. | | `enabled_stores` | Stores the agent can access. List of `{"store": "namespace/name", "access": "readonly"}` or `{"store": "namespace/name", "access": "readwrite"}` | | `enabled_connectors` | Connectors available as tools. List of `{"connector": "namespace/name", "operations": [...], "parameters": {"op_name": {"param": "value"}}}`. Parameters support Jinja2 templates and are locked (hidden from LLM). | | `hooks` | Message lifecycle hooks. `{"on_user_message": [...], "on_assistant_message": [...]}` | | `system_tools` | Platform capabilities. List of strings or config objects. See [System Tools](#system-tools). | | `icon` | Icon reference (see [Icons](#icons)) | **Wildcard patterns** for `enabled_*` fields: | Pattern | Matches | | ---------------- | -------------------------------------- | | `namespace/name` | Exact resource | | `namespace/*` | All active resources in that namespace | | `*/*` | All active resources | ```yaml theme={null} enabledFunctions: - sales/* # all functions in the sales namespace - shared/utils # one specific function enabledQueries: - "*/*" # all queries (quote wildcards in YAML) enabledCollections: - collection: "docs/*" access: readonly ``` Wildcards expand at tool discovery time. The resolved tools are validated against user permissions at execution time. **Message hooks:** Functions that run before/after agent messages. Each hook has: * `function`: reference to a function (`namespace/name`) * `async`: if true, fire-and-forget (no impact on latency) * `on_timeout`: `block` (stop pipeline) or `passthrough` (continue) — sync hooks only Hook functions receive `{"message": {"role": "...", "content": "..."}, "chat_id": "...", "agent": {"namespace": "...", "name": "..."}, "user_id": "..."}` and can return: * `{"content": "..."}` to mutate the message * `{"block": true, "reply": "..."}` to stop the pipeline * `null` to pass through unchanged ```yaml theme={null} agents: - name: my-agent hooks: onUserMessage: - function: default/guardrail async: false onTimeout: block onAssistantMessage: - function: default/pii-filter async: false onTimeout: passthrough ``` **Management endpoints:** ``` POST /api/v1/agents # Create agent GET /api/v1/agents # List agents GET /api/v1/agents/{namespace}/{name} # Get agent PUT /api/v1/agents/{namespace}/{name} # Update agent DELETE /api/v1/agents/{namespace}/{name} # Delete agent ``` **Runtime endpoints (chats):** ``` POST /agents/{namespace}/{name}/invoke # Invoke (sync request/response) POST /agents/{namespace}/{name}/chats # Create chat GET /chats # List user's chats GET /chats/{id} # Get chat with messages PUT /chats/{id} # Update chat DELETE /chats/{id} # Delete chat POST /chats/{id}/messages # Send message POST /chats/{id}/messages/stream # Send message (SSE streaming) GET /chats/{id}/stream/{channel_id} # Reconnect to active stream POST /chats/{id}/approve-tool/{tool_call_id} # Approve/reject a tool call ``` **Invoke endpoint:** A synchronous request/response alternative to the two-step chat flow. Intended for integrations (Slack, Telegram, webhooks) that need a simple call-and-response. ```bash theme={null} # Simple invoke curl -X POST https://yourdomain.com/agents/support/helper/invoke \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "What is the status of order #123?"}' # → {"reply": "Your order is on its way...", "chat_id": "c_abc123"} # With session key (conversation continuity) curl -X POST https://yourdomain.com/agents/support/helper/invoke \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"message": "Follow up on that", "session_key": "slack:U09ABC123"}' # → Same chat_id as previous call with this session_key ``` * `session_key`: Maps an external identifier (Slack channel, Telegram chat, WhatsApp number) to a persistent Sinas chat. One chat per `(agent_id, session_key)` pair. * `reset: true`: Archives the existing session and starts a new conversation. * `input`: Agent input variables, only used when creating a new chat. * Streams internally, returns assembled reply as a single JSON payload. **How chat works:** 1. Create a chat linked to an agent (optionally with input variables validated against `input_schema`) 2. Send a message — Sinas builds the conversation context with the system prompt, preloaded skills, message history, and available tools 3. The LLM generates a response, possibly calling tools 4. If tools are called, Sinas executes them (in parallel where possible) and sends results back to the LLM for a follow-up response 5. The final response is streamed to the client via Server-Sent Events **Ephemeral chats** can be created with a TTL by passing `expires_in` (seconds) when creating the chat. Expired chats are automatically hard-deleted (with all messages) by a scheduled cleanup job: ```bash theme={null} # Create an ephemeral chat that expires in 1 hour curl -X POST https://yourdomain.com/agents/default/default/chats \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"expires_in": 3600}' ``` **Chat archiving** — Chats can be archived via `PUT /chats/{id}` with `{"archived": true}`. Archived chats are hidden from the default list but can be included with `?include_archived=true`. **Agent-to-agent calls** go through the Redis queue so sub-agents run in separate workers, avoiding recursive blocking. Results stream back via Redis Streams. **Function parameter defaults** pre-fill values when an agent calls a function. Supports Jinja2 templates referencing the agent's input variables: ```json theme={null} { "email/send_email": { "sender": "{{company_email}}", "priority": "high" } } ``` **User context in templates** — the calling user is always available as the `user` variable in system prompts and in function/query/connector parameter templates: ```jinja theme={null} You are assisting {{user.email}} from the {{user.custom_fields.department}} department. Answer in the user's language: {{user.custom_fields.locale}}. ``` * `user.id` — user UUID * `user.email` — user email * `user.custom_fields` — the user's [custom fields](/admin/users-roles#custom-fields) The `user` variable is platform-provided and overrides any same-named agent input variable, so locked parameters like `{{user.custom_fields.region}}` cannot be spoofed by the caller. # Components Source: https://docs.sinas.co/build-resources/components Renderable UI components Components are embeddable UI widgets built with JSX/HTML/JS and compiled by Sinas into browser-ready bundles. They can call agents, functions, queries, and access state through proxy endpoints. **Key properties:** | Property | Description | | -------------------- | ----------------------------------------------------------------------------------------- | | `namespace` / `name` | Unique identifier | | `title` | Display title | | `source_code` | JSX/HTML/JS source | | `compiled_bundle` | Auto-generated browser-ready JS | | `input_schema` | JSON Schema for component configuration | | `enabled_agents` | Agents the component can call | | `enabled_functions` | Functions the component can call | | `enabled_queries` | Queries the component can execute | | `enabled_components` | Other components it can embed | | `enabled_stores` | Stores the component can access (`{"store": "ns/name", "access": "readonly\|readwrite"}`) | | `css_overrides` | Custom CSS | | `visibility` | `private`, `shared`, or `public` | Components use the `sinas-ui` library (loaded from npm/unpkg) for a consistent look and feel. **Management endpoints:** ``` POST /api/v1/components # Create component GET /api/v1/components # List components GET /api/v1/components/{namespace}/{name} # Get component PUT /api/v1/components/{namespace}/{name} # Update component DELETE /api/v1/components/{namespace}/{name} # Delete component POST /api/v1/components/{namespace}/{name}/compile # Trigger compilation ``` **Share links** allow embedding components outside Sinas with optional expiration and view limits: ``` POST /api/v1/components/{namespace}/{name}/shares # Create share link GET /api/v1/components/{namespace}/{name}/shares # List share links DELETE /api/v1/components/{namespace}/{name}/shares/{token} # Revoke share link ``` **Runtime rendering:** ``` GET /components/{namespace}/{name}/render # Render as full HTML page GET /components/shared/{token} # Render via share token ``` **Proxy endpoints** allow components to call backend resources from the browser securely — the proxy enforces the component's `enabled_*` permissions: ``` POST /components/{ns}/{name}/proxy/queries/{q_ns}/{q_name}/execute # Execute query POST /components/{ns}/{name}/proxy/functions/{fn_ns}/{fn_name}/execute # Execute function POST /components/{ns}/{name}/proxy/states/{state_ns} # Access state ``` # Connectors Source: https://docs.sinas.co/build-resources/connectors HTTP API integrations with operations and auth Named HTTP client configurations with typed operations. Executed in-process in the backend (no container overhead). Operations are exposed as agent tools. Auth resolved from the Secrets store at call time. **Endpoints:** ``` POST /api/v1/connectors # Create connector GET /api/v1/connectors # List connectors GET /api/v1/connectors/{namespace}/{name} # Get connector PUT /api/v1/connectors/{namespace}/{name} # Update connector DELETE /api/v1/connectors/{namespace}/{name} # Delete connector POST /api/v1/connectors/parse-openapi # Parse OpenAPI spec (no connector required) POST /api/v1/connectors/{namespace}/{name}/import-openapi # Import operations from OpenAPI spec into connector POST /api/v1/connectors/{namespace}/{name}/test/{operation} # Test an operation POST /api/v1/connectors/{namespace}/{name}/oauth/authorize # Begin per-user OAuth flow (returns provider URL) GET /api/v1/connectors/{namespace}/{name}/oauth/status # Current user's OAuth connection status DELETE /api/v1/connectors/{namespace}/{name}/oauth/token # Disconnect (delete the user's stored token) ``` **Auth types:** `bearer`, `basic`, `api_key`, `oauth2_client_credentials`, `oauth2_authorization_code`, `sinas_token` (forwards caller's JWT), `none` Auth is resolved from the Secrets store. Private secrets override shared for the calling user — enabling multi-tenant patterns where each user can have their own API key for the same connector. An `api_key` can be sent as a header (default; `header` names it, default `X-Api-Key`) or as a query parameter (`position: query` + `paramName`, default `api_key`). Importing an OpenAPI spec also reads its `securitySchemes` and pre-fills a suggested auth config. ## OAuth 2.0 Two grants are supported. In both, `secret` names the Secret holding the **client secret** — the secret value itself never lives in connector config. **Client credentials** (`oauth2_client_credentials`) — service-to-service. Sinas fetches a token from `tokenUrl`, caches it in-process until shortly before expiry, and refreshes automatically: ```yaml theme={null} connectors: - name: analytics-api namespace: default baseUrl: https://api.example.com auth: type: oauth2_client_credentials tokenUrl: https://auth.example.com/oauth/token clientId: my-client-id secret: ANALYTICS_CLIENT_SECRET # Secret holding the client secret scopes: [read:stats] clientAuthMethod: body # "body" (client_secret_post) or "basic" (client_secret_basic) tokenParams: { audience: "https://api.example.com" } # optional extra token-request params ``` **Authorization code, per user** (`oauth2_authorization_code`) — the connector acts on behalf of the individual user. Each user clicks **Connect Account** in the console, consents at the provider, and gets their own encrypted token row (PKCE-protected; refreshed automatically on expiry): ```yaml theme={null} auth: type: oauth2_authorization_code authorizeUrl: https://provider.example.com/oauth/authorize tokenUrl: https://provider.example.com/oauth/token clientId: my-client-id secret: PROVIDER_CLIENT_SECRET scopes: [read:user] ``` Register this **redirect URI** with the provider (must match exactly): `https:///auth/connectors/oauth/callback` If a user has not connected (or their authorization expired with no refresh token), operations fail with an explicit "reconnect" error rather than sending an unauthenticated request. The flow is bound to the initiating browser via an HttpOnly cookie; set `OAUTH_BIND_BROWSER_SESSION=false` only in split-origin local dev (console and API on different ports). **Agent configuration:** ```yaml theme={null} agents: - name: slack-bot enabledConnectors: - connector: default/slack-api operations: [post_message, get_channel_info] parameters: post_message: channel: "{{ default_channel }}" ``` **YAML config:** ```yaml theme={null} connectors: - name: slack-api namespace: default baseUrl: https://slack.com/api auth: type: bearer secret: SLACK_BOT_TOKEN operations: - name: post_message method: POST path: /chat.postMessage parameters: type: object properties: channel: { type: string } text: { type: string } required: [channel, text] ``` **Execution history:** ``` GET /executions # List executions (filterable by chat_id, trigger_type, status) GET /executions/{execution_id} # Get execution details (includes tool_call_id link) POST /executions/{execution_id}/continue # Resume a paused execution with user input ``` Executions include a `tool_call_id` field linking them to the tool call that triggered them, enabling execution tree visualization in the Logs page. **Input schema presets:** The function editor includes built-in presets for common input/output schemas. Use the "Load preset" dropdown when editing schemas: | Preset | Use case | | ----------------------------- | --------------------------------------------------------------------------------------------- | | **Pre-upload filter** | Content filtering before file upload (receives file content, returns approved/rejected) | | **Post-upload** | Processing after successful file upload (receives file\_id, metadata) | | **CDC (Change Data Capture)** | Processing database changes (receives table, rows, poll\_column, count) | | **Message Hook** | Message lifecycle hook (receives message, chat\_id, agent; returns content mutation or block) | # Functions Source: https://docs.sinas.co/build-resources/functions Python code in sandboxed containers Functions are Python code that runs in isolated Docker containers. They can be used as agent tools, triggered by webhooks or schedules, or executed directly. **Key properties:** | Property | Description | | -------------------- | ------------------------------------------------------------------------------------ | | `namespace` / `name` | Unique identifier | | `code` | Python source code | | `description` | Shown to the LLM when used as an agent tool | | `input_schema` | JSON Schema for input validation | | `output_schema` | JSON Schema for output validation | | `shared_pool` | Run in shared container instead of sandbox container (admin-only) | | `requires_approval` | Require user approval when called by an agent | | `timeout` | Per-function timeout in seconds (overrides global `FUNCTION_TIMEOUT`, default: null) | ## Function signature The entry point must be named `handler`. It receives two arguments — `input_data` (validated against input\_schema) and `context` (execution metadata): ```python theme={null} def handler(input_data, context): # input_data: dict validated against input_schema # # context dict — always present: # { # "user_id": str, # ID of the user who triggered execution # "user_email": str, # Email of the triggering user # "user_custom_fields": dict, # The user's custom fields (org-specific profile data) # "access_token": str, # Short-lived JWT for calling the Sinas API # "execution_id": str, # Unique execution ID # "trigger_type": str, # "AGENT" | "API" | "WEBHOOK" | "SCHEDULE" | "CDC" | "HOOK" | "MANUAL" # "chat_id": str, # Chat ID (when triggered by an agent, empty otherwise) # "secrets": dict, # Decrypted secrets (shared pool only): {"NAME": "value"} # } return {"result": "value"} # Must match output_schema ``` > **Legacy:** Functions named after the resource name (e.g. `def send_email(input_data, context)`) still work but log a deprecation warning. Migrate to `def handler(...)`. The `access_token` lets functions call back into the Sinas API with the triggering user's identity — useful for reading state, triggering other functions, or accessing any other endpoint. ## Trigger-specific input\_data Depending on how the function is invoked, `input_data` is populated differently: | Trigger | input\_data contents | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **AGENT / API / MANUAL / SCHEDULE** | Values matching your input schema, provided by the caller | | **WEBHOOK** | Webhook `default_values` merged with request body/query params | | **CDC** | `{"table": "schema.table", "operation": "CHANGE", "rows": [...], "poll_column": str, "count": int, "timestamp": str}` | | **HOOK** | `{"message": {"role": "user"\|"assistant", "content": "..."}, "chat_id": str, "agent": {"namespace": str, "name": str}, "session_key": str\|null, "user_id": str}` | | **Collection content filter** | `{"content_base64": str, "namespace": str, "collection": str, "filename": str, "content_type": str, "size_bytes": int, "user_metadata": dict, "user_id": str}` | | **Collection post-upload** | `{"file_id": str, "namespace": str, "collection": str, "filename": str, "version": int, "file_path": str, "user_id": str, "metadata": dict}` | ## Hook function return values Functions used as message hooks (configured in agent's `hooks` field) can return: | Return value | Effect | | --------------------------------- | ------------------------------------------------------------ | | `None` or `{}` | Pass through unchanged | | `{"content": "..."}` | Mutate the message content | | `{"block": true, "reply": "..."}` | Block the pipeline, return reply to client (sync hooks only) | Async hooks run fire-and-forget. For `on_assistant_message` async hooks, the return value retroactively updates the stored message (not the already-streamed response). ## Interactive input (shared containers only) Functions running in shared containers (`shared_pool=true`) can call `input()` to pause and wait for user input: ```python theme={null} def handler(input_data, context): name = input("What is your name?") # Pauses execution confirm = input(f"Confirm {name}?") # Can call multiple times return {"name": name, "confirmed": confirm} ``` When `input()` is called: 1. The execution status changes to `AWAITING_INPUT` with the prompt string 2. The function thread blocks until a resume value is provided 3. The calling agent or API client resumes execution with the user's response 4. Multiple `input()` calls are supported (each triggers a new pause/resume cycle) In sandbox containers (`shared_pool=false`), calling `input()` raises a `RuntimeError`. **Execution:** Functions run in pre-warmed Docker containers from a managed pool. Input is validated before execution, output is validated after. All executions are logged with status, duration, input/output, and any errors. **Nested executions are depth-limited.** A function can invoke other functions or agents (e.g. by calling the Sinas API with its execution token). Such chains are bounded by `MAX_EXECUTION_DEPTH` (defaults to the shared worker count): a call past the limit is rejected immediately rather than silently exhausting the shared worker pool and deadlocking. Keep `shared_pool` functions shallow, or raise `SHARED_POOL_RESERVE` / scale workers if you intentionally nest deeply. See [environment variables](/getting-started/environment-variables). **Endpoints:** ``` POST /functions/{namespace}/{name}/execute # Execute (sync, waits for result) POST /functions/{namespace}/{name}/execute/async # Execute (async, returns execution_id) POST /api/v1/functions # Create function GET /api/v1/functions # List functions GET /api/v1/functions/{namespace}/{name} # Get function PUT /api/v1/functions/{namespace}/{name} # Update function DELETE /api/v1/functions/{namespace}/{name} # Delete function GET /api/v1/functions/{namespace}/{name}/versions # List code versions ``` # Queries Source: https://docs.sinas.co/build-resources/queries Parameterized SQL against database connections Queries are saved SQL templates that can be executed directly or used as agent tools. **Key properties:** | Property | Description | | ------------------------ | ----------------------------------------------------- | | `namespace` / `name` | Unique identifier | | `database_connection_id` | Which database connection to use | | `description` | Shown to the LLM as the tool description | | `operation` | `read` or `write` | | `sql` | SQL with `:param_name` placeholders | | `input_schema` | JSON Schema for parameter validation | | `output_schema` | JSON Schema for output validation | | `timeout_ms` | Query timeout (default: 5000ms) | | `max_rows` | Max rows returned for read operations (default: 1000) | **Agent query parameters** support defaults and locking: ```yaml theme={null} query_parameters: "analytics/user_orders": "user_id": value: "{{user_id}}" # Jinja2 template from agent input locked: true # Hidden from LLM, always injected "status": value: "pending" locked: false # Shown to LLM with default, LLM can override ``` Locked parameters prevent the LLM from seeing or modifying security-sensitive values (like `user_id`). **Contextual parameters:** The following parameters are automatically injected into every query execution and can be referenced in SQL: | Parameter | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `:user_id` | UUID of the user who triggered the query | | `:user_email` | Email of the triggering user | | `:user_custom_` | Each scalar entry in the user's [custom fields](/admin/users-roles#custom-fields) (e.g. `:user_custom_region` for `custom_fields.region`) | These are always available regardless of the query's `input_schema`. Use them for row-level security: ```sql theme={null} -- Only return orders belonging to the calling user SELECT * FROM orders WHERE created_by = :user_id -- Audit trail INSERT INTO audit_log (action, performed_by) VALUES (:action, :user_email) -- Per-user scoping on an org-specific attribute SELECT * FROM accounts WHERE region = :user_custom_region ``` Only scalar custom fields (string, number, boolean) whose names are valid SQL identifiers are exposed; nested objects and lists are skipped. **Endpoints:** ``` POST /queries/{namespace}/{name}/execute # Execute with parameters (runtime) POST /api/v1/queries # Create query GET /api/v1/queries # List queries GET /api/v1/queries/{namespace}/{name} # Get query PUT /api/v1/queries/{namespace}/{name} # Update query DELETE /api/v1/queries/{namespace}/{name} # Delete query ``` *** # Secrets Source: https://docs.sinas.co/build-resources/secrets Encrypted values for API keys and credentials Write-only credential store. Values are encrypted at rest and never returned via the API. Secrets are available in function context as `context['secrets']` — only for shared pool (trusted) functions. Connectors resolve auth from secrets automatically. **Visibility:** * `shared` (default) — global, available to all users and connectors * `private` — per-user, only used when that user triggers a connector or function Private secrets override shared secrets with the same name. This enables multi-tenant patterns: admin sets a shared `HUBSPOT_API_KEY`, individual users can override with their own private key. **Endpoints:** ``` POST /api/v1/secrets # Create or update (upsert by name+visibility) GET /api/v1/secrets # List names and descriptions (no values) GET /api/v1/secrets/{name} # Get metadata (no value) PUT /api/v1/secrets/{name} # Update value or description DELETE /api/v1/secrets/{name} # Delete ``` **Access at runtime (shared pool functions only):** ```python theme={null} def my_function(input, context): # Private secrets override shared for the calling user token = context['secrets']['SLACK_BOT_TOKEN'] ``` **YAML config:** ```yaml theme={null} secrets: - name: SLACK_BOT_TOKEN value: xoxb-... # omit to skip value update on re-apply description: Slack bot OAuth token # visibility defaults to "shared" in YAML config ``` # Skills Source: https://docs.sinas.co/build-resources/skills Knowledge resources for agents Skills are reusable instruction documents that give agents specialized knowledge or guidelines. **Key properties:** | Property | Description | | -------------------- | -------------------------------------------------------------------- | | `namespace` / `name` | Unique identifier | | `description` | What the skill helps with (shown to the LLM as the tool description) | | `content` | Markdown instructions | **Two modes:** | Mode | Behavior | Best for | | ---------------------------------- | ------------------------------------------- | -------------------------------------------------------------- | | **Preloaded** (`preload: true`) | Injected into the system prompt | Tone guidelines, safety rules, persona traits | | **Progressive** (`preload: false`) | Exposed as a tool the LLM calls when needed | Research methods, domain expertise, task-specific instructions | Example agent configuration: ```yaml theme={null} enabled_skills: - skill: "default/tone_guidelines" preload: true # Always present in system prompt - skill: "default/web_research" preload: false # LLM decides when to retrieve it ``` **Endpoints:** ``` POST /api/v1/skills # Create skill GET /api/v1/skills # List skills GET /api/v1/skills/{namespace}/{name} # Get skill PUT /api/v1/skills/{namespace}/{name} # Update skill DELETE /api/v1/skills/{namespace}/{name} # Delete skill ``` # System Tools Source: https://docs.sinas.co/build-resources/system-tools Platform capabilities: code execution, packages, introspection System tools are opt-in platform capabilities beyond the normal function/query toolkit. Enable them via the `system_tools` property on agents. Each tool is either a simple string (no config needed) or an object with a `name` and tool-specific configuration. ```yaml theme={null} agents: - name: my-agent systemTools: - codeExecution - packageManagement - configIntrospection - name: databaseIntrospection connections: - built-in - analytics-db ``` **Available system tools:** | Tool | Type | Description | | ----------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | `codeExecution` | string | Generate and execute Python code in sandboxed containers | | `configIntrospection` | string | Read-only inspection of the current Sinas configuration: list resource types, browse resources by name/description, read full resource detail | | `packageManagement` | string | Validate, preview, install, and uninstall Sinas packages. Install and uninstall require user approval. | | `databaseIntrospection` | object | Read-only schema inspection of database connections. Requires `connections` list specifying which connections the agent can access. | **Config introspection tools:** * `sinas_config_inspect` — Resource type counts (overview) * `sinas_config_list(type, namespace?)` — Names + descriptions for a type * `sinas_config_get(type, namespace, name)` — Full detail of one resource **Package management tools:** * `sinas_package_validate(yaml)` — Validate package YAML (syntax + schema) * `sinas_package_preview(yaml)` — Dry-run install (shows what would change) * `sinas_package_install(yaml)` — Install package (requires approval) * `sinas_package_uninstall(name)` — Uninstall package (requires approval) * `sinas_package_list()` — List installed packages * `sinas_package_export(name)` — Export package as YAML **Database introspection tools:** * `sinas_db_list_tables(connectionName)` — List tables with schemas, types, row counts, and table annotations * `sinas_db_describe_table(table, connectionName, schema?)` — Columns, types, indexes, foreign keys, and column annotations The `databaseIntrospection` tool requires a `connections` list. The agent can only introspect connections listed in its config. This uses the same connection pool as regular queries and includes annotations from the semantic layer (table/column display names and descriptions). **Collection file tools (readwrite access):** When an agent has `access: readwrite` on an enabled collection, it gets additional tools: * `write_file_{ns}_{name}(filename, content)` — Write/overwrite a file (creates new version) * `edit_file_{ns}_{name}(filename, old_string, new_string)` — Surgical edit via exact string replacement * `delete_file_{ns}_{name}(filename)` — Delete a file These are in addition to the read tools (`search_collection_*`, `get_file_*`) that every enabled collection provides. `get_file` supports `offset` and `limit` parameters for reading specific line ranges of large files. # Database Connections Source: https://docs.sinas.co/configure/database-connections External database connections Database connections store credentials and manage connection pools for external databases. **Supported databases:** PostgreSQL, ClickHouse, Snowflake **Key properties:** | Property | Description | | -------------------------------------------------- | ------------------------------------------------ | | `name` | Unique connection name | | `connection_type` | `postgresql`, `clickhouse`, or `snowflake` | | `host`, `port`, `database`, `username`, `password` | Connection details | | `ssl_mode` | Optional SSL configuration | | `config` | Pool settings (`min_pool_size`, `max_pool_size`) | Passwords are encrypted at rest. Connection pools are managed automatically and invalidated when settings change. **Endpoints (admin only):** ``` POST /api/v1/database-connections # Create connection GET /api/v1/database-connections # List connections GET /api/v1/database-connections/{name} # Get by name PATCH /api/v1/database-connections/{id} # Update DELETE /api/v1/database-connections/{id} # Delete POST /api/v1/database-connections/test # Test raw connection params POST /api/v1/database-connections/{id}/test # Test saved connection ``` # LLM Providers Source: https://docs.sinas.co/configure/llm-providers API keys and model configuration LLM providers connect Sinas to language model APIs. **Supported providers:** | Type | Description | | ----------- | -------------------------------------------------------------------- | | `openai` | OpenAI API (GPT-4, GPT-4o, o1, etc.) and OpenAI-compatible endpoints | | `anthropic` | Anthropic API (Claude 3, Claude 4, etc.) | | `mistral` | Mistral AI (Mistral Large, Pixtral, etc.) | | `ollama` | Local models via Ollama | **Key properties:** | Property | Description | | --------------- | ------------------------------------------------------------- | | `name` | Unique provider name | | `provider_type` | `openai`, `anthropic`, `mistral`, or `ollama` | | `api_key` | API key (encrypted at rest, never returned in API responses) | | `api_endpoint` | Custom endpoint URL (required for Ollama, useful for proxies) | | `default_model` | Model used when agents don't specify one | | `config` | Additional settings (e.g., `max_tokens`, `organization_id`) | | `is_default` | Whether this is the system-wide default provider | **Provider resolution for agents:** 1. Agent's explicit `llm_provider_id` if set 2. Agent's `model` field with the resolved provider 3. Provider's `default_model` 4. System default provider as final fallback **Endpoints (admin only):** ``` POST /api/v1/llm-providers # Create provider GET /api/v1/llm-providers # List providers GET /api/v1/llm-providers/{name} # Get provider PATCH /api/v1/llm-providers/{id} # Update provider DELETE /api/v1/llm-providers/{id} # Delete provider ``` # Templates Source: https://docs.sinas.co/configure/templates Reusable email and document templates Templates are Jinja2-based documents for emails, notifications, and dynamic content. **Key properties:** | Property | Description | | -------------------- | --------------------------------------------- | | `namespace` / `name` | Unique identifier | | `title` | Optional title template (e.g., email subject) | | `html_content` | Jinja2 HTML template | | `text_content` | Optional plain-text fallback | | `variable_schema` | JSON Schema for validating template variables | HTML output is auto-escaped to prevent XSS. Missing variables cause errors (strict mode). **Management endpoints:** ``` POST /api/v1/templates # Create template GET /api/v1/templates # List templates GET /api/v1/templates/{id} # Get by ID GET /api/v1/templates/by-name/{namespace}/{name} # Get by name PATCH /api/v1/templates/{id} # Update DELETE /api/v1/templates/{id} # Delete ``` **Runtime endpoints:** ``` POST /templates/{id}/render # Render template with variables POST /templates/{id}/send # Render and send as email ``` *** # Concepts Source: https://docs.sinas.co/getting-started/concepts Namespaces, tools, triggers, and declarative configuration ## Namespaces Most resources are organized by **namespace** and **name**. A namespace groups related resources (e.g., `support/ticket-agent`, `analytics/daily-report`). The default namespace is `default`. Resources are uniquely identified by their `namespace/name` pair. ## Tools Agents interact with the outside world through **tools** — capabilities you enable per agent: | Tool type | What it does | | --------------- | ------------------------------------------ | | **Functions** | Execute Python code in isolated containers | | **Agents** | Call other agents as sub-agents | | **Skills** | Retrieve instruction/knowledge documents | | **Queries** | Run SQL against external databases | | **Collections** | Search uploaded files | | **States** | Read and write persistent key-value data | ## Trigger Types Functions and agents can be triggered in multiple ways: * **API** — Direct execution via the runtime API * **Manual** — Via the console UI * **Agent** — Called as a tool during a chat conversation * **Webhook** — Via an HTTP request to a configured endpoint * **Schedule** — Via a cron expression on a timer * **CDC** — Automatically when rows are inserted or updated in an external database table ## Declarative Configuration All resources can be defined in a YAML file and applied idempotently via the API or on startup. Config-managed resources are tracked with checksums for change detection. See [Config Manager](#config-manager) for details. *** # Deployment Source: https://docs.sinas.co/getting-started/deployment Install, update, and configure Sinas ## Prerequisites * A VPS or server with 2+ CPU cores, 4GB+ RAM, 50GB+ storage * A domain name pointed at the server (A record) * An SMTP service for login emails (SendGrid, Mailgun, AWS SES, etc.) — only required if you choose `otp` or `password+otp` auth mode. Skip for `password`-only / airgapped installs. ## Install ```bash theme={null} sudo curl -fsSL https://raw.githubusercontent.com/sinas-platform/sinas/main/install.sh -o /tmp/sinas-install.sh && sudo bash /tmp/sinas-install.sh ``` The installer will: * Install Docker if needed * Generate secure keys (`SECRET_KEY`, `ENCRYPTION_KEY`, `DATABASE_PASSWORD`) * Prompt for your domain, admin email, and authentication mode (and SMTP and/or admin password depending on the mode you pick) * Create `.env` in `/opt/sinas/` * Pull pre-built images from the container registry and start all services * Caddy automatically provisions SSL via Let's Encrypt All services start automatically: PostgreSQL, PgBouncer, Redis, ClickHouse, the backend API (port 8000), queue workers, the scheduler, and the web console (port 51245). Migrations run automatically on startup. ## Update ```bash theme={null} cd /opt/sinas docker compose pull docker compose up -d ``` ## Manual development installation For local development, see [INSTALL.md](https://github.com/sinas-platform/sinas/blob/main/INSTALL.md). ## Log in 1. Open the console at `https://yourdomain.com:51245` 2. Enter your `SUPERADMIN_EMAIL` address 3. Then, depending on your `AUTH_MODE`: * `otp` — check your inbox for the 6-digit code * `password` — enter your `SUPERADMIN_PASSWORD` * `password+otp` — enter your password, then the emailed code 4. You're in. See [Authentication](/platform/authentication) for the full reference. ## Configure an LLM provider Before agents can work, you need at least one LLM provider: ```bash theme={null} curl -X POST https://yourdomain.com/api/v1/llm-providers \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "openai", "provider_type": "openai", "api_key": "sk-...", "default_model": "gpt-4o", "is_default": true }' ``` ## Start chatting A default agent is created on startup. Create a chat and send a message: ```bash theme={null} # Create a chat with the default agent curl -X POST https://yourdomain.com/agents/default/default/chats \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' # Send a message (use the chat_id from the response) curl -X POST https://yourdomain.com/chats/{chat_id}/messages/stream \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"content": "Hello!"}' ``` *** # Environment Variables Source: https://docs.sinas.co/getting-started/environment-variables All configuration options ## Required | Variable | Description | | ------------------- | ------------------------------------------------------------------------- | | `SECRET_KEY` | JWT signing key (auto-generated by install script) | | `ENCRYPTION_KEY` | Fernet key for encrypting stored credentials (LLM API keys, DB passwords) | | `DATABASE_PASSWORD` | PostgreSQL password | | `SUPERADMIN_EMAIL` | Admin user created on first startup | | `DOMAIN` | Your domain for Caddy auto-HTTPS (e.g., `sinas.example.com`) | ## Authentication | Variable | Default | Description | | ----------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AUTH_MODE` | `otp` | One of `otp`, `password`, `password+otp`. See [Authentication](/platform/authentication) for the differences. | | `SUPERADMIN_PASSWORD` | *(none)* | Seed password for the superadmin. Only used when `AUTH_MODE` includes `password`. Setting this and restarting acts as a password-reset escape hatch. | | `TOKEN_EXCHANGE_DEFAULT_ROLE` | `GuestUsers` | Role assigned to users auto-provisioned via `POST /auth/token/exchange`. See [Token Exchange](/platform/authentication#token-exchange-bring-your-own-auth). | | `OAUTH_BIND_BROWSER_SESSION` | `true` | Bind connector OAuth (authorization-code) flows to the browser that started them via an HttpOnly nonce cookie — prevents account-linking CSRF. Set `false` only for split-origin local dev (console and API on different ports); keep `true` in production. | ## SMTP Required when `AUTH_MODE` is `otp` or `password+otp`. Skip entirely for `password` mode (airgapped deployments). | Variable | Default | Description | | --------------- | -------- | ------------------------------------------------ | | `SMTP_HOST` | *(none)* | SMTP server hostname (e.g., `smtp.sendgrid.net`) | | `SMTP_PORT` | `587` | SMTP port | | `SMTP_USER` | *(none)* | SMTP username | | `SMTP_PASSWORD` | *(none)* | SMTP password or API key | | `SMTP_DOMAIN` | *(none)* | Email "from" domain (e.g., `example.com`) | ## Application | Variable | Default | Description | | ----------------------------- | -------- | ------------------------------------- | | `DEBUG` | `false` | Enable verbose logging | | `CORS_ORIGINS` | *(none)* | Comma-separated allowed origins | | `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | JWT access token lifetime | | `REFRESH_TOKEN_EXPIRE_DAYS` | `30` | Refresh token lifetime | | `OTP_EXPIRE_MINUTES` | `10` | OTP code validity | | `OTP_MAX_ATTEMPTS` | `2` | Wrong OTP guesses before invalidation | ## Rate Limiting | Variable | Default | Description | | ---------------------------- | ------- | ----------------------------------------- | | `RATE_LIMIT_LOGIN_IP_MAX` | `10` | Max login requests per IP per window | | `RATE_LIMIT_LOGIN_EMAIL_MAX` | `5` | Max login requests per email per window | | `RATE_LIMIT_OTP_IP_MAX` | `10` | Max OTP verify requests per IP per window | | `RATE_LIMIT_WINDOW_SECONDS` | `900` | Rate limit window (15 minutes) | ## Function Execution | Variable | Default | Description | | --------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `FUNCTION_TIMEOUT` | `300` | Max execution time in seconds | | `MAX_FUNCTION_MEMORY` | `512` | Memory limit in MB | | `MAX_FUNCTION_CPU` | `1.0` | CPU cores per function | | `MAX_FUNCTION_STORAGE` | `1g` | Disk storage limit | | `FUNCTION_CONTAINER_IDLE_TIMEOUT` | `3600` | Idle container cleanup (seconds) | | `ALLOW_PACKAGE_INSTALLATION` | `true` | Allow `pip install` in functions | | `ALLOWED_PACKAGES` | *(all)* | Comma-separated package whitelist | | `MAX_EXECUTION_DEPTH` | *(= workers)* | Max nesting depth for execution chains (a function/agent invoking another execution). A deeper call is rejected fast instead of deadlocking the shared pool. Defaults to `DEFAULT_WORKER_COUNT`; `0` disables. | | `SHARED_POOL_RESERVE` | `0` | Shared-worker slots reserved for nested calls so a parent waiting on a child can't starve the pool. `0` = off. For full single-chain safety set to `MAX_EXECUTION_DEPTH - 1`. | ## Executors How Sinas runs user code. `SANDBOX_EXECUTOR` handles untrusted code (untrusted functions and agent code execution) and must isolate every execution; `TRUSTED_EXECUTOR` handles admin-approved functions (`shared_pool=true`). | Variable | Default | Description | | ------------------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SANDBOX_EXECUTOR` | `docker_pool` | `docker_pool` (warm container pool), `docker_ephemeral` (single-use container per execution, baked package image), `k8s_pod` (single-use Kubernetes pod per execution), or `disabled` (reject sandbox executions) | | `TRUSTED_EXECUTOR` | `docker_shared` | `docker_shared` (long-lived shared worker containers) or `inprocess` (run inside the queue-worker process — no Docker socket needed, but no `input()`/pause support) | With `SANDBOX_EXECUTOR=k8s_pod` and `TRUSTED_EXECUTOR=inprocess`, no process needs a Docker socket — this is the configuration the Helm chart uses. `inprocess` runs trusted code with the worker's own environment and credentials, so only enable `shared_pool` on functions you'd trust with them. ### Kubernetes executor (`SANDBOX_EXECUTOR=k8s_pod`) Requires running in-cluster with a ServiceAccount that can `create`/`get`/ `list`/`delete` pods and use `pods/exec` in the sandbox namespace (the Helm chart provisions this). | Variable | Default | Description | | ---------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | | `K8S_SANDBOX_NAMESPACE` | *(own namespace)* | Namespace for sandbox pods | | `K8S_SANDBOX_IMAGE` | `FUNCTION_CONTAINER_IMAGE` | Image for sandbox pods (must be pullable by the cluster) | | `K8S_SANDBOX_SERVICE_ACCOUNT` | *(namespace default)* | ServiceAccount assigned to sandbox pods | | `K8S_SANDBOX_POD_READY_TIMEOUT` | `120` | Seconds to wait for a sandbox pod to become ready | | `K8S_SANDBOX_INSTALL_DEPENDENCIES` | `true` | `pip install` platform dependencies into each pod; set to `false` when they are baked into `K8S_SANDBOX_IMAGE` | ## Sandbox Containers Used by `SANDBOX_EXECUTOR=docker_pool` (the warm pool). | Variable | Default | Description | | ------------------------ | ------- | ------------------------------------ | | `SANDBOX_MIN_SIZE` | `4` | Containers to pre-create | | `SANDBOX_MAX_SIZE` | `20` | Maximum sandbox containers | | `SANDBOX_MIN_IDLE` | `2` | Replenish when idle drops below this | | `SANDBOX_MAX_EXECUTIONS` | `100` | Recycle after N executions | ## Agent Processing | Variable | Default | Description | | ---------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MAX_TOOL_ITERATIONS` | `25` | Max consecutive tool-call rounds per message | | `MAX_HISTORY_MESSAGES` | `100` | Messages loaded for conversation context | | `AGENT_JOB_TIMEOUT` | `600` | Agent job timeout (seconds) | | `AGENT_DELEGATE_TIMEOUT` | `600` | How long a parent agent waits for a delegated sub-agent (block mode). Raise together with `AGENT_JOB_TIMEOUT` — in block mode the parent's own job clock keeps running while it waits | | `AGENT_DELEGATE_MODE` | `block` | `block`: parent holds its worker slot while awaiting sub-agents. `suspend`: parent job ends at delegation and a resume job continues the conversation when the children finish (frees the slot) | | `AGENT_MAX_DELEGATION_DEPTH` | `5` | Reject agent-to-agent delegation chains deeper than this (`0` disables the bound) | | `AGENT_SUBAGENT_QUEUE` | `true` | Route delegated (sub-agent) jobs to the dedicated `queue-agent-sub` worker so parents can never starve their children of worker slots | | `CODE_EXECUTION_TIMEOUT` | `120` | Code execution timeout (seconds) | ## Tool Results | Variable | Default | Description | | ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TOOL_RESULT_CONTEXT_MAX_SIZE` | `102400` | Max characters of a single tool result entering the LLM context (\~25K tokens). Oversized results are truncated **structure-aware**: JSON lists trimmed to whole elements, long strings clipped, always with a machine-readable `{"_truncated": true, "returned": X, "total": Y}` marker so the model pages instead of retrying. Lower (e.g. `50000`) for a tighter per-turn token budget | | `TOOL_RESULT_MAX_SIZE` | `102400` | Max bytes the tool result **store** persists per result row (distinct from the context cap above) | | `TOOL_RESULT_MAX_INLINE` | `5` | Last N tool results kept inline in conversation history | | `TOOL_RESULT_RETENTION_DAYS` | `30` | Days stored tool results are retained | ## Scaling | Variable | Default | Description | | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `BACKEND_REPLICAS` | `1` | Backend API replicas | | `UVICORN_WORKERS` | `4` | Workers per backend replica | | `QUEUE_WORKER_REPLICAS` | `2` | Function queue worker replicas | | `QUEUE_AGENT_REPLICAS` | `2` | Agent queue worker replicas | | `QUEUE_FUNCTION_CONCURRENCY` | `10` | Concurrent functions per worker | | `QUEUE_AGENT_CONCURRENCY` | `5` | Concurrent agent jobs per worker | | `QUEUE_AGENT_SUB_REPLICAS` | `2` | Sub-agent queue worker replicas (delegated agent jobs) | | `QUEUE_AGENT_SUB_CONCURRENCY` | `5` | Concurrent jobs per sub-agent worker | | `DEFAULT_WORKER_COUNT` | `4` | Shared (trusted) worker containers for `shared_pool` functions. Also the default ceiling for `MAX_EXECUTION_DEPTH`. | ## Resource Limits (Docker) | Variable | Default | Description | | ------------------------ | ------- | ----------------------------------- | | `APP_CPU_LIMIT` | `2.0` | Max CPU cores for backend container | | `APP_MEMORY_LIMIT` | `2G` | Max RAM for backend container | | `APP_CPU_RESERVATION` | `0.5` | Guaranteed CPU cores | | `APP_MEMORY_RESERVATION` | `512M` | Guaranteed RAM | ## Database | Variable | Default | Description | | --------------- | ---------------------- | -------------------------------------------------- | | `DATABASE_USER` | `postgres` | PostgreSQL user | | `DATABASE_HOST` | `postgres` | PostgreSQL host | | `DATABASE_PORT` | `5432` | PostgreSQL port | | `DATABASE_NAME` | `sinas` | Database name | | `DATABASE_URL` | *(built from above)* | Full connection string (overrides individual vars) | | `REDIS_URL` | `redis://redis:6379/0` | Redis connection string | ## ClickHouse (Optional) | Variable | Default | Description | | --------------------------------- | ------------ | ------------------------------ | | `CLICKHOUSE_HOST` | `clickhouse` | ClickHouse host | | `CLICKHOUSE_PORT` | `8123` | ClickHouse HTTP port | | `CLICKHOUSE_USER` | `default` | ClickHouse user | | `CLICKHOUSE_PASSWORD` | *(empty)* | ClickHouse password | | `CLICKHOUSE_DATABASE` | `sinas` | ClickHouse database | | `CLICKHOUSE_RETENTION_DAYS` | `90` | Data retention (no S3) | | `CLICKHOUSE_HOT_RETENTION_DAYS` | `30` | Hot retention (with S3) | | `CLICKHOUSE_S3_ENDPOINT` | *(none)* | S3 endpoint for tiered storage | | `CLICKHOUSE_S3_BUCKET` | *(none)* | S3 bucket name | | `CLICKHOUSE_S3_ACCESS_KEY_ID` | *(none)* | S3 access key | | `CLICKHOUSE_S3_SECRET_ACCESS_KEY` | *(none)* | S3 secret key | | `CLICKHOUSE_S3_REGION` | `us-east-1` | S3 region | ## Declarative Config | Variable | Default | Description | | ------------------- | -------- | ---------------------------- | | `CONFIG_FILE` | *(none)* | Path to YAML config file | | `AUTO_APPLY_CONFIG` | `false` | Apply config file on startup | The simplest useful setup requires the required variables plus a configured LLM provider. Everything else (functions, skills, state, etc.) is optional and can be added incrementally. *** # Deploy on Kubernetes Source: https://docs.sinas.co/getting-started/kubernetes Run Sinas on any Kubernetes cluster with the bundled Helm chart Sinas ships a Helm chart at [`charts/sinas`](https://github.com/sinas-platform/sinas/tree/main/charts/sinas) that deploys the full platform — backend, queue workers, scheduler, CDC worker, console — plus bundled PostgreSQL, Redis, and ClickHouse. Untrusted code (untrusted functions and agent code execution) runs in **ephemeral hardened Pods**: one pod per execution, created through the Kubernetes API and deleted afterwards. No Docker socket, no privileged Docker-in-Docker. Admin-approved (`shared_pool`) functions run in-process in the queue workers. This works on any conformant cluster — kind, k3s, Scaleway Kapsule, GKE, **AWS EKS** (EKS nodes run containerd and have no Docker socket, which is exactly why the pod-based executor exists). ## Install The chart is published with every release — no checkout required. Install it straight from the registry (replace `0.3.0` with the version you want): ```bash theme={null} helm install sinas oci://ghcr.io/sinas-platform/charts/sinas --version 0.3.0 \ --namespace sinas --create-namespace \ --set domain=sinas.example.com \ --set ingress.className=nginx \ --set secrets.secretKey=$(openssl rand -hex 32) \ --set secrets.encryptionKey=$(python3 -c "import base64,os;print(base64.urlsafe_b64encode(os.urandom(32)).decode())") \ --set postgres.password=$(openssl rand -hex 16) \ --set clickhouse.password=$(openssl rand -hex 16) \ --set superadminEmail=you@example.com ``` Every release also attaches a packaged `sinas-.tgz` you can install from directly, and a checkout works too: ```bash theme={null} # from the release asset helm install sinas https://github.com/sinas-platform/sinas/releases/download/0.3.0/sinas-0.3.0.tgz [...] # or from a checkout of the tag git checkout 0.3.0 && helm install sinas ./charts/sinas [...] ```
Full example with a local checkout ```bash theme={null} helm install sinas ./charts/sinas \ --namespace sinas --create-namespace \ --set domain=sinas.example.com \ --set ingress.className=nginx \ --set secrets.secretKey=$(openssl rand -hex 32) \ --set secrets.encryptionKey=$(python3 -c "import base64,os;print(base64.urlsafe_b64encode(os.urandom(32)).decode())") \ --set postgres.password=$(openssl rand -hex 16) \ --set clickhouse.password=$(openssl rand -hex 16) \ --set superadminEmail=you@example.com ```
The backend runs database migrations on startup; first boot takes a minute. Watch sandbox pods appear during executions: ```bash theme={null} kubectl get pods -n sinas -l sinas.type=sandbox-executor -w ``` ## How sandbox pods are secured Each execution pod is created with the same hardening as the Docker sandbox: * all capabilities dropped (`CHOWN`/`SETUID`/`SETGID` added back), no privilege escalation, `RuntimeDefault` seccomp profile * memory / CPU / ephemeral-storage limits from `MAX_FUNCTION_*` * in-memory `/tmp` (100Mi), single-use, deleted after the execution, `activeDeadlineSeconds` as a leak backstop * **no ServiceAccount token** — sandbox pods cannot talk to the Kubernetes API * a NetworkPolicy that allows DNS and internet egress only: sandbox pods can never reach cluster-internal services (PostgreSQL, Redis, other namespaces) The services themselves use a ServiceAccount whose Role is limited to `pods` + `pods/exec` in the release namespace — that is the entire privilege surface replacing the Docker socket. NetworkPolicies require a CNI that enforces them (Cilium, Calico, kindnetd on recent kind, VPC CNI with a policy engine on EKS). Without enforcement the deployment still works, but sandbox pods are not network-isolated. ## Executor image and cold-start latency Sandbox pods run `executor.image`. By default, platform dependencies (the admin-managed package list) are `pip install`ed into each pod at creation, which adds seconds to every sandbox execution. For production, bake them in: ```dockerfile theme={null} FROM ghcr.io/sinas-platform/sinas/executor:latest RUN pip install --no-cache-dir ``` ```yaml theme={null} executor: image: registry.example.com/sinas-executor-baked:v1 installDependencies: false ``` ## Advanced: per-client node scheduling These aren't exposed as values in the bundled `charts/sinas` chart — they're env vars for operators building their own multi-tenant deployment tooling around the k8s\_pod executor (e.g. a chart that provisions one release per customer). Sinas doesn't decide scheduling policy itself; it just applies whatever it's given, so the same knobs work whether you want every customer's sandbox pods spread across dedicated nodes or packed together on shared ones — that choice lives entirely in your own tooling, not here. | Setting | Default | Description | | --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `K8S_RELEASE_NAME` | *(empty)* | Stamped as the `app.kubernetes.io/instance` label on sandbox pods, so your own affinity rules have something to match on. No label if unset. | | `K8S_SANDBOX_NODE_SELECTOR` | `{}` | JSON-encoded `nodeSelector` applied verbatim to sandbox pods. | | `K8S_SANDBOX_TOLERATIONS` | `[]` | JSON-encoded list of `toleration` objects applied verbatim. | | `K8S_SANDBOX_AFFINITY` | `{}` | JSON-encoded K8s `affinity` object (`podAffinity` to pack a customer's sandbox pods onto the same node as their other workloads, `podAntiAffinity` to spread them across dedicated nodes, `nodeAffinity`, or any combination) applied verbatim. | All four are no-ops by default, matching the behavior described above with no scheduling constraint at all. ## Sizing under namespace quotas Sandbox pods are created **on demand, per execution**, in the release namespace — so they compete with the platform's own pods for any namespace `ResourceQuota`. Plan the quota headroom explicitly: * **Sandbox pod size** is set by the executor, with `requests == limits`: `MAX_FUNCTION_MEMORY` (MiB, default `512`) and `MAX_FUNCTION_CPU` (cores, default `1.0`), plus an ephemeral-storage limit from `MAX_FUNCTION_STORAGE`. Pass them via the chart's `extraEnv` to shrink each execution's footprint, e.g. `MAX_FUNCTION_MEMORY=192`, `MAX_FUNCTION_CPU=0.5`. * **Concurrency ceiling** ≈ what's left of the quota after the platform pods, divided by one sandbox pod's request. Example: with a `requests.memory: 3Gi` quota and the default chart components requesting \~1.4Gi, a 192Mi sandbox pod allows \~8 concurrent executions; a 512Mi one allows \~3. Check `requests.cpu` and the `pods:` count the same way — the binding constraint is whichever runs out first. * **Failure mode:** when the quota is exhausted, pod creation is rejected and the execution fails with a quota error — reduce sandbox size, raise the quota, or lower worker concurrency (`queueWorker`/`queueAgent` values) to bound simultaneous executions. * **Trusted (`inprocess`) code has no per-function memory cap** — it runs inside the queue-worker process, so the worker's `resources.limits.memory` *is* the tenant's burst envelope for `shared_pool` functions (worker baseline + concurrency × per-call allocation). Size it accordingly. * **Datastores** (postgres, redis, pgbouncer) set no container resources in the chart; on clusters with a `LimitRange` they inherit its defaults, which count against the quota too. Give them explicit values in your own manifest patches if you need deterministic accounting. The chart's per-component `resources` are values — override any of them per deployment. ## Compact profile (density deployments) For packing several small instances onto one box (e.g. **3 instances on a 4GB node**), drop ClickHouse and size requests near measured idle usage (backend idles \~175MB, workers \~90–120MB): ```yaml theme={null} # compact-values.yaml — ≈650–750MB per instance at idle clickhouse: enabled: false # no ClickHouse pod; request/execution log UI stays empty backend: resources: requests: { cpu: "200m", memory: "224Mi" } limits: { cpu: "1", memory: "768Mi" } queueWorker: replicas: 1 concurrency: 4 # bounds simultaneous executions (and sandbox pods) resources: requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "500m", memory: "512Mi" } # = shared_pool burst envelope queueAgent: replicas: 1 concurrency: 2 resources: requests: { cpu: "100m", memory: "160Mi" } limits: { cpu: "500m", memory: "512Mi" } extraEnv: - name: MAX_FUNCTION_MEMORY # sandbox pod request=limit, MiB value: "192" - name: MAX_FUNCTION_CPU value: "0.5" ``` The math for 3 × on a 4GB node: 3 instances × \~700MB ≈ 2.1GB, k3s host overhead \~0.7GB → \~2.8GB idle, leaving \~700MB shared burst headroom — enough for a few concurrent 192Mi sandbox pods across tenants. Keep `queueWorker.concurrency` low so tenants can't burst past it, or set `SANDBOX_EXECUTOR=disabled` for trusted-only tiers. With `clickhouse.enabled: false` the backend detects the empty `CLICKHOUSE_HOST` and disables logging cleanly (no reconnect attempts); the Logs pages return empty results. ## Key values | Value | Default | Description | | -------------------------------- | ------------- | ------------------------------------------------------------------ | | `domain` | — | Hostname for console + API (one shared hostname) | | `ingress.enabled` | `true` | Create a standard `Ingress`; disable to bring your own routing | | `ingress.className` | — | e.g. `nginx`, `traefik`, `alb` | | `executor.sandbox` | `k8s_pod` | Sandbox executor; `disabled` for trusted-only deployments | | `executor.trusted` | `inprocess` | Trusted executor | | `executor.image` | ghcr executor | Image for sandbox pods | | `executor.installDependencies` | `true` | Per-pod `pip install` (set `false` with a baked image) | | `registry.username/password` | — | Pull secret for private registries (also attached to sandbox pods) | | `fileStorage.storageClass` | *(emptyDir)* | RWX storage class for shared file storage on multi-node clusters | | `networkPolicy.enabled` | `true` | Namespace isolation + sandbox lockdown | | `networkPolicy.ingressNamespace` | *(any)* | Restrict which namespace may reach backend/console | ## Limitations * `input()` / human-in-the-loop pauses are unavailable: sandbox pods are single-use (sandbox mode has always rejected `input()`), and the `inprocess` trusted executor is run-to-completion. Durable pause/resume is tracked in [issue #79](https://github.com/sinas-platform/sinas/issues/79). * The bundled datastores are convenience-grade. For production, point `DATABASE_URL`-family settings at managed services (e.g. RDS, ElastiCache) and disable the bundled StatefulSets. # Introduction Source: https://docs.sinas.co/index AI agent platform for building intelligent applications Sinas is a platform for building AI-powered applications. It brings together multi-provider LLM agents, serverless Python functions, persistent state, database querying, file storage, and template rendering — all behind a single API with role-based access control. **What you can do with Sinas:** * **Build AI agents** with configurable LLM providers (OpenAI, Anthropic, Mistral, Ollama), tool calling, streaming responses, and agent-to-agent orchestration. * **Run Python functions** in isolated Docker containers, triggered by agents, webhooks, cron schedules, or the API. * **Store and retrieve state** across conversations with namespace-based access control. * **Query external databases** (PostgreSQL, ClickHouse, Snowflake) through saved SQL templates that agents can use as tools. * **Manage files** with versioning, metadata validation, and upload processing hooks. * **Render templates** using Jinja2 for emails, notifications, and dynamic content. * **Define everything in YAML** for GitOps workflows with idempotent apply, change detection, and dry-run. Sinas runs as a set of Docker services: the API server, queue workers (for functions and agents), a scheduler, PostgreSQL, PgBouncer, Redis, ClickHouse (optional for request logging), and a web console. # API Overview Source: https://docs.sinas.co/platform/api-overview Runtime, management, and adapter APIs Sinas has two API layers: ## Runtime API (`/`) The runtime API is mounted at the root. It handles authentication, chat, execution, state, file operations, and discovery. These are the endpoints your applications and end users interact with. ``` /auth/... # Authentication /agents/... # Create chats with agents /chats/... # Send messages, manage chats /functions/... # Execute functions (sync and async) /queries/... # Execute database queries /webhooks/... # Trigger webhook-linked functions /executions/... # View execution history and results /jobs/... # Check job status /states/... # Key-value state storage /files/... # Upload, download, search files /templates/... # Render and send templates /components/... # Render components, proxy endpoints /manifests/... # Manifest status validation /discovery/... # List resources visible to the current user ``` ## Management API (`/api/v1/`) The management API handles CRUD operations on all resources. These are typically used by admins, the console UI, and configuration tools. ``` /api/v1/agents/... # Agent CRUD /api/v1/functions/... # Function CRUD /api/v1/skills/... # Skill CRUD /api/v1/llm-providers/... # LLM provider management (admin) /api/v1/database-connections/... # DB connection management (admin) /api/v1/queries/... # Query CRUD /api/v1/collections/... # Collection CRUD /api/v1/templates/... # Template CRUD /api/v1/webhooks/... # Webhook CRUD /api/v1/schedules/... # Schedule CRUD /api/v1/components/... # Component CRUD + compilation + share links /api/v1/manifests/... # Manifest CRUD /api/v1/roles/... # Role & permission management /api/v1/users/... # User management /api/v1/api-keys/... # API key management /api/v1/dependencies/... # Python dependency approval (admin) /api/v1/packages/... # Integration package management /api/v1/workers/... # Worker management (admin) /api/v1/containers/... # Container pool management (admin) /api/v1/config/... # Declarative config apply/validate/export (admin) /api/v1/queue/... # Queue stats, job list, DLQ, cancel (admin) /api/v1/request-logs/... # Request log search (admin) ``` ## OpenAI SDK Adapter (`/adapters/openai`) An OpenAI SDK-compatible API that maps to Sinas agents and LLM providers. Point any OpenAI SDK client at this endpoint to use Sinas agents. ``` POST /adapters/openai/v1/chat/completions # Chat completion (maps to agent or direct LLM) GET /adapters/openai/v1/models # List available models (agents + provider models) GET /adapters/openai/v1/models/{model_id} # Get model info ``` Agents are listed as models with names like `agent:namespace/name`. Provider models are listed with their provider prefix. ## Interactive API Docs Swagger UI is available at `/docs` (runtime API), `/api/v1/docs` (management API), and `/adapters/openai/docs` (OpenAI adapter) for exploring all endpoints and schemas interactively. ## Discovery Endpoints The discovery API returns resources visible to the current user, optionally filtered by app context: ``` GET /discovery/agents # Agents the user can chat with GET /discovery/functions # Functions the user can see GET /discovery/skills # Skills the user can see GET /discovery/collections # Collections the user can access GET /discovery/templates # Templates the user can use ``` Pass an app context via the `X-Application` header or `?app=namespace/name` query parameter to filter results to a specific app's exposed namespaces. *** # Async jobs Source: https://docs.sinas.co/platform/async-jobs Queue functions for background execution and receive a callback when they finish Use the async runtime API when you want Sinas to run a function on behalf of a user *without* blocking your app's request. Submit the job, get an `execution_id` back, and either poll for status or have Sinas POST the result to a URL you provide. This is the right pattern when an app — like a custom UI or another service — needs to fan out work for a logged-in user. Every job is attributed to the user whose bearer token kicked it off, so audit trails remain intact. ## When to use it * **Bulk workloads** — ingestion runs, batch enrichment, anything that fans into N function calls. * **Long jobs** — a function that legitimately runs minutes or longer; the client doesn't want to hold an open HTTP connection. * **Cross-app workflows** — a function on Sinas that calls back into your app's API as the same user (Sinas-issued tokens are accepted by any app that validates via Sinas auth). For sync execution where you want the result inline, use `POST /functions/{ns}/{name}/execute` instead. ## Endpoint ```http theme={null} POST /functions/{namespace}/{name}/execute/async Authorization: Bearer Content-Type: application/json { "input": { ... }, "trigger_id": "my-app:run:01HX...", "delay_seconds": 0, "callback_url": "https://my-app.example.com/sinas/callbacks/01HX..." } ``` | Field | Type | Required | Notes | | --------------- | ------- | -------- | ---------------------------------------------------------------------------------------- | | `input` | object | yes | Validated against the function's `input_schema`. | | `trigger_id` | string | no | App-supplied correlation id stored on the execution record. Defaults to `"runtime-api"`. | | `delay_seconds` | integer | no | Delay before the worker picks up the job. `null` = enqueue immediately. | | `callback_url` | string | no | HTTPS URL Sinas POSTs to once the execution terminates. See [Callbacks](#callbacks). | **Auth:** the bearer's `sub` *is* the job's user. There's no `user_id` override and no "act-as-user" mode — every queued job is attributable to the human (or human-equivalent account) whose token initiated it. Audit-log integrity depends on this. **Permission:** reuses the existing function-execute permissions — `sinas.functions/{ns}/{name}.execute:own` (or `:all`). No extra permission gates the queue path; anything you can execute synchronously, you can enqueue. ### Response ```http theme={null} 202 Accepted { "execution_id": "exec_01HX...", "status": "queued" } ``` Poll execution state via `GET /executions/{execution_id}` (returns the current `status`, `output_data`, `error`, etc.). ## Callbacks If `callback_url` is set, Sinas fires a **single** HTTP POST to that URL when the execution terminates (success, failure, or timeout). Fire-and-forget — no retries. ```http theme={null} POST Authorization: Bearer Content-Type: application/json { "execution_id": "exec_01HX...", "trigger_id": "my-app:run:01HX...", "function": { "namespace": "myapp", "name": "ingest_document" }, "status": "success", // "success" | "failure" "started_at": "2026-05-14T13:00:00Z", "finished_at": "2026-05-14T13:08:23Z", "duration_ms": 503000, "result": { ... }, // present on success "error": null // present on failure } ``` The callback carries a fresh Sinas-signed access token for the originating user. Apps validate it through the same auth path they already use for inbound function calls (Python SDK: `SinasAuth`; JS SDK: `client.auth.getMe()`). ### Delivery semantics * **Fire-and-forget**: one POST attempt with a 10s timeout. No retry, no DLQ. * **No backpressure**: Sinas does not wait for callback success before marking the execution complete. * **Resilience fallback**: if the callback fails to deliver, poll `GET /executions/{execution_id}` to reconcile. The execution row records `callback_status` (`sent` / `failed`) and `callback_response_code` for operator visibility. ### Allowlist policy Callback hosts are gated by the `CALLBACK_URL_HOSTS` environment variable: | Value | Meaning | | ------------------------- | ---------------------------------------------------------------------- | | unset / empty | Callbacks **disabled**. Any `callback_url` in a request returns `400`. | | `*` | Permissive — any HTTPS URL accepted (subject to SSRF guards). | | comma-separated host list | Exact-host allowlist. | SSRF guards always apply regardless of mode: the URL must use `https://` and resolve to a non-private address. Managed-service operators typically deploy with an explicit allowlist; self-hosted single-tenant operators flip to `*` once. ## SDK helpers ### Python ```python theme={null} from sinas import SinasClient client = SinasClient(base_url="https://sinas.example.com", token=user_bearer) result = client.functions.enqueue( namespace="myapp", name="ingest_document", input={"doc_id": "01HX..."}, trigger_id=f"myapp:run:{run_id}", callback_url=f"https://myapp.example.com/sinas/callbacks/{run_id}", ) # result == {"execution_id": "...", "status": "queued"} ``` ### JavaScript ```ts theme={null} import { SinasClient, enqueueFunction } from '@sinas/sdk'; const client = new SinasClient({ baseUrl: 'https://sinas.example.com', getAccessToken: () => userAccessToken, }); const { executionId } = await enqueueFunction( client, 'myapp/ingest_document', { doc_id: '01HX...' }, { triggerId: `myapp:run:${runId}`, callbackUrl: `https://myapp.example.com/sinas/callbacks/${runId}`, }, ); ``` ## Worked example Your app submits 50 documents for ingestion on behalf of a signed-in user. 1. The app's backend, holding the user's bearer, calls `client.functions.enqueue(...)` once per document. 2. Each `execution_id` lands in a queue. A Sinas worker dequeues and runs `myapp/ingest_document` under the user's identity. 3. The function calls the app's `/api/v1/documents/{id}/mark-processed` with the per-execution access token. The app's `SinasAuth` validates the token via Sinas's `/auth/me`, applies the user's permissions, persists the update. 4. When the function finishes, Sinas POSTs the result to `callback_url`. The app updates the run's progress and notifies the user. If `ingest_document` invokes a Sinas agent (`client.chats.invoke(...)`), the agent runs in-process under the same user; sub-agent fan-out is internal and doesn't re-cross the app boundary, so the function token's TTL never bites. ## Batches Submitting N executions one at a time works, but for genuine bulk workloads (50+ runs at once) you'd rather submit them as a single unit, store one id, and poll one aggregate. The `batches` API does that — for both functions and agents. ### Submit — function batch ```http theme={null} POST /functions/{namespace}/{name}/execute/batch Authorization: Bearer { "inputs": [ {"doc_id": "1"}, {"doc_id": "2"} ], "trigger_id_prefix": "myapp:run:01HX", // optional; per-child trigger_id becomes "{prefix}:{i}" "delay_seconds": 0, // optional "callback_url": "https://app/cb/per-exec", // optional, fires per child "batch_callback_url": "https://app/cb/batch" // optional, fires once when batch terminates } → 202 { "batch_id": "...", "execution_ids": ["...", "..."], "total": 2, "status": "queued" } ``` ### Submit — agent batch ```http theme={null} POST /agents/{namespace}/{name}/chats/batch Authorization: Bearer { "inputs": [ { "input_variables": {"company": "Acme"}, "message": "Synthesize..." }, { "input_variables": {"company": "Beta"}, "message": "Synthesize..." } ], "trigger_id_prefix": "myapp:syn:01HX", "callback_url": "...", "batch_callback_url": "..." } → 202 { "batch_id": "...", "execution_ids": [...], "chat_ids": [...], "total": 2, "status": "queued" } ``` Each input creates a fresh chat with `agent.initial_messages` pre-populated (templated with `input_variables`) followed by `message`. Agent batches have an **approval policy**: if a child agent tries to call a tool that requires approval, the execution is marked `failed` with an error — bulk agent runs must use agents whose enabled tools don't require approval. Per-execution callback `result` for agent batches: ```json theme={null} "result": { "chat_id": "...", "final_message": "the assistant's last message text", "final_message_role": "assistant", "tool_calls": [...] } ``` Full transcript fetchable via `GET /chats/{chat_id}`. ### Poll a batch ```http theme={null} GET /batches/{batch_id} → 200 { "batch_id": "...", "kind": "function", // or "agent" "target": {"namespace": "myapp", "name": "ingest_document"}, "user_id": "...", "total": 50, "completed": 32, "failed": 1, "running": 5, "queued": 12, "cancelled": 0, "status": "running", // queued | running | completed | failed | partial | cancelled "started_at": "...", "finished_at": null, "trigger_id_prefix": "myapp:run:01HX" } ``` Terminal `status` values: * `completed` — all children completed successfully * `partial` — all children terminal, ≥1 failed * `failed` — all children failed * `cancelled` — batch cancelled before all children terminated ### Drill in ```http theme={null} GET /batches/{batch_id}/executions?status=failed&limit=50 → 200 { "executions": [...] } ``` ### Cancel ```http theme={null} POST /batches/{batch_id}/cancel → 200 { "batch_id": "...", "status": "cancelled", "cancelled_children": 12 } ``` Children with status `pending` or `awaiting_input` become `cancelled`. Running children must complete naturally; their results still count toward the batch. ### Batch callback When the last child terminates, Sinas POSTs the batch summary once to `batch_callback_url`: ```json theme={null} { "batch_id": "...", "kind": "function", "target": {"namespace": "myapp", "name": "ingest_document"}, "status": "partial", "total": 50, "completed": 49, "failed": 1, "cancelled": 0, "started_at": "...", "finished_at": "...", "trigger_id_prefix": "myapp:run:01HX" } ``` Same auth / SSRF / fire-and-forget semantics as the per-execution callback. Per-execution callbacks (`callback_url`) and the batch callback are independent — both can be set, both fire. ### Limits * `MAX_BATCH_SIZE` (env) caps how many `inputs` a single batch can have (default `1000`). * Single-target batches only: every child in a batch hits the same function (or agent). For mixed targets, submit multiple batches. ### SDK helpers **Python:** ```python theme={null} from sinas import SinasClient client = SinasClient(base_url=..., token=user_bearer) # Function batch batch = client.functions.submit_batch( namespace="myapp", name="ingest_document", inputs=[{"doc_id": d.id} for d in docs], trigger_id_prefix=f"myapp:run:{run.id}", batch_callback_url=f"https://myapp.example.com/sinas/batch/{run.id}", ) # Agent batch batch = client.agents.submit_batch( namespace="myapp", name="synthesize", inputs=[ {"input_variables": {"company": c.name}, "message": f"Synthesize for {c.name}"} for c in companies ], batch_callback_url="...", ) # Poll status = client.batches.get(batch["batch_id"]) print(f"{status['completed']} / {status['total']}") # Drill into failures failed = client.batches.list_executions(batch["batch_id"], status="FAILED") ``` **JavaScript:** ```ts theme={null} import { SinasClient, submitFunctionBatch, submitAgentBatch, getBatch, } from '@sinas/sdk'; const client = new SinasClient({ baseUrl, getAccessToken: () => userToken }); const batch = await submitFunctionBatch( client, 'myapp/ingest_document', docs.map((d) => ({ doc_id: d.id })), { triggerIdPrefix: `myapp:run:${runId}`, batchCallbackUrl: `https://myapp.example.com/sinas/batch/${runId}`, }, ); const status = await getBatch(client, batch.batchId); console.log(`${status.completed} / ${status.total}`); ``` ## Related * [Functions](/build-resources/functions) — defining the code that runs. * [API Overview](/platform/api-overview) — runtime vs. management surfaces. * [RBAC](/platform/rbac) — how `sinas.functions/{ns}/{name}.execute` permissions resolve. # Authentication Source: https://docs.sinas.co/platform/authentication Auth modes, login flows, and API keys Sinas supports three authentication modes for end-user login, plus API keys for programmatic access. The mode is set per-deployment via the `AUTH_MODE` environment variable and cannot be changed at runtime. ## Auth Modes | Mode | What's required to log in | When to use | | --------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `otp` (default) | Email + 6-digit OTP | Default. Proves email control on every login. Requires SMTP. | | `password` | Email + password | Airgapped or no-SMTP deployments. No email round-trip. | | `password+otp` | Email + password + OTP | Most secure. Treats the OTP as a liveness check on the email account — useful when you need to lock out ex-employees by revoking their work mailbox. Requires SMTP. | Clients (custom frontends, the JS/Python SDKs, the official console) discover the active mode via the unauthenticated `GET /info` endpoint and branch their login UI accordingly. ## Login Flows ### `otp` 1. Client posts email to `POST /auth/login` 2. Sinas sends a 6-digit code (valid 10 min by default) 3. Client posts code to `POST /auth/verify-otp` and receives access + refresh tokens ### `password` 1. Client posts email + password to `POST /auth/login` 2. Sinas verifies and immediately returns access + refresh tokens ### `password+otp` 1. Client posts email + password to `POST /auth/login` 2. On password match, Sinas sends a 6-digit OTP and returns an OTP session id 3. Client posts code to `POST /auth/verify-otp` and receives access + refresh tokens ## Tokens All modes issue the same JWT pair: * **Access token** — short-lived (default 15 min), sent as `Authorization: Bearer ` * **Refresh token** — long-lived (default 30 days), exchanged at `POST /auth/refresh` ``` POST /auth/login # Start login (mode-dependent payload) POST /auth/verify-otp # Verify OTP for otp / password+otp modes POST /auth/refresh # Get new access token POST /auth/logout # Revoke refresh token GET /auth/me # Get current user info ``` ## Superadmin Bootstrap The first admin user is seeded from environment variables on backend startup: * `SUPERADMIN_EMAIL` — email of the user to create / promote to Admins * `SUPERADMIN_PASSWORD` — only used when `AUTH_MODE` is `password` or `password+otp`. Setting this on a running deployment and restarting the backend will reset the password — the escape hatch for "admin lost their password." ## Password Reset For modes that include passwords: * **User-initiated** — not implemented when `AUTH_MODE=password` (no email channel guaranteed). Use admin reset. * **Admin-initiated** — an admin generates a one-time reset link from the user management page in the console and delivers it out-of-band (Slack, in person). * **Lost-superadmin escape hatch** — set `SUPERADMIN_PASSWORD` in the env and restart the backend. ## API Keys For programmatic access (scripts, CI/CD, integrations), create API keys instead of using short-lived JWT tokens. Each key has its own set of permissions (a subset of the creating user's permissions). API keys work identically across all auth modes. ``` POST /api/v1/api-keys # Create key (plaintext returned once) GET /api/v1/api-keys # List keys GET /api/v1/api-keys/{id} # Get key details DELETE /api/v1/api-keys/{id} # Revoke key ``` API keys can be used via `Authorization: Bearer ` or `X-API-Key: ` headers. Keys can have optional expiration dates. ## Token Exchange (bring your own auth) If your application already authenticates users itself, you don't need to run a second Sinas login flow. Your backend exchanges its knowledge of "who is logged in" for Sinas tokens: 1. An admin links your application's user ids to Sinas users as [external identities](/admin/users-roles#external-identities) — or you let the exchange auto-provision them. 2. Your backend calls the exchange endpoint with an API key holding the `sinas.auth.exchange:all` permission: ```json theme={null} POST /auth/token/exchange X-API-Key: { "provider": "acme-app", "subject": "usr_8f3a2", "email": "jane@acme.com", "metadata": {"name": "Jane"}, "custom_fields": {"plan": "enterprise"}, "auto_provision": true } ``` 3. The response contains a normal Sinas access + refresh token pair for that user, which your frontend uses against the runtime API. All ownership scoping (`:own`), permissions, and audit logging apply as if the user had logged in directly. Behavior details: * The user is resolved by `(provider, subject)`. If unknown and `email` is given, an existing user with that email is matched and the identity is linked to them. * With `auto_provision: true`, unknown users are created and assigned the `TOKEN_EXCHANGE_DEFAULT_ROLE` (default `GuestUsers`). `email` is required to provision. * `metadata` is stored on the identity and refreshed on every exchange. `custom_fields` is shallow-merged into the user's custom fields (partner keys win, admin-set keys survive). * `provisioned: true` in the response indicates the exchange created the user. The exchange endpoint trusts the caller to assert identities — treat keys with `sinas.auth.exchange:all` like any other admin credential. *** # Role-Based Access Control Source: https://docs.sinas.co/platform/rbac Roles, permissions, and agent execution ## Overview Users are assigned to **roles**, and roles define **permissions**. A user's effective permissions are the union of all permissions from all their roles (OR logic). Permissions are loaded from the database on every request — changes take effect immediately. ## Default Roles | Role | Description | | -------------- | --------------------------------------------------------------------------- | | **Admins** | Full access to everything (`sinas.*:all`) | | **Users** | Create and manage own resources, chat with any agent, execute own functions | | **GuestUsers** | Read and update own profile only | ## Permission Format ``` .[/].: ``` **Components:** | Part | Description | Examples | | ------------ | ---------------------------- | ------------------------------------------------------- | | **Service** | Top-level namespace | `sinas`, or a custom prefix like `titan`, `acme` | | **Resource** | Resource type | `agents`, `functions`, `states`, `users` | | **Path** | Optional namespace/name path | `/marketing/send_email`, `/*` | | **Action** | What operation is allowed | `create`, `read`, `update`, `delete`, `execute`, `chat` | | **Scope** | Ownership scope | `:own` (user's resources), `:all` (all resources) | ## Permission Matching Rules **Scope hierarchy:** `:all` automatically grants `:own`. A user with `sinas.agents.read:all` passes any check for `sinas.agents.read:own`. **Wildcards** can be used at any level: | Pattern | Matches | | ----------------------------------------- | ---------------------------------------------------- | | `sinas.*:all` | Everything in Sinas (admin access) | | `sinas.agents/*/*.chat:all` | Chat with any agent in any namespace | | `sinas.functions/marketing/*.execute:own` | Execute any function in the `marketing` namespace | | `sinas.states/*.read:own` | Read own states in any namespace | | `sinas.chats.*:own` | All chat actions (read, update, delete) on own chats | **Namespaced resource permissions** use slashes in the resource path: ``` sinas.agents/support/ticket-bot.chat:own # Chat with specific agent sinas.functions/*/send_email.execute:own # Execute send_email in any namespace sinas.states/api_keys.read:all # Read all shared states in api_keys namespace ``` **Non-namespaced resource permissions** use simple dot notation: ``` sinas.webhooks.create:own # Create webhooks sinas.schedules.read:own # Read own schedules sinas.users.update:own # Update own profile ``` ## Custom Permissions The permission system is not limited to `sinas.*`. You can define permissions with any service prefix for your own applications: ``` titan.student_profile.read:own titan.courses/math/*.enroll:own acme.billing.invoices.read:all myapp.*:all ``` These work identically to built-in permissions — same wildcard matching, same scope hierarchy. This lets you use Sinas as the authorization backend for external applications. ## Checking Permissions from External Services Use the `POST /auth/check-permissions` endpoint to verify whether the current user (identified by their Bearer token or API key) has specific permissions: ```bash theme={null} curl -X POST https://yourdomain.com/auth/check-permissions \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "permissions": ["titan.student_profile.read:own", "titan.courses.enroll:own"], "logic": "AND" }' ``` Response: ```json theme={null} { "result": true, "logic": "AND", "checks": [ {"permission": "titan.student_profile.read:own", "has_permission": true}, {"permission": "titan.courses.enroll:own", "has_permission": true} ] } ``` * **`logic: "AND"`** — User must have ALL listed permissions (default) * **`logic: "OR"`** — User must have AT LEAST ONE of the listed permissions This makes Sinas usable as a centralized authorization service for any number of external applications. ## Managing Roles ``` POST /api/v1/roles # Create role GET /api/v1/roles # List roles GET /api/v1/roles/{name} # Get role details PATCH /api/v1/roles/{name} # Update role DELETE /api/v1/roles/{name} # Delete role POST /api/v1/roles/{name}/members # Add user to role DELETE /api/v1/roles/{name}/members/{id} # Remove user from role POST /api/v1/roles/{name}/permissions # Set permission DELETE /api/v1/roles/{name}/permissions # Remove permission GET /api/v1/permissions/reference # List all known permissions ``` ## Agent Execution & Permissions Agents define which tools are available (via `enabled_*` fields), but the user's role permissions are checked at execution time. Both conditions must be met: 1. **Agent declares the tool** — the resource is in the agent's `enabledFunctions`, `enabledQueries`, `enabledStores`, `enabledCollections`, or `enabledAgents` 2. **User has the permission** — checked when the tool is actually called If the user lacks permission, the tool call returns an error that the LLM can explain to the user. Tools are still visible to the LLM regardless — the check happens at execution, not discovery. | Resource | Agent config | Permission checked at execution | | ----------- | -------------------- | --------------------------------------------------------------------------- | | Functions | `enabledFunctions` | `sinas.functions/{ns}/{name}.execute:own` | | Queries | `enabledQueries` | `sinas.queries/{ns}/{name}.execute:own` | | Collections | `enabledCollections` | `sinas.collections/{ns}/{name}.download:own` (read) / `.upload:own` (write) | | Stores | `enabledStores` | `sinas.stores/{ns}/{name}.read_state:own` / `.write_state:own` | | Sub-agents | `enabledAgents` | `sinas.agents/{ns}/{name}.chat:all` | | Connectors | `enabledConnectors` | Agent-gated only (no standalone execution path) | **Connectors** are the exception — they have no independent API and can only be accessed through an agent, so the agent's `enabledConnectors` (with per-operation filtering) is the sole access control. **Namespace wildcards** make this manageable. Grant `sinas.functions/sales/*.execute:own` once on a role, and all functions in the `sales/` namespace are accessible. *** # Collections & Files Source: https://docs.sinas.co/storage/collections File storage with versioning and metadata Collections are containers for file uploads with versioning, metadata validation, and processing hooks. **Collection properties:** | Property | Description | | ------------------------- | ---------------------------------------------------- | | `namespace` / `name` | Unique identifier | | `metadata_schema` | JSON Schema that file metadata must conform to | | `content_filter_function` | Function that runs on upload to approve/reject files | | `post_upload_function` | Function that runs after upload for processing | | `max_file_size_mb` | Per-file size limit (default: 100 MB) | | `max_total_size_gb` | Total collection size limit (default: 10 GB) | **File features:** * **Versioning** — Every upload creates a new version. Previous versions are preserved. * **Metadata** — Each file carries JSON metadata validated against the collection's schema. * **Visibility** — Files can be `private` (owner only) or `shared` (users with collection `:all` access). * **Content filtering** — Optional function runs on upload that can approve, reject, or modify the file. **Management endpoints:** ``` POST /api/v1/collections # Create collection GET /api/v1/collections # List collections GET /api/v1/collections/{namespace}/{name} # Get collection PUT /api/v1/collections/{namespace}/{name} # Update DELETE /api/v1/collections/{namespace}/{name} # Delete (cascades to files) ``` **Runtime file endpoints:** ``` POST /files/{namespace}/{collection} # Upload file GET /files/{namespace}/{collection} # List files GET /files/{namespace}/{collection}/{filename} # Download file PATCH /files/{namespace}/{collection}/{filename} # Update metadata DELETE /files/{namespace}/{collection}/{filename} # Delete file POST /files/{namespace}/{collection}/{filename}/url # Generate temporary download URL POST /files/{namespace}/{collection}/search # Search files ``` # States Source: https://docs.sinas.co/storage/states Key-value stores with encryption and visibility States are a persistent key-value store organized by namespace. Agents use states to maintain memory and context across conversations. **Key properties:** | Property | Description | | ----------------- | --------------------------------------------------------------------------- | | `namespace` | Organizational grouping (e.g., `preferences`, `memory`, `api_keys`) | | `key` | Unique key within user + namespace | | `value` | Any JSON data | | `visibility` | `private` (owner only) or `shared` (users with namespace `:all` permission) | | `description` | Optional description | | `tags` | Tags for filtering and search | | `relevance_score` | Priority for context retrieval (0.0–1.0, default: 1.0) | | `encrypted` | If `true`, value is encrypted at rest with Fernet and decrypted on read | | `expires_at` | Optional expiration time | **Encrypted states:** Set `encrypted: true` when creating or updating a state to store the value encrypted. The plaintext value is stored in `encrypted_value` (Fernet-encrypted) while `value` is set to `{}`. On read, the value is transparently decrypted. This is useful for storing API keys, tokens, or other secrets. **Agent state access** is declared per agent via `enabled_stores`: ```yaml theme={null} enabledStores: - store: "shared_knowledge/main" access: readonly - store: "conversation_memory/main" access: readwrite ``` Read-only stores give the agent a `retrieve_context` tool. Read-write stores additionally provide `save_context`, `update_context`, and `delete_context`. **Endpoints:** ``` POST /states # Create state entry GET /states # List (supports namespace, visibility, tags, search filters) GET /states/{id} # Get state PUT /states/{id} # Update state DELETE /states/{id} # Delete state ``` *** # Database Triggers Source: https://docs.sinas.co/triggers/database-triggers Change data capture with polling Database triggers watch external database tables for changes and automatically execute functions when new or updated rows are detected. This is poll-based Change Data Capture — no setup required on the source database. **How it works:** 1. A separate CDC service polls the configured table at a fixed interval 2. It queries rows where `poll_column > last_bookmark` (e.g., `updated_at > '2026-03-01T10:00:00'`) 3. If new rows are found, they are batched into a single function call 4. The bookmark advances to the highest value in the batch 5. On first activation, the bookmark is set to `MAX(poll_column)` — no backfill of existing data **Key properties:** | Property | Description | | -------------------------------------- | --------------------------------------------------------------------------- | | `name` | Unique trigger name (per user) | | `database_connection_id` | Which database connection to poll | | `schema_name` | Database schema (default: `public`) | | `table_name` | Table to watch | | `operations` | `["INSERT"]`, `["UPDATE"]`, or both | | `function_namespace` / `function_name` | Function to execute when changes are detected | | `poll_column` | Monotonically increasing column used as bookmark (e.g., `updated_at`, `id`) | | `poll_interval_seconds` | How often to poll (1–3600, default: `10`) | | `batch_size` | Max rows per poll (1–10000, default: `100`) | | `is_active` | Enable/disable without deleting | | `last_poll_value` | Current bookmark (managed automatically) | | `error_message` | Last error, if any (visible in UI) | The `poll_column` must be a column whose value only increases — timestamps, auto-increment IDs, or sequences. The column type is detected automatically and comparisons are cast to the correct type. **Function input payload:** When changes are detected, the target function receives all new rows in a single call: ```json theme={null} { "table": "public.orders", "operation": "CHANGE", "rows": [ {"id": 123, "status": "paid", "amount": 99.50, "updated_at": "2026-03-02T10:30:00Z"}, {"id": 124, "status": "pending", "amount": 45.00, "updated_at": "2026-03-02T10:30:01Z"} ], "poll_column": "updated_at", "count": 2, "timestamp": "2026-03-02T10:30:05Z" } ``` If a poll returns zero rows, no function call is made. **Error handling:** On failure, the trigger logs the error to `error_message` and retries with exponential backoff (up to 60 seconds). The trigger continues retrying until deactivated or the issue is resolved. **Limitations:** * Cannot detect `DELETE` operations (poll-based limitation) * Changes are detected with a delay equal to the poll interval * The `poll_column` must never decrease — resetting it will cause missed or duplicate rows **Endpoints:** ``` POST /api/v1/database-triggers # Create trigger GET /api/v1/database-triggers # List triggers GET /api/v1/database-triggers/{name} # Get trigger PATCH /api/v1/database-triggers/{name} # Update trigger DELETE /api/v1/database-triggers/{name} # Delete trigger ``` **Declarative configuration:** ```yaml theme={null} databaseTriggers: - name: "customer_changes" connectionName: "prod_database" tableName: "customers" operations: ["INSERT", "UPDATE"] functionName: "sync/process_customer" pollColumn: "updated_at" pollIntervalSeconds: 10 batchSize: 100 ``` *** # Schedules Source: https://docs.sinas.co/triggers/schedules Cron-based scheduled jobs Schedules trigger functions or agents on a cron timer. **Key properties:** | Property | Description | | ---------------------------------- | -------------------------------------------------- | | `name` | Unique name (per user) | | `schedule_type` | `function` or `agent` | | `target_namespace` / `target_name` | Function or agent to trigger | | `cron_expression` | Standard cron expression (e.g., `0 9 * * MON-FRI`) | | `timezone` | Schedule timezone (default: `UTC`) | | `input_data` | Input passed to the function or agent | | `content` | Message content (agent schedules only) | For agent schedules, a new chat is created for each run with the schedule name and timestamp as the title. **Endpoints:** ``` POST /api/v1/schedules # Create schedule GET /api/v1/schedules # List schedules GET /api/v1/schedules/{name} # Get schedule PATCH /api/v1/schedules/{name} # Update schedule DELETE /api/v1/schedules/{name} # Delete schedule ``` # Webhooks Source: https://docs.sinas.co/triggers/webhooks Inbound HTTP webhook handlers Webhooks expose functions as HTTP endpoints. When a request arrives at a webhook path, Sinas executes the linked function with the request data. **Key properties:** | Property | Description | | -------------------------------------- | -------------------------------------------------------------------- | | `path` | URL path (e.g., `stripe/payment-webhook`) | | `http_method` | GET, POST, PUT, DELETE, or PATCH | | `function_namespace` / `function_name` | Target function | | `requires_auth` | Whether the caller must provide a Bearer token | | `default_values` | Default parameters merged with request data (request takes priority) | **How input is extracted:** * `POST`/`PUT`/`PATCH` with JSON body → body becomes the input * `GET` → query parameters become the input * Default values are merged underneath (request data overrides) **Example:** ```bash theme={null} # Create a webhook curl -X POST https://yourdomain.com/api/v1/webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "path": "stripe/payment", "function_namespace": "payments", "function_name": "process_webhook", "http_method": "POST", "requires_auth": false, "default_values": {"source": "stripe"} }' # Trigger it curl -X POST https://yourdomain.com/webhooks/stripe/payment \ -H "Content-Type: application/json" \ -d '{"event": "charge.succeeded", "amount": 1000}' # Function receives: {"source": "stripe", "event": "charge.succeeded", "amount": 1000} ``` **Endpoints:** ``` POST /api/v1/webhooks # Create webhook GET /api/v1/webhooks # List webhooks GET /api/v1/webhooks/{path} # Get webhook PATCH /api/v1/webhooks/{path} # Update webhook DELETE /api/v1/webhooks/{path} # Delete webhook ```