# ───────────────────────────────────────────────────────────────────────────── # Nasiko — server environment reference # # cp server/.env.example server/.env # then edit # just run # sources server/.env # # This is the COMPLETE list of environment variables the OSS server reads. # The root `.env.example` is the shorter docker-compose quick start; this file # is the annotated reference for every knob, and the one `just run` sources. # # NEVER commit server/.env (it is gitignored). # ───────────────────────────────────────────────────────────────────────────── # # HOW TO READ THIS FILE # • REQUIRED → the server exits at startup if missing or invalid. # • Everything else is commented out and shows its built-in default, so an # uncommented line is always a deliberate override. # • Values shown as `(unset)` have no default — the feature is off until set. # # Boot requires DATABASE_URL, REDIS_URL, SECRETS_ENCRYPTION_KEY, JWT_SECRET # and S3_SECRET_KEY (sections 1 and 4). Bootstrap login additionally needs # ADMIN_USERNAME and ADMIN_PASSWORD. The supplied values are for local demos; # replace the encryption/signing keys and admin password before first boot. # Features have additional requirements: local coding-agent routing needs # AGENT_JWT_SECRET (section 6), and telemetry needs section 12 configured. # Docker Compose supplies the infrastructure/telemetry URLs automatically. # ═══════════════════════════════════════════════════════════════════════════ # 1) REQUIRED # ═══════════════════════════════════════════════════════════════════════════ # --- Connection URLs --------------------------------------------------------- # REQUIRED. Under docker compose these are overridden with service names. DATABASE_URL=postgres://nasiko:nasiko@localhost:5432/nasiko_dev REDIS_URL=redis://localhost:6379 # --- Security keys ----------------------------------------------------------- # REQUIRED. AES-256-GCM key encrypting agent secrets at rest. Must be exactly # 32 bytes, base64-encoded — the server validates this at startup and refuses # to boot on a malformed key. Generate one with: openssl rand -base64 32 SECRETS_ENCRYPTION_KEY=kmd9Ic4cgD9YYE/owDL3QSzpKI/BZ3pmVZQ67X+eABc= # REQUIRED. Signs user session JWTs. openssl rand -base64 48 JWT_SECRET=dev-secret-change-me # --- Bootstrap admin --------------------------------------------------------- # Needed for first-login bootstrap, not a startup validation requirement. # Both must be set for the server to create the admin account. ADMIN_PASSWORD=changeme ADMIN_USERNAME=admin # ═══════════════════════════════════════════════════════════════════════════ # 2) SERVER & NETWORK # ═══════════════════════════════════════════════════════════════════════════ # CP_BIND=0.0.0.0:8080 # CP_DOMAIN= # (unset) public hostname, for URL derivation # APP_BASE_URL= # ("") where OAuth flows redirect back to # CORS_ALLOWED_ORIGINS= # ("") comma-separated; empty = same-origin only RUST_LOG=info # tracing filter # ═══════════════════════════════════════════════════════════════════════════ # 3) AGENT RUNTIME # ═══════════════════════════════════════════════════════════════════════════ # How deployed agents are run: # local → processes on the host (source/dev, no Docker needed) # docker → containers via the Docker socket # simulated → deploys nothing; every agent resolves to SIM_AGENT_URL. Lets you # run the control plane with no container runtime at all. AGENT_RUNTIME=local # Endpoint every agent resolves to under AGENT_RUNTIME=simulated. # SIM_AGENT_URL=http://localhost:8000 # Docker network the server joins to reach agent containers by name. Leave # unset when the server runs on the host; set it when the server is itself a # container (docker compose sets this for you). # DOCKER_AGENT_NETWORK= # (unset) # AGENT_DEFAULT_MEMORY=1Gi # AGENT_MEMORY_VOLUME=nasiko-agent-memory # backs `--writable` /workspace # AGENT_MEMORY_INIT_IMAGE=alpine:3.21 # seeds per-agent /workspace subdirs # Space-separated container images auto-deployed at boot. Read ONCE at startup; # changing it needs a restart. Each image is copied into the embedded registry # first and deployed from there, so later pulls never leave the server. SEED_AGENTS= # SEED_IMAGE_PLATFORM=linux/amd64 # os/arch copied from multi-platform seed images # SEED_FORCE_PULL= # (unset) set to any value to re-pull seed images # How often container-uptime rows are polled for FinOps. # CONTAINER_HOURS_POLL_SECS=60 # ═══════════════════════════════════════════════════════════════════════════ # 4) OBJECT STORAGE & THE EMBEDDED OCI REGISTRY # ═══════════════════════════════════════════════════════════════════════════ S3_ENDPOINT=http://localhost:9000 # any S3-compatible endpoint S3_BUCKET=nasiko S3_ACCESS_KEY=nasiko S3_SECRET_KEY=nasiko123 # REQUIRED S3_REGION=us-east-1 # S3_FORCE_PATH_STYLE= # (auto) force path-style addressing # Where the free-assessment form (POST /api/assessments) is forwarded: the Nasiko # assessment intake. Unset → the route answers 503 and the form falls back to its # copy-your-answers step. # ASSESSMENT_INTAKE_URL=https://yelsilh253pidzyo7chd2tb3mu0ngwmf.lambda-url.us-west-2.on.aws/ # Bucket holding OCI blobs/manifests for the embedded registry at /v2/*. # OCI_STORAGE_BUCKET=nasiko-artifacts # OCI_REGISTRY_HOST= # (unset) overrides auto-detection # Registry that built agent images are pushed to and pulled from. Empty uses # the server's own embedded registry. # AGENT_IMAGE_REGISTRY= # ("") # Credential for the ONE external registry seed images (SEED_AGENTS) come # from. AGENT_REGISTRY_HOST is the bare host image refs start with (ghcr.io, # us-central1-docker.pkg.dev, ...); unset = Docker Hub. Sent only to that host. # AGENT_REGISTRY_USERNAME= # (unset) # AGENT_REGISTRY_PASSWORD= # (unset) # AGENT_REGISTRY_HOST= # (unset) Docker Hub # Shared cluster-wide credential build tooling uses to push to /v2/* as # `Basic build-service:`. Empty disables that credential type. # BUILD_PUSH_TOKEN= # ("") # Parallel server-side builds. Clamped to 1-16. # BUILD_CONCURRENCY=4 # ═══════════════════════════════════════════════════════════════════════════ # 5) LLM — routing engine, orchestrator, and deployed agents # ═══════════════════════════════════════════════════════════════════════════ # Provider key for the routing engine and the LLM gateway's platform fallback. # Gateway-wired agents receive an identity JWT rather than the provider key. # A custom LLM config can use its own stored provider credentials instead; # it still requires AGENT_JWT_SECRET for local coding-agent routing (section 6). # OPENAI_API_KEY=sk-your-openai-api-key # OPENAI_BASE_URL= # (unset) OpenAI-compatible endpoint # OPENAI_BASE_URL is for the routing engine/direct-provider path; the gateway's # built-in OpenAI provider uses OPENAI_API_BASE (section 6). Custom providers # registered through the API/UI use their stored endpoints and credentials. # Optional service for decomposing a workflow instruction into steps. # Missing MODEL_API_URL makes that operation return 503, not server startup fail. # MODEL_APIKEY is only needed if the decomposer service requires authentication. # MODEL_API_URL= # (unset) # MODEL_APIKEY= # (unset) # OPENAI_MODEL=gpt-4o-mini # default model for agents # ROUTER_MODEL=gpt-4o-mini # stage-3 agent selection # EMBEDDING_MODEL=text-embedding-3-small # stages 1-2 shortlist/rerank # CAPABILITY_GENERATOR_MODEL=gpt-4o-mini # generates agent capability summaries # MCP_DESCRIPTION_MODEL=gpt-4o-mini # MCP tool-description fallback # Routing engine shape: stage 1 is skipped below the threshold; stage 1 returns # the top SIZE candidates. # ROUTER_SHORTLIST_THRESHOLD=15 # ROUTER_SHORTLIST_SIZE=10 # ═══════════════════════════════════════════════════════════════════════════ # 6) LLM ROUTER — built into the server; routes agents' own LLM calls # ═══════════════════════════════════════════════════════════════════════════ # Base URL deployed agents use to reach the router. For a host-run server with # Docker agents, use http://host.docker.internal:8080 where supported, not # localhost (which points to the agent container itself). # Compose supplies http://server:8080. Local coding clients instead use their # CLI cluster URL, e.g. nasiko connect http://localhost:8080. # LLM_GATEWAY_BASE_URL= # ("") # Bind address for the OPTIONAL standalone `llm-router` binary. Ignored by the # server, which mounts the router in-process on CP_BIND. # LLM_ROUTER_BIND=0.0.0.0:8081 # REQUIRED for local coding-agent routing (nasiko connect claude|codex|opencode # --config ), including custom configs. Generate independently of JWT_SECRET: # openssl rand -base64 48. Keep it server-side, never in the coding-agent config. # Empty: /api/agents/{id}/llm-token returns 503; gateway auth fails closed. # The server still boots and container deployment skips gateway wiring. # AGENT_JWT_SECRET= # ("") # AGENT_JWT_ALGORITHM=HS256 # Fallback when an agent has no llm_config row. # DEFAULT_PROVIDER=openai # DEFAULT_MODEL=gpt-4o-mini # Platform-owned provider keys. Each falls back to the bare provider name # (PLATFORM_OPENAI_API_KEY, else OPENAI_API_KEY). # PLATFORM_OPENAI_API_KEY= # PLATFORM_ANTHROPIC_API_KEY= # PLATFORM_GEMINI_API_KEY= # PLATFORM_OPENROUTER_API_KEY= # Provider base URLs (override for self-hosted gateways or tests). # OPENAI_API_BASE=https://api.openai.com/v1 # ANTHROPIC_API_BASE=https://api.anthropic.com/v1 # GEMINI_API_BASE=https://generativelanguage.googleapis.com/v1beta # OPENROUTER_API_BASE=https://openrouter.ai/api/v1 # Optional OpenRouter attribution headers — affect openrouter.ai app rankings # only, harmless to leave empty. # OPENROUTER_HTTP_REFERER= # OPENROUTER_X_TITLE= # LLM_CONFIG_CACHE_TTL=30 # per-agent llm_config cache, seconds # ROUTER_DECISION_TTL_SECS=3600 # conversation model-stickiness window # Max age of a running flow that may still authorize an LLM call. Defaults to # NASIKO_FLOW_TIMEOUT_SECS — never set it BELOW that value, or a turn still # inside its budget takes a 403 part-way through. See section 9. # LLM_ATTRIBUTION_WINDOW_SECS= # (defaults to the flow timeout) # Background catalog/price syncs. Both reach the network at boot; turn them off # for air-gapped installs. # MODEL_CATALOG_SYNC_ENABLED=true # MODEL_CATALOG_SYNC_INTERVAL_SECS=86400 # MODEL_PRICING_SYNC_ENABLED=true # PRICING_SYNC_INTERVAL_SECS=86400 # PORTKEY_PRICING_BASE_URL= # (built-in) price-book source override # OPENROUTER_MODELS_URL= # (built-in) model-catalog source override # Salience gate — decides which turns are worth a larger model. # SALIENCE_GATE_ENABLED=true # SALIENCE_LOW_THRESHOLD=0.20 # SALIENCE_HIGH_THRESHOLD=0.80 # SALIENCE_WEIGHTS_PATH= # ("") external weights file # ═══════════════════════════════════════════════════════════════════════════ # 7) TOKEN COMPRESSION — shrinks payloads before they reach a model # ═══════════════════════════════════════════════════════════════════════════ # Router-side compression of provider-bound content. # TOKEN_COMPRESS_ENABLED=true # master kill switch # TOKEN_COMPRESS_MIN_BYTES=2048 # TOKEN_COMPRESS_TYPES=json,log,diff # prose/code/html/markup/text also valid # TOKEN_COMPRESS_LEVEL=conservative # conservative | balanced | aggressive # TOKEN_COMPRESS_DRY_RUN=false # measure only, change nothing # TOKEN_BREVITY=true # strip redundant whitespace/framing # TOKEN_BREVITY_MIN_BYTES=0 # Keeps the pre-compression original recoverable for this long. # TOKEN_COMPRESS_RECOVERY=true # TOKEN_COMPRESS_RECOVERY_MIN_BYTES=8192 # TOKEN_COMPRESS_RECOVERY_TTL_SECS=86400 # Orchestrator-side compression (tool results and replayed history). # TOKEN_COMPRESS_TOOL_RESULTS=true # TOKEN_COMPRESS_TOOL_RESULTS_MIN_BYTES=2048 # TOKEN_COMPRESS_HISTORY=true # TOKEN_COMPRESS_HISTORY_MIN_BYTES=2048 # ═══════════════════════════════════════════════════════════════════════════ # 8) CHAT CONTEXT — how much conversation history is replayed # ═══════════════════════════════════════════════════════════════════════════ # Each user picks a strategy (pacms|topk|lastk) and a tier (low|medium|high) # with `nasiko context-strategy set` / `nasiko budget set`. These keys decide # what those tiers are WORTH in this deployment; users cannot change them. # # `pacms` reads the tier as a token budget: # PACMS_BUDGET_LOW=500 # PACMS_BUDGET_MEDIUM=1000 # PACMS_BUDGET_HIGH=5000 # # `topk`/`lastk` read the same tier as an item count: # CONTEXT_K_LOW=1 # CONTEXT_K_MEDIUM=5 # CONTEXT_K_HIGH=20 # # Candidate window both embedding-backed strategies draw from, and how many # recent messages `pacms` always keeps regardless of score. # PACMS_HISTORY_POOL_SIZE=150 # PACMS_HISTORY_MANDATORY_RECENT=3 # ═══════════════════════════════════════════════════════════════════════════ # 9) FLOW GUARD & TIMEOUTS — the limits that stop runaway agent loops # ═══════════════════════════════════════════════════════════════════════════ # Redis-backed, keyed by the W3C traceparent trace id. Fails CLOSED: if Redis # is unreachable, inter-agent calls are rejected rather than waved through. # NASIKO_FLOW_MAX_DEPTH=5 # max call depth per flow # NASIKO_FLOW_MAX_FAN_OUT=20 # max agent invocations per flow # NASIKO_FLOW_MAX_TOKENS=100000 # token budget per flow # The platform's widest window. The MCP gateway and the LLM router both refuse # to serve a flow older than this, so nothing an agent turn depends on may # outlive it. # NASIKO_FLOW_TIMEOUT_SECS=600 # Redis TTL for the flow's guard state. Defaults to the flow timeout + 60s and # must ALWAYS exceed it: the guard reads `started_at` out of this key, so a # shorter TTL expires the state the timeout check depends on — the check then # passes silently and the depth/fan-out counters reset mid-flow. # NASIKO_FLOW_STATE_TTL_SECS= # (flow timeout + 60) # Budget for one agent hop: the A2A proxy, the orchestrator's streaming and # non-streaming agent calls, and the MAF executor's. Keep at or below # NASIKO_FLOW_TIMEOUT_SECS. Accepts the former name ROUTER_AGENT_TIMEOUT_SECS. # AGENT_CALL_TIMEOUT_SECS=600 # ═══════════════════════════════════════════════════════════════════════════ # 10) HUMAN-IN-THE-LOOP # ═══════════════════════════════════════════════════════════════════════════ # How long a pending hitl_requests row stays answerable. # HITL_REQUEST_TTL_DAYS=7 # Resume dispatcher: poll cadence, stuck-claim recovery sweep, claim lease, # retry ceiling, and the delay between attempts. # HITL_RESUME_POLL_INTERVAL_SECS=5 # HITL_RESUME_RECOVERY_INTERVAL_SECS=600 # HITL_RESUME_LEASE_MINUTES=16 # HITL_RESUME_MAX_ATTEMPTS=3 # HITL_RESUME_RETRY_DELAY_SECS=2 # Re-sends of a resume the agent answered "task not found" before the row is recorded as # agent_lost_task (a round-robin Service may reach a replica that never saw the task). # HITL_LOST_TASK_RETRIES=3 # Retry ceiling for a MAF step. # MAF_MAX_ATTEMPTS=3 # ═══════════════════════════════════════════════════════════════════════════ # 11) MCP GATEWAY — merged tool access for agents # ═══════════════════════════════════════════════════════════════════════════ # Composio integration. Unset = Composio disabled; generic and uploaded MCP # servers still work without it. # COMPOSIO_API_KEY=ak_your-composio-api-key # COMPOSIO_BASE_URL=https://backend.composio.dev # COMPOSIO_WEBHOOK_SECRET= # (unset) unset skips signature checks — dev only # Comma-separated Composio toolkits auto-registered at boot. These ship with # curated display metadata: # slack gmail github notion jira linear asana trello discord dropbox # salesforce hubspot zendesk intercom stripe airtable clickup monday # gitlab pagerduty sentry confluence bitbucket googlecalendar googledrive # googlesheets # NO-OP without COMPOSIO_API_KEY — setting this alone logs a warning at boot # and does nothing further. # SEED_TOOLKITS= # ("") # Public URL of the gateway, injected into every agent as MCP_GATEWAY_URL. # For a host-run server with Docker-based agents: # MCP_GATEWAY_PUBLIC_URL=http://host.docker.internal:8080/api/mcp # # Agents also receive MCP_GATEWAY_TOKEN (their own credential) and # MCP_GATEWAY_CONNECT_URL — the same endpoint with that credential in the path, # for MCP clients that accept only a URL and cannot set an Authorization # header. Prefer the header form where the client supports it. # OAuth redirect bases, opened in the USER'S OWN BROWSER. # MCP_OAUTH_REDIRECT_BASE_URL=http://localhost:8080/api/mcp # COMPOSIO_CALLBACK_BASE_URL=http://localhost:8080/api/mcp # Redis-cached TTLs (seconds). # MCP_SESSION_TTL_SECONDS=300 # MCP_PERM_CACHE_TTL_SECONDS=30 # MCP_MANIFEST_TTL_SECONDS=300 # MCP_TOOLCOUNT_TTL_SECONDS=3600 # "Upload your own MCP server" build/deploy settings. # MCP_UPLOAD_MAX_BYTES=52428800 # 50 MiB # MCP_GATEWAY_MAX_BODY_BYTES=8388608 # 8 MiB — request body cap on the agent-facing /api/mcp routes # MCP_UPLOAD_DEFAULT_PORT=8080 # port the uploaded container must bind via $PORT # MCP_SERVERS_NETWORK=nasiko-mcp-servers-net # Query-aware tool discovery: how tools are searched, how many are returned, # and the cap on meta-tool results. # MCP_TOOL_SEARCH_MODE=semantic # semantic | bm25 # MCP_TOOL_SEARCH_TOOL_LIMIT=15 # MCP_TOOL_SEARCH_META_LIMIT=10 # DEV/TEST ONLY. Disables the SSRF guard that blocks private/loopback connector # URLs. Never set this in a deployment that accepts untrusted connector URLs. # MCP_ALLOW_PRIVATE_URLS= # (unset = guard active) # Gateway's own sentence advertised ahead of every connector's harvested # initialize.instructions. Unset/blank = a built-in generic orientation sentence. # MCP_GATEWAY_INSTRUCTIONS= # HMAC key the gateway signs the x-nasiko-identity header with, when forwarding # a caller's (agent_id, user_id, flow_id) to a system backend (e.g. the # workspace server). Unset/blank falls back to JWT_SECRET. # MCP_IDENTITY_SIGNING_KEY= # ═══════════════════════════════════════════════════════════════════════════ # 12) OBSERVABILITY # ═══════════════════════════════════════════════════════════════════════════ # Trace and log backends. The observability API turns itself on only when BOTH # are set to a non-empty value. # TEMPO_URL=http://localhost:3200 # LOKI_URL=http://localhost:3100 # OTLP endpoint the SERVER exports its own spans to. # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # OTEL_EXPORTER_OTLP_PROTOCOL=grpc # grpc (4317) | http/protobuf (4318) # OTEL_EXPORTER_OTLP_HEADERS= # (unset) e.g. auth headers for a hosted collector # OTLP endpoint injected into AGENT CONTAINERS — must be reachable from inside # a container, which is usually a different address than the one above. # OTEL_COLLECTOR_ENDPOINT=http://otel-collector:4317 # OTEL_SERVICE_NAME=nasiko-cp # OTEL_TRACES_SAMPLER_ARG=1.0 # 1.0 = sample everything # OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true # capture prompts/completions # Base OTLP/HTTP JSON endpoint for the durable coding-agent outbox. The server # appends /v1/traces and /v1/logs. Unset leaves receipts pending with no worker. # CODING_AGENT_OTLP_ENDPOINT=http://localhost:4318 # Backfill of token usage from trace spans: cadence, re-read overlap that # tolerates late-arriving spans, and rows per batch. # TRACE_USAGE_SYNC_SECS=120 # TRACE_USAGE_OVERLAP_SECS=600 # TRACE_USAGE_BATCH_SIZE=50 # ═══════════════════════════════════════════════════════════════════════════ # 13) GITHUB OAUTH — optional; enables import-from-GitHub / clone flows # ═══════════════════════════════════════════════════════════════════════════ # Create an OAuth App at https://github.com/settings/developers. # GITHUB_CLIENT_ID=your-github-oauth-client-id # GITHUB_CLIENT_SECRET=your-github-oauth-client-secret # GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback # Shared OAuth relay used as the redirect_uri instead of GITHUB_CALLBACK_URL, # so many clusters can share one GitHub OAuth app. # GITHUB_CENTRAL_CALLBACK_URL= # (unset) # Signs the OAuth `state` parameter. Defaults to GITHUB_CLIENT_SECRET. # OAUTH_STATE_SIGNING_KEY= # (GitHub client secret) # Clone limits for source deploys. # GITHUB_CLONE_TIMEOUT_SECS=300 # GITHUB_CLONE_MAX_SIZE_MB=500 # ═══════════════════════════════════════════════════════════════════════════ # 14) NETWORK ALLOWLISTS & DISCOVERY # ═══════════════════════════════════════════════════════════════════════════ # Git hosts accepted for source-clone deploys. # GIT_CLONE_ALLOWED_HOSTS=github.com,gitlab.com,bitbucket.org # OCI registry hosts accepted by /api/catalog/import/registry. # EMPTY REJECTS ALL registry imports — the safe default. # REGISTRY_IMPORT_ALLOWED_HOSTS= # ("") # Where agents publish/discover A2A agent cards. Unset derives it from this # server's own /a2a/v1 endpoint. # A2A_DISCOVERY_URL= # (unset) # ═══════════════════════════════════════════════════════════════════════════ # 15) AGENT STATE - durable tasks and key-value state held for agents # ═══════════════════════════════════════════════════════════════════════════ # How long a stored task or key-value row lives after its last write (1-365). # Reads do not extend it; expired rows read as absent and are swept every 10 minutes. # Keep AGENT_STATE_TTL_DAYS at least HITL_REQUEST_TTL_DAYS (the server warns at startup # otherwise), or a task paused for a human can lose its state before the human answers. # AGENT_STATE_TTL_DAYS=7 # Largest task body or value an agent may store, counted on the bytes as sent. # Writes over it get a 413. # AGENT_STATE_MAX_BODY_BYTES=1048576 # Requests one agent may make to /api/agent-state per minute, for each of its ready replicas # (an agent running three replicas may make three times this). Over it gets a 429. # Counted in memory by each server process, as the other rate limits are. One network # address may make ten times this before any of its tokens is looked up. # AGENT_STATE_RATE_LIMIT_PER_MINUTE=1200 # Most body bytes one listing page carries. A page stops before the next row would pass it # and returns a cursor, so short pages are normal. # AGENT_STATE_LIST_BYTES_BUDGET=8388608