1 line
108 KiB
JSON
1 line
108 KiB
JSON
{"repo": "chagel/metis", "files": {"config/initializers/agent.rb": {"c1a6c18a91fa3aa4936ee5321dca88bb53279c39": {"t": 1785745745.3243275, "n": 11681, "h": "20e2c1fac2021e43", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\n# :microsandbox — pi inside a self-hosted libkrun microVM (in-process,\n# no daemon or cloud API). VM-grade isolation on the worker's\n# own hardware; requires Linux with KVM or macOS on Apple\n# Silicon, the optional `microsandbox` bundler group, and an\n# OCI image with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Idle window after which EvictDockerWorkspacesJob warm-evicts a\n# host-bind-mount conversation's persistent workspace/ — both runtimes that\n# keep the scope on the host (docker, microsandbox); sessions/ is kept, so pi\n# still resumes with --continue (see docs/session-persistence.md). In-flight\n# turns and active workflow runs are never evicted. Garbage or a non-positive\n# value fails boot: a silently defaulted cleanup window is dangerous.\ndocker_eviction_hours = Integer(ENV.fetch(\"METIS_DOCKER_WORKSPACE_EVICTION_HOURS\", \"72\"))\nraise \"METIS_DOCKER_WORKSPACE_EVICTION_HOURS must be positive\" unless docker_eviction_hours.positive?\nRails.application.config.x.agent.docker_workspace_eviction_window = docker_eviction_hours.hours\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# OCI image used by the :microsandbox runtime — pi baked in. The docker:image\n# build satisfies the contract, but microsandbox pulls from OCI registries (it\n# cannot see a local Docker daemon's store), so push the image somewhere the\n# worker can pull it from and name it here.\nRails.application.config.x.agent.microsandbox_image =\n ENV[\"METIS_MICROSANDBOX_IMAGE\"].presence || \"metis-pi\"\n\n# Optional registry credentials for that pull — a private registry, or lifting\n# Docker Hub's anonymous rate limit. Unset, an existing `docker login`\n# (~/.docker/config.json) is still honored.\nRails.application.config.x.agent.microsandbox_registry_username =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_USERNAME\"].presence\nRails.application.config.x.agent.microsandbox_registry_password =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_PASSWORD\"].presence\n\n# Guest-write budget (MiB) on the bind-mounted conversation scope. Unset uses\n# the runtime's default (4 GiB); raise it when working trees outgrow that.\n# Parsed strictly and failed at boot rather than coerced: `to_i` turns garbage\n# into 0, which reaches the mount spec as a zero-byte write budget.\nmicrosandbox_quota_mib = ENV[\"METIS_MICROSANDBOX_WORKSPACE_QUOTA_MIB\"].presence\nif microsandbox_quota_mib\n microsandbox_quota_mib = Integer(microsandbox_quota_mib)\n raise \"METIS_MICROSANDBOX_WORKSPACE_QUOTA_MIB must be positive\" unless microsandbox_quota_mib.positive?\nend\nRails.application.config.x.agent.microsandbox_workspace_quota_mib = microsandbox_quota_mib\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"moonshotai\" => { label: \"Moonshot AI\", env: \"MOONSHOT_API_KEY\" },\n \"moonshotai-cn\" => { label: \"Moonshot AI (CN)\", env: \"MOONSHOT_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "2b975223c5f45c30ce00a3e0d783da51b725716c": {"t": 1785756114.8253, "n": 11681, "h": "20e2c1fac2021e43", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\n# :microsandbox — pi inside a self-hosted libkrun microVM (in-process,\n# no daemon or cloud API). VM-grade isolation on the worker's\n# own hardware; requires Linux with KVM or macOS on Apple\n# Silicon, the optional `microsandbox` bundler group, and an\n# OCI image with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Idle window after which EvictDockerWorkspacesJob warm-evicts a\n# host-bind-mount conversation's persistent workspace/ — both runtimes that\n# keep the scope on the host (docker, microsandbox); sessions/ is kept, so pi\n# still resumes with --continue (see docs/session-persistence.md). In-flight\n# turns and active workflow runs are never evicted. Garbage or a non-positive\n# value fails boot: a silently defaulted cleanup window is dangerous.\ndocker_eviction_hours = Integer(ENV.fetch(\"METIS_DOCKER_WORKSPACE_EVICTION_HOURS\", \"72\"))\nraise \"METIS_DOCKER_WORKSPACE_EVICTION_HOURS must be positive\" unless docker_eviction_hours.positive?\nRails.application.config.x.agent.docker_workspace_eviction_window = docker_eviction_hours.hours\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# OCI image used by the :microsandbox runtime — pi baked in. The docker:image\n# build satisfies the contract, but microsandbox pulls from OCI registries (it\n# cannot see a local Docker daemon's store), so push the image somewhere the\n# worker can pull it from and name it here.\nRails.application.config.x.agent.microsandbox_image =\n ENV[\"METIS_MICROSANDBOX_IMAGE\"].presence || \"metis-pi\"\n\n# Optional registry credentials for that pull — a private registry, or lifting\n# Docker Hub's anonymous rate limit. Unset, an existing `docker login`\n# (~/.docker/config.json) is still honored.\nRails.application.config.x.agent.microsandbox_registry_username =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_USERNAME\"].presence\nRails.application.config.x.agent.microsandbox_registry_password =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_PASSWORD\"].presence\n\n# Guest-write budget (MiB) on the bind-mounted conversation scope. Unset uses\n# the runtime's default (4 GiB); raise it when working trees outgrow that.\n# Parsed strictly and failed at boot rather than coerced: `to_i` turns garbage\n# into 0, which reaches the mount spec as a zero-byte write budget.\nmicrosandbox_quota_mib = ENV[\"METIS_MICROSANDBOX_WORKSPACE_QUOTA_MIB\"].presence\nif microsandbox_quota_mib\n microsandbox_quota_mib = Integer(microsandbox_quota_mib)\n raise \"METIS_MICROSANDBOX_WORKSPACE_QUOTA_MIB must be positive\" unless microsandbox_quota_mib.positive?\nend\nRails.application.config.x.agent.microsandbox_workspace_quota_mib = microsandbox_quota_mib\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"moonshotai\" => { label: \"Moonshot AI\", env: \"MOONSHOT_API_KEY\" },\n \"moonshotai-cn\" => { label: \"Moonshot AI (CN)\", env: \"MOONSHOT_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "4702a7e73ad60acc044c5c82ae46e9f8105773dc": {"t": 1785756116.3461714, "n": 11212, "h": "d5de7df1a1746baa", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\n# :microsandbox — pi inside a self-hosted libkrun microVM (in-process,\n# no daemon or cloud API). VM-grade isolation on the worker's\n# own hardware; requires Linux with KVM or macOS on Apple\n# Silicon, the optional `microsandbox` bundler group, and an\n# OCI image with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Idle window after which EvictDockerWorkspacesJob warm-evicts a Docker\n# conversation's persistent workspace/ (sessions/ kept — pi still resumes\n# with --continue; see docs/session-persistence.md). In-flight turns and\n# active workflow runs are never evicted. Garbage or a non-positive value\n# fails boot: a silently defaulted cleanup window is dangerous.\ndocker_eviction_hours = Integer(ENV.fetch(\"METIS_DOCKER_WORKSPACE_EVICTION_HOURS\", \"72\"))\nraise \"METIS_DOCKER_WORKSPACE_EVICTION_HOURS must be positive\" unless docker_eviction_hours.positive?\nRails.application.config.x.agent.docker_workspace_eviction_window = docker_eviction_hours.hours\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# OCI image used by the :microsandbox runtime — pi baked in. The docker:image\n# build satisfies the contract, but microsandbox pulls from OCI registries (it\n# cannot see a local Docker daemon's store), so push the image somewhere the\n# worker can pull it from and name it here.\nRails.application.config.x.agent.microsandbox_image =\n ENV[\"METIS_MICROSANDBOX_IMAGE\"].presence || \"metis-pi\"\n\n# Optional registry credentials for that pull — a private registry, or lifting\n# Docker Hub's anonymous rate limit. Unset, an existing `docker login`\n# (~/.docker/config.json) is still honored.\nRails.application.config.x.agent.microsandbox_registry_username =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_USERNAME\"].presence\nRails.application.config.x.agent.microsandbox_registry_password =\n ENV[\"METIS_MICROSANDBOX_REGISTRY_PASSWORD\"].presence\n\n# Guest-write budget (MiB) on the bind-mounted conversation scope. Unset uses\n# the runtime's default (4 GiB); raise it when working trees outgrow that.\nRails.application.config.x.agent.microsandbox_workspace_quota_mib =\n ENV[\"METIS_MICROSANDBOX_WORKSPACE_QUOTA_MIB\"].presence&.to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"moonshotai\" => { label: \"Moonshot AI\", env: \"MOONSHOT_API_KEY\" },\n \"moonshotai-cn\" => { label: \"Moonshot AI (CN)\", env: \"MOONSHOT_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "2e458673cc0cc58a30ac4baf06845cdf2dc13ce0": {"t": 1785756117.5694737, "n": 9768, "h": "226a6cc850643c55", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Idle window after which EvictDockerWorkspacesJob warm-evicts a Docker\n# conversation's persistent workspace/ (sessions/ kept — pi still resumes\n# with --continue; see docs/session-persistence.md). In-flight turns and\n# active workflow runs are never evicted. Garbage or a non-positive value\n# fails boot: a silently defaulted cleanup window is dangerous.\ndocker_eviction_hours = Integer(ENV.fetch(\"METIS_DOCKER_WORKSPACE_EVICTION_HOURS\", \"72\"))\nraise \"METIS_DOCKER_WORKSPACE_EVICTION_HOURS must be positive\" unless docker_eviction_hours.positive?\nRails.application.config.x.agent.docker_workspace_eviction_window = docker_eviction_hours.hours\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"moonshotai\" => { label: \"Moonshot AI\", env: \"MOONSHOT_API_KEY\" },\n \"moonshotai-cn\" => { label: \"Moonshot AI (CN)\", env: \"MOONSHOT_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "b91168415371e2f39b4c9035498611b106dd028a": {"t": 1785756119.5081646, "n": 9615, "h": "ad5df234c56378e2", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Idle window after which EvictDockerWorkspacesJob warm-evicts a Docker\n# conversation's persistent workspace/ (sessions/ kept — pi still resumes\n# with --continue; see docs/session-persistence.md). In-flight turns and\n# active workflow runs are never evicted. Garbage or a non-positive value\n# fails boot: a silently defaulted cleanup window is dangerous.\ndocker_eviction_hours = Integer(ENV.fetch(\"METIS_DOCKER_WORKSPACE_EVICTION_HOURS\", \"72\"))\nraise \"METIS_DOCKER_WORKSPACE_EVICTION_HOURS must be positive\" unless docker_eviction_hours.positive?\nRails.application.config.x.agent.docker_workspace_eviction_window = docker_eviction_hours.hours\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "605a2ba688c662182622794365a44c4aa47da540": {"t": 1785756121.1561987, "n": 8969, "h": "0a8acc8c294e9365", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n\n# Web-search backend for the agent's `web_search` tool (the web-tools pi\n# extension). A shared, deployment-level resource — no per-user keys. The\n# extension picks the first configured provider (Serper > Brave > SearXNG),\n# and falls back to keyless DuckDuckGo when none is set. DuckDuckGo\n# rate-limits datacenter IPs, so configure Serper (https://serper.dev) or\n# Brave (https://brave.com/search/api/) for reliable search from the sandbox\n# runtimes. Plumbed into the sandbox by Agent::Runtime::Base#sandbox_env.\nRails.application.config.x.agent.serper_api_key = ENV[\"SERPER_API_KEY\"].presence\nRails.application.config.x.agent.brave_search_api_key = ENV[\"BRAVE_SEARCH_API_KEY\"].presence\nRails.application.config.x.agent.searxng_url = ENV[\"SEARXNG_URL\"].presence\n"}, "d3accf07487a4cd12b589385295afd9c2b35767e": {"t": 1785756122.357064, "n": 8203, "h": "7df8ec690d462452", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# OCI runtime for the :docker runtime's containers. Unset uses the daemon\n# default (runc) — namespace isolation over a shared host kernel. Set to\n# \"runsc\" (gVisor) for a user-space kernel that intercepts syscalls,\n# closing the kernel-escape gap that separates a plain container from a\n# microVM — the isolation tier Metis wants for untrusted multi-tenant\n# turns, at near-native local cost. Requires the runtime installed and\n# registered on the host daemon. See docs/coding-runtime.md.\nRails.application.config.x.agent.docker_runtime =\n ENV[\"METIS_DOCKER_RUNTIME\"].presence\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n"}, "6cc5d84e97a4779aed9f488197d0a09f208bf1a0": {"t": 1785756123.9243274, "n": 7623, "h": "4508eca533daf60f", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\n# :daytona— pi inside a Daytona elastic sandbox. The Daytona-backed\n# isolated runtime; requires DAYTONA_API_KEY and a snapshot\n# with pi baked in (see the daytona:snapshot rake task).\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# Daytona credentials and target for the :daytona runtime. The API key is a\n# shared, deployment-level resource (no per-user keys). api_url/target are\n# optional — unset uses the SDK defaults (https://app.daytona.io/api, the\n# org's default region).\nRails.application.config.x.agent.daytona_api_key = ENV[\"DAYTONA_API_KEY\"].presence\nRails.application.config.x.agent.daytona_api_url = ENV[\"DAYTONA_API_URL\"].presence\nRails.application.config.x.agent.daytona_target = ENV[\"DAYTONA_TARGET\"].presence\n\n# Daytona snapshot (image) used by the :daytona runtime — should have pi\n# installed. See the daytona:snapshot rake task for the build definition.\nRails.application.config.x.agent.daytona_snapshot =\n ENV.fetch(\"METIS_DAYTONA_SNAPSHOT\", \"metis-pi\")\n\n# Idle-lifecycle intervals (minutes) passed to Daytona at sandbox create.\n# Unlike E2B, where a suspended sandbox is free, Daytona keeps billing a\n# STOPPED sandbox for disk storage; ARCHIVED moves the filesystem to cheap\n# object storage (lower cost, slower to resume). So these intervals are cost\n# control, not just cleanup — and they replace E2B's EvictPausedSandboxesJob\n# (no Daytona eviction cron). A deleted sandbox's next turn provisions fresh\n# (working tree gone, history intact). 0 disables an interval.\n#\n# auto_stop — a *crash* safety net only. Runtime::Daytona already stops\n# the sandbox at end of turn (that is what ends compute\n# billing); autoStop just catches one left RUNNING by a killed\n# worker. It auto-stops on SDK inactivity, and a long\n# autonomous turn produces none (log streaming rides the\n# preview proxy, which Daytona excludes), so this MUST exceed\n# the longest turn — keep it ≥ METIS_STALLED_TURN_MINUTES or a\n# live turn gets stopped mid-flight.\n# auto_archive — how long a stopped (idle) sandbox waits before its\n# filesystem moves to cheap storage. Lower = less disk cost,\n# but a returning conversation pays a slower cold resume.\n# auto_delete — how long before an idle sandbox is reaped entirely.\nRails.application.config.x.agent.daytona_auto_stop_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_STOP_MINUTES\", \"120\").to_i\nRails.application.config.x.agent.daytona_auto_archive_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_ARCHIVE_MINUTES\", \"60\").to_i\nRails.application.config.x.agent.daytona_auto_delete_minutes =\n ENV.fetch(\"METIS_DAYTONA_AUTO_DELETE_MINUTES\", \"1440\").to_i\n\n# Keep-warm window (seconds). After a turn, Runtime::Daytona schedules the\n# sandbox stop this many seconds out (DaytonaStopJob) instead of stopping\n# inline — so the stop never holds the worker, and a follow-up within the\n# window reuses the still-running box (no stop, no resume). The trade-off is\n# idle compute billing for the window; autoStop (above) is the backstop if the\n# job never runs. 0 = stop as soon as the job runs (async, no warm window).\nRails.application.config.x.agent.daytona_keep_warm_seconds =\n ENV.fetch(\"METIS_DAYTONA_KEEP_WARM_SECONDS\", \"120\").to_i\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n"}, "234864e8e0317122bec7929265f4746ee8e3144c": {"t": 1785756125.1510043, "n": 4316, "h": "54f177fb90261a14", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# How long an assistant turn may sit pending/streaming before\n# ReapStalledTurnsJob treats it as abandoned — its agent process died\n# mid-turn (a worker restart, an OOM, a dev code-reload on the in-process\n# :async adapter), so ChatJob's rescue/ensure never marked it errored and\n# the UI shows \"Working…\" forever. Generous on purpose: there is no\n# per-turn heartbeat, so the window must sit comfortably above the longest\n# real turn or a still-running turn would be reaped (and its freed\n# composer would admit a concurrent turn against the live process).\nRails.application.config.x.agent.stalled_turn_window =\n ENV.fetch(\"METIS_STALLED_TURN_MINUTES\", \"120\").to_i.minutes\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n"}, "996d0ee60436202508a6e5c59bb09919157ebd8a": {"t": 1785756126.4059486, "n": 3640, "h": "70efb15cf770dae3", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Canonical per-provider metadata — the one place each provider's display\n# label and API-key env var live, keyed by pi's provider id. Both fields\n# are optional and independent: `openai-codex` authenticates via the\n# ChatGPT backend (no key), and keyless providers fall back to a titleized\n# label. Ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis.\n#\n# :label -> Agent::ModelCatalogSync seeds LlmProvider#label on first sync\n# :env -> the env var the provider's API key is read from (below)\nRails.application.config.x.agent.provider_metadata = {\n \"anthropic\" => { label: \"Anthropic\", env: \"ANTHROPIC_API_KEY\" },\n \"openai\" => { label: \"OpenAI\", env: \"OPENAI_API_KEY\" },\n \"openai-codex\" => { label: \"OpenAI Codex\" },\n \"google\" => { label: \"Google\", env: \"GEMINI_API_KEY\" },\n \"deepseek\" => { label: \"DeepSeek\", env: \"DEEPSEEK_API_KEY\" },\n \"mistral\" => { env: \"MISTRAL_API_KEY\" },\n \"groq\" => { env: \"GROQ_API_KEY\" },\n \"cerebras\" => { env: \"CEREBRAS_API_KEY\" },\n \"xai\" => { env: \"XAI_API_KEY\" },\n \"openrouter\" => { env: \"OPENROUTER_API_KEY\" },\n \"together\" => { env: \"TOGETHER_API_KEY\" },\n \"fireworks\" => { env: \"FIREWORKS_API_KEY\" },\n \"huggingface\" => { env: \"HF_TOKEN\" }\n}.freeze\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi as\n# --api-key. A shared, deployment-level resource — Metis has no per-user\n# keys. Only providers with a configured (non-blank) key end up here.\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.provider_metadata.filter_map do |provider, meta|\n [ provider, ENV[meta[:env]] ] if meta[:env]\n end.to_h.compact_blank\n"}, "3ce8609dcce04cd7747803a48ac792a4647042c9": {"t": 1785756128.1452463, "n": 3135, "h": "73a64d959e008b0e", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi\n# as --api-key. These are a shared, deployment-level resource — Metis\n# has no per-user keys.\n#\n# Provider ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis. Only entries with a non-blank value end up\n# in the map.\n# The provider -> env-var-name map is the single source of truth: the key\n# values below read from it, and Agent::ModelCatalogSync derives the\n# control-session env from it too (so a new provider is added in one place).\nRails.application.config.x.agent.api_key_env_names = {\n \"anthropic\" => \"ANTHROPIC_API_KEY\",\n \"openai\" => \"OPENAI_API_KEY\",\n \"google\" => \"GEMINI_API_KEY\",\n \"deepseek\" => \"DEEPSEEK_API_KEY\",\n \"mistral\" => \"MISTRAL_API_KEY\",\n \"groq\" => \"GROQ_API_KEY\",\n \"cerebras\" => \"CEREBRAS_API_KEY\",\n \"xai\" => \"XAI_API_KEY\",\n \"openrouter\" => \"OPENROUTER_API_KEY\",\n \"together\" => \"TOGETHER_API_KEY\",\n \"fireworks\" => \"FIREWORKS_API_KEY\",\n \"huggingface\" => \"HF_TOKEN\"\n}\nRails.application.config.x.agent.api_keys =\n Rails.application.config.x.agent.api_key_env_names\n .transform_values { |env_name| ENV[env_name] }\n .compact_blank\n"}, "6fe920eb9005c43ffea82f4f04e5e7409d029c20": {"t": 1785756129.849926, "n": 2807, "h": "611a9b21f12676c5", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment. A conversation's\n# provider id is matched against this map and the key is passed to pi\n# as --api-key. These are a shared, deployment-level resource — Metis\n# has no per-user keys.\n#\n# Provider ids and env var names mirror pi's own conventions\n# (https://pi.dev/docs/latest/providers) so the same env that runs pi\n# locally works for Metis. Only entries with a non-blank value end up\n# in the map.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GEMINI_API_KEY\"],\n \"deepseek\" => ENV[\"DEEPSEEK_API_KEY\"],\n \"mistral\" => ENV[\"MISTRAL_API_KEY\"],\n \"groq\" => ENV[\"GROQ_API_KEY\"],\n \"cerebras\" => ENV[\"CEREBRAS_API_KEY\"],\n \"xai\" => ENV[\"XAI_API_KEY\"],\n \"openrouter\" => ENV[\"OPENROUTER_API_KEY\"],\n \"together\" => ENV[\"TOGETHER_API_KEY\"],\n \"fireworks\" => ENV[\"FIREWORKS_API_KEY\"],\n \"huggingface\" => ENV[\"HF_TOKEN\"]\n}.compact_blank\n"}, "349a0cbd48c9e15a037e349d04a4c4f18f9d7c1d": {"t": 1785756131.087897, "n": 2284, "h": "671795ff81a28f43", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Idle window after which a paused E2B sandbox is evicted (killed) by\n# EvictPausedSandboxesJob. E2B keeps paused sandboxes indefinitely\n# unless we tell it otherwise (docs/coding-runtime.md), so this knob\n# bounds how long a long-idle conversation's working tree survives —\n# the next turn provisions a fresh sandbox.\nRails.application.config.x.agent.e2b_eviction_window =\n ENV.fetch(\"METIS_E2B_EVICTION_HOURS\", \"24\").to_i.hours\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment (.env in development,\n# loaded by foreman for bin/dev; real env vars in production). A\n# conversation's provider is matched against this map and the key is\n# passed to pi as --api-key. These are a shared, deployment-level\n# resource — Metis has no per-user keys.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GOOGLE_API_KEY\"]\n}.compact_blank\n"}, "c46e8526ef2f279a0b0a8487d92b49f3407d270c": {"t": 1785756132.390072, "n": 1852, "h": "60ae32d6090bb4de", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment (.env in development,\n# loaded by foreman for bin/dev; real env vars in production). A\n# conversation's provider is matched against this map and the key is\n# passed to pi as --api-key. These are a shared, deployment-level\n# resource — Metis has no per-user keys.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GOOGLE_API_KEY\"]\n}.compact_blank\n"}, "c26cfd91f3d15c63ad83bfb076d24eb25ee12328": {"t": 1785756133.7849522, "n": 1806, "h": "340737f164d756c9", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :docker — pi in a Docker container. Namespace-isolated; self-hosted,\n# needs a Docker daemon and an image (see docker:image).\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# Docker image used by the :docker runtime — pi baked in. Build it with\n# the docker:image rake task.\nRails.application.config.x.agent.docker_image =\n ENV.fetch(\"METIS_DOCKER_IMAGE\", \"metis-pi\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment (.env in development,\n# loaded by foreman for bin/dev; real env vars in production). A\n# conversation's provider is matched against this map and the key is\n# passed to pi as --api-key. A per-user ApiKey overrides it.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GOOGLE_API_KEY\"]\n}.compact_blank\n"}, "fb949db1eaa1f23d6197d101fb98ee9738531a46": {"t": 1785756135.1722968, "n": 1463, "h": "5980be6a828d1557", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice. See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment (.env in development,\n# loaded by foreman for bin/dev; real env vars in production). A\n# conversation's provider is matched against this map and the key is\n# passed to pi as --api-key. A per-user ApiKey overrides it.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GOOGLE_API_KEY\"]\n}.compact_blank\n"}, "8687dec0bc6b32ed91ea12e279793a847cf703fd": {"t": 1785756136.5517128, "n": 1511, "h": "63fbc840b8a2b39b", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice (unlike the backend, which is per-conversation). See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# pi's default provider/model — used when a conversation sets none of\n# its own (the new-chat composer normally does). See\n# Agent::Adapters::Pi#credential_args.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\n\n# Per-provider API keys, read from the environment (.env in development,\n# loaded by foreman for bin/dev; real env vars in production). A\n# conversation's provider is matched against this map and the key is\n# passed to pi as --api-key. A per-user ApiKey overrides it.\nRails.application.config.x.agent.api_keys = {\n \"anthropic\" => ENV[\"ANTHROPIC_API_KEY\"],\n \"openai\" => ENV[\"OPENAI_API_KEY\"],\n \"google\" => ENV[\"GOOGLE_API_KEY\"]\n}.compact_blank\n"}, "0a686af0427cbcbda95767862b8f3465ba46eba7": {"t": 1785756137.8893633, "n": 1385, "h": "129f1f1ed87edae1", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice (unlike the backend, which is per-conversation). See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n\n# pi's LLM credentials — a deployment-level default (no per-user UI yet).\n# Per-conversation Conversation#settings and per-user ApiKey override\n# these when set; see Agent::Adapters::Pi#credential_args. The api key is\n# a secret — keep it in Rails credentials (agent.api_key); METIS_AGENT_API_KEY\n# overrides it for environments that prefer env vars.\nRails.application.config.x.agent.provider = ENV[\"METIS_AGENT_PROVIDER\"].presence\nRails.application.config.x.agent.model = ENV[\"METIS_AGENT_MODEL\"].presence\nRails.application.config.x.agent.api_key =\n ENV[\"METIS_AGENT_API_KEY\"].presence ||\n Rails.application.credentials.dig(:agent, :api_key)\n"}, "4ff390985909e407413d160257f286b7331db3dd": {"t": 1785756139.1253402, "n": 739, "h": "872b00e98d234735", "c": "# Agent runtime configuration.\n#\n# The runtime — *where* the coding agent runs — is a per-deployment\n# choice (unlike the backend, which is per-conversation). See\n# Agent::Runtime.\n#\n# :local — pi as a local subprocess. NOT isolated; single-operator /\n# development only.\n# :e2b — pi inside a secure E2B microVM. The isolated runtime;\n# requires E2B_API_KEY and a template with pi baked in.\nRails.application.config.x.agent.runtime =\n ENV.fetch(\"METIS_AGENT_RUNTIME\", \"local\").to_sym\n\n# E2B template (image) used by the :e2b runtime — should have pi\n# installed. See the e2b:template rake task for the build definition.\nRails.application.config.x.agent.e2b_template =\n ENV.fetch(\"METIS_E2B_TEMPLATE\", \"base\")\n"}, "ca6a5ccf59abc17c34d86d7c7560012e75323a81": {"t": 1785756142.1637237, "n": 651, "h": "0d600d67cb2c06cb", "c": "# Durable root for agent files — pi session files (the agent's\n# conversation memory) and, later, per-conversation working directories.\n#\n# This MUST be persistent. It is deliberately not under tmp/, which Rails\n# treats as disposable (rails tmp:clear, deploys, container restarts wipe\n# it). In production, set METIS_AGENT_ROOT to a mounted volume.\n#\n# Test runs use a disposable tmp/ path — ephemeral is correct there.\ndefault_root =\n if Rails.env.test?\n Rails.root.join(\"tmp/test_agent\")\n else\n Rails.root.join(\"storage/agent\")\n end\n\nRails.application.config.x.agent.root =\n Pathname.new(ENV.fetch(\"METIS_AGENT_ROOT\", default_root.to_s))\n"}}}} |