{"repo": "avishai-tsabari/mercury", "files": {"src/agent/container-env.ts": {"df2e21a4b691603b1ed424b6341a4b17793de1df": {"t": 1785745597.5792797, "n": 4938, "h": "d10a51c1072a8588", "c": "/**\n * Which host environment variables reach agent containers.\n *\n * Mercury passes `MERCURY_*` vars into containers with the prefix stripped.\n * That is convenient but blunt: a secret added to `.env` for one purpose is\n * exposed to every space's container, whoever triggered the turn. Vars an\n * extension declares via `mercury.env()` avoid this — they are injected in\n * runtime.ts behind the extension's permission gate instead.\n *\n * `containerEnvPassthrough: \"claimed\"` narrows the blunt path to nothing, so\n * declared vars are the only way in.\n */\n\n/** Vars that must never reach a container, regardless of passthrough mode. */\nexport const BLOCKED_ENV_VARS = new Set([\n \"MERCURY_API_SECRET\",\n // Host-only: signs per-turn caller tokens. Injecting it would let the\n // agent forge a token for any caller, defeating the whole mechanism.\n \"MERCURY_CALLER_TOKEN_KEY\",\n // Host-only: the inner→outer API socket path is set by code per spawn;\n // never let an agent override which socket mrctl targets.\n \"MERCURY_API_SOCKET\",\n \"MERCURY_CHAT_API_KEY\",\n \"MERCURY_ADMINS\",\n // Host-only: affects `docker run` flags, not the agent process inside the container\n \"MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT\",\n // Host-only: selects the OCI runtime for `docker run --runtime`; not meaningful inside the container\n \"MERCURY_CONTAINER_RUNTIME\",\n // Host-only: resolved volume mountpoint on the host; inner containers don't need it\n \"MERCURY_HOST_DATA_DIR\",\n \"MERCURY_SLACK_BOT_TOKEN\",\n \"MERCURY_SLACK_SIGNING_SECRET\",\n \"MERCURY_DISCORD_BOT_TOKEN\",\n \"MERCURY_DISCORD_GATEWAY_SECRET\",\n \"MERCURY_TELEGRAM_BOT_TOKEN\",\n \"MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN\",\n \"MERCURY_TEAMS_APP_ID\",\n \"MERCURY_TEAMS_APP_PASSWORD\",\n \"MERCURY_WHATSAPP_AUTH_DIR\",\n]);\n\n/**\n * Model-provider credentials. pi reads these inside the container, so they are\n * runtime plumbing rather than user secrets — and no extension declares them,\n * which means \"claimed\" mode would otherwise leave the agent with no way to\n * reach a model at all. Exempt from the claimed filter for that reason; still\n * subject to BLOCKED_ENV_VARS.\n *\n * Mirrors the provider list in core/routes/dashboard.ts (drift is covered by a\n * test), plus MERCURY_ANTHROPIC_OAUTH_TOKEN, which console provisioning sets\n * instead of an API key.\n */\nexport const MODEL_PROVIDER_ENV_VARS = new Set([\n \"MERCURY_AI_GATEWAY_API_KEY\",\n \"MERCURY_ANTHROPIC_API_KEY\",\n \"MERCURY_ANTHROPIC_OAUTH_TOKEN\",\n \"MERCURY_AWS_BEARER_TOKEN_BEDROCK\",\n \"MERCURY_AZURE_OPENAI_API_KEY\",\n \"MERCURY_CEREBRAS_API_KEY\",\n \"MERCURY_DEEPSEEK_API_KEY\",\n \"MERCURY_GEMINI_API_KEY\",\n \"MERCURY_GITHUB_COPILOT_OAUTH_TOKEN\",\n \"MERCURY_GOOGLE_CLOUD_API_KEY\",\n \"MERCURY_GROQ_API_KEY\",\n \"MERCURY_HF_TOKEN\",\n \"MERCURY_KIMI_API_KEY\",\n \"MERCURY_MINIMAX_API_KEY\",\n \"MERCURY_MINIMAX_CN_API_KEY\",\n \"MERCURY_MISTRAL_API_KEY\",\n \"MERCURY_OPENAI_API_KEY\",\n \"MERCURY_OPENROUTER_API_KEY\",\n \"MERCURY_XAI_API_KEY\",\n \"MERCURY_ZAI_API_KEY\",\n]);\n\nexport type EnvPassthroughMode = \"all\" | \"claimed\";\n\n/**\n * Host `MERCURY_*` var names that are neither blocked nor claimed by an\n * extension — the ones the blind passthrough carries into every container.\n *\n * Sorted, and names only: callers log these, and the values are the secrets.\n */\nexport function listUnclaimedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return Object.keys(env)\n .filter(\n (key) =>\n key.startsWith(\"MERCURY_\") &&\n env[key] !== undefined &&\n !BLOCKED_ENV_VARS.has(key) &&\n !claimed?.has(key),\n )\n .sort();\n}\n\n/**\n * Undeclared vars that are not model-provider plumbing — the ones worth\n * surfacing at startup. Provider keys are expected to be here and would only\n * add noise; a var in this list is one nobody scoped to anything.\n */\nexport function listUnexpectedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return listUnclaimedPassthroughVars(env, claimed).filter(\n (key) => !MODEL_PROVIDER_ENV_VARS.has(key),\n );\n}\n\n/**\n * The blind-passthrough pairs for a container spawn, with `MERCURY_` stripped.\n *\n * Extension-declared vars are excluded here on purpose — runtime.ts injects\n * those separately, behind the permission check. In \"claimed\" mode only\n * model-provider credentials pass, so the agent can still reach a model.\n */\nexport function selectPassthroughEnv(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n mode: EnvPassthroughMode,\n): Array<{ key: string; value: string }> {\n const names =\n mode === \"claimed\"\n ? listUnclaimedPassthroughVars(env, claimed).filter((key) =>\n MODEL_PROVIDER_ENV_VARS.has(key),\n )\n : listUnclaimedPassthroughVars(env, claimed);\n\n return names.map((key) => ({\n key: key.replace(\"MERCURY_\", \"\"),\n // listUnclaimedPassthroughVars already filtered out undefined values.\n value: env[key] as string,\n }));\n}\n"}, "e98ae354efa1b4f8420e2ed247314ef126ecd366": {"t": 1785754628.6003802, "n": 4938, "h": "d10a51c1072a8588", "c": "/**\n * Which host environment variables reach agent containers.\n *\n * Mercury passes `MERCURY_*` vars into containers with the prefix stripped.\n * That is convenient but blunt: a secret added to `.env` for one purpose is\n * exposed to every space's container, whoever triggered the turn. Vars an\n * extension declares via `mercury.env()` avoid this — they are injected in\n * runtime.ts behind the extension's permission gate instead.\n *\n * `containerEnvPassthrough: \"claimed\"` narrows the blunt path to nothing, so\n * declared vars are the only way in.\n */\n\n/** Vars that must never reach a container, regardless of passthrough mode. */\nexport const BLOCKED_ENV_VARS = new Set([\n \"MERCURY_API_SECRET\",\n // Host-only: signs per-turn caller tokens. Injecting it would let the\n // agent forge a token for any caller, defeating the whole mechanism.\n \"MERCURY_CALLER_TOKEN_KEY\",\n // Host-only: the inner→outer API socket path is set by code per spawn;\n // never let an agent override which socket mrctl targets.\n \"MERCURY_API_SOCKET\",\n \"MERCURY_CHAT_API_KEY\",\n \"MERCURY_ADMINS\",\n // Host-only: affects `docker run` flags, not the agent process inside the container\n \"MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT\",\n // Host-only: selects the OCI runtime for `docker run --runtime`; not meaningful inside the container\n \"MERCURY_CONTAINER_RUNTIME\",\n // Host-only: resolved volume mountpoint on the host; inner containers don't need it\n \"MERCURY_HOST_DATA_DIR\",\n \"MERCURY_SLACK_BOT_TOKEN\",\n \"MERCURY_SLACK_SIGNING_SECRET\",\n \"MERCURY_DISCORD_BOT_TOKEN\",\n \"MERCURY_DISCORD_GATEWAY_SECRET\",\n \"MERCURY_TELEGRAM_BOT_TOKEN\",\n \"MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN\",\n \"MERCURY_TEAMS_APP_ID\",\n \"MERCURY_TEAMS_APP_PASSWORD\",\n \"MERCURY_WHATSAPP_AUTH_DIR\",\n]);\n\n/**\n * Model-provider credentials. pi reads these inside the container, so they are\n * runtime plumbing rather than user secrets — and no extension declares them,\n * which means \"claimed\" mode would otherwise leave the agent with no way to\n * reach a model at all. Exempt from the claimed filter for that reason; still\n * subject to BLOCKED_ENV_VARS.\n *\n * Mirrors the provider list in core/routes/dashboard.ts (drift is covered by a\n * test), plus MERCURY_ANTHROPIC_OAUTH_TOKEN, which console provisioning sets\n * instead of an API key.\n */\nexport const MODEL_PROVIDER_ENV_VARS = new Set([\n \"MERCURY_AI_GATEWAY_API_KEY\",\n \"MERCURY_ANTHROPIC_API_KEY\",\n \"MERCURY_ANTHROPIC_OAUTH_TOKEN\",\n \"MERCURY_AWS_BEARER_TOKEN_BEDROCK\",\n \"MERCURY_AZURE_OPENAI_API_KEY\",\n \"MERCURY_CEREBRAS_API_KEY\",\n \"MERCURY_DEEPSEEK_API_KEY\",\n \"MERCURY_GEMINI_API_KEY\",\n \"MERCURY_GITHUB_COPILOT_OAUTH_TOKEN\",\n \"MERCURY_GOOGLE_CLOUD_API_KEY\",\n \"MERCURY_GROQ_API_KEY\",\n \"MERCURY_HF_TOKEN\",\n \"MERCURY_KIMI_API_KEY\",\n \"MERCURY_MINIMAX_API_KEY\",\n \"MERCURY_MINIMAX_CN_API_KEY\",\n \"MERCURY_MISTRAL_API_KEY\",\n \"MERCURY_OPENAI_API_KEY\",\n \"MERCURY_OPENROUTER_API_KEY\",\n \"MERCURY_XAI_API_KEY\",\n \"MERCURY_ZAI_API_KEY\",\n]);\n\nexport type EnvPassthroughMode = \"all\" | \"claimed\";\n\n/**\n * Host `MERCURY_*` var names that are neither blocked nor claimed by an\n * extension — the ones the blind passthrough carries into every container.\n *\n * Sorted, and names only: callers log these, and the values are the secrets.\n */\nexport function listUnclaimedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return Object.keys(env)\n .filter(\n (key) =>\n key.startsWith(\"MERCURY_\") &&\n env[key] !== undefined &&\n !BLOCKED_ENV_VARS.has(key) &&\n !claimed?.has(key),\n )\n .sort();\n}\n\n/**\n * Undeclared vars that are not model-provider plumbing — the ones worth\n * surfacing at startup. Provider keys are expected to be here and would only\n * add noise; a var in this list is one nobody scoped to anything.\n */\nexport function listUnexpectedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return listUnclaimedPassthroughVars(env, claimed).filter(\n (key) => !MODEL_PROVIDER_ENV_VARS.has(key),\n );\n}\n\n/**\n * The blind-passthrough pairs for a container spawn, with `MERCURY_` stripped.\n *\n * Extension-declared vars are excluded here on purpose — runtime.ts injects\n * those separately, behind the permission check. In \"claimed\" mode only\n * model-provider credentials pass, so the agent can still reach a model.\n */\nexport function selectPassthroughEnv(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n mode: EnvPassthroughMode,\n): Array<{ key: string; value: string }> {\n const names =\n mode === \"claimed\"\n ? listUnclaimedPassthroughVars(env, claimed).filter((key) =>\n MODEL_PROVIDER_ENV_VARS.has(key),\n )\n : listUnclaimedPassthroughVars(env, claimed);\n\n return names.map((key) => ({\n key: key.replace(\"MERCURY_\", \"\"),\n // listUnclaimedPassthroughVars already filtered out undefined values.\n value: env[key] as string,\n }));\n}\n"}, "fdcc255c94aac8f27ac58c7531e038ee09d94995": {"t": 1785754631.5143316, "n": 3086, "h": "29a70d81684a1776", "c": "/**\n * Which host environment variables reach agent containers.\n *\n * Mercury passes `MERCURY_*` vars into containers with the prefix stripped.\n * That is convenient but blunt: a secret added to `.env` for one purpose is\n * exposed to every space's container, whoever triggered the turn. Vars an\n * extension declares via `mercury.env()` avoid this — they are injected in\n * runtime.ts behind the extension's permission gate instead.\n *\n * `containerEnvPassthrough: \"claimed\"` narrows the blunt path to nothing, so\n * declared vars are the only way in.\n */\n\n/** Vars that must never reach a container, regardless of passthrough mode. */\nexport const BLOCKED_ENV_VARS = new Set([\n \"MERCURY_API_SECRET\",\n // Host-only: signs per-turn caller tokens. Injecting it would let the\n // agent forge a token for any caller, defeating the whole mechanism.\n \"MERCURY_CALLER_TOKEN_KEY\",\n // Host-only: the inner→outer API socket path is set by code per spawn;\n // never let an agent override which socket mrctl targets.\n \"MERCURY_API_SOCKET\",\n \"MERCURY_CHAT_API_KEY\",\n \"MERCURY_ADMINS\",\n // Host-only: affects `docker run` flags, not the agent process inside the container\n \"MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT\",\n // Host-only: selects the OCI runtime for `docker run --runtime`; not meaningful inside the container\n \"MERCURY_CONTAINER_RUNTIME\",\n // Host-only: resolved volume mountpoint on the host; inner containers don't need it\n \"MERCURY_HOST_DATA_DIR\",\n \"MERCURY_SLACK_BOT_TOKEN\",\n \"MERCURY_SLACK_SIGNING_SECRET\",\n \"MERCURY_DISCORD_BOT_TOKEN\",\n \"MERCURY_DISCORD_GATEWAY_SECRET\",\n \"MERCURY_TELEGRAM_BOT_TOKEN\",\n \"MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN\",\n \"MERCURY_TEAMS_APP_ID\",\n \"MERCURY_TEAMS_APP_PASSWORD\",\n \"MERCURY_WHATSAPP_AUTH_DIR\",\n]);\n\nexport type EnvPassthroughMode = \"all\" | \"claimed\";\n\n/**\n * Host `MERCURY_*` var names that are neither blocked nor claimed by an\n * extension — the ones the blind passthrough carries into every container.\n *\n * Sorted, and names only: callers log these, and the values are the secrets.\n */\nexport function listUnclaimedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return Object.keys(env)\n .filter(\n (key) =>\n key.startsWith(\"MERCURY_\") &&\n env[key] !== undefined &&\n !BLOCKED_ENV_VARS.has(key) &&\n !claimed?.has(key),\n )\n .sort();\n}\n\n/**\n * The blind-passthrough pairs for a container spawn, with `MERCURY_` stripped.\n *\n * Extension-declared vars are excluded here on purpose — runtime.ts injects\n * those separately, behind the permission check. In \"claimed\" mode this\n * returns nothing at all.\n */\nexport function selectPassthroughEnv(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n mode: EnvPassthroughMode,\n): Array<{ key: string; value: string }> {\n if (mode === \"claimed\") return [];\n\n return listUnclaimedPassthroughVars(env, claimed).map((key) => ({\n key: key.replace(\"MERCURY_\", \"\"),\n // listUnclaimedPassthroughVars already filtered out undefined values.\n value: env[key] as string,\n }));\n}\n"}, "dbdb0499f264b1613a79ffb262b3b155e46ad6c7": {"t": 1786067415.4427974, "n": 4938, "h": "d10a51c1072a8588", "c": "/**\n * Which host environment variables reach agent containers.\n *\n * Mercury passes `MERCURY_*` vars into containers with the prefix stripped.\n * That is convenient but blunt: a secret added to `.env` for one purpose is\n * exposed to every space's container, whoever triggered the turn. Vars an\n * extension declares via `mercury.env()` avoid this — they are injected in\n * runtime.ts behind the extension's permission gate instead.\n *\n * `containerEnvPassthrough: \"claimed\"` narrows the blunt path to nothing, so\n * declared vars are the only way in.\n */\n\n/** Vars that must never reach a container, regardless of passthrough mode. */\nexport const BLOCKED_ENV_VARS = new Set([\n \"MERCURY_API_SECRET\",\n // Host-only: signs per-turn caller tokens. Injecting it would let the\n // agent forge a token for any caller, defeating the whole mechanism.\n \"MERCURY_CALLER_TOKEN_KEY\",\n // Host-only: the inner→outer API socket path is set by code per spawn;\n // never let an agent override which socket mrctl targets.\n \"MERCURY_API_SOCKET\",\n \"MERCURY_CHAT_API_KEY\",\n \"MERCURY_ADMINS\",\n // Host-only: affects `docker run` flags, not the agent process inside the container\n \"MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT\",\n // Host-only: selects the OCI runtime for `docker run --runtime`; not meaningful inside the container\n \"MERCURY_CONTAINER_RUNTIME\",\n // Host-only: resolved volume mountpoint on the host; inner containers don't need it\n \"MERCURY_HOST_DATA_DIR\",\n \"MERCURY_SLACK_BOT_TOKEN\",\n \"MERCURY_SLACK_SIGNING_SECRET\",\n \"MERCURY_DISCORD_BOT_TOKEN\",\n \"MERCURY_DISCORD_GATEWAY_SECRET\",\n \"MERCURY_TELEGRAM_BOT_TOKEN\",\n \"MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN\",\n \"MERCURY_TEAMS_APP_ID\",\n \"MERCURY_TEAMS_APP_PASSWORD\",\n \"MERCURY_WHATSAPP_AUTH_DIR\",\n]);\n\n/**\n * Model-provider credentials. pi reads these inside the container, so they are\n * runtime plumbing rather than user secrets — and no extension declares them,\n * which means \"claimed\" mode would otherwise leave the agent with no way to\n * reach a model at all. Exempt from the claimed filter for that reason; still\n * subject to BLOCKED_ENV_VARS.\n *\n * Mirrors the provider list in core/routes/dashboard.ts (drift is covered by a\n * test), plus MERCURY_ANTHROPIC_OAUTH_TOKEN, which console provisioning sets\n * instead of an API key.\n */\nexport const MODEL_PROVIDER_ENV_VARS = new Set([\n \"MERCURY_AI_GATEWAY_API_KEY\",\n \"MERCURY_ANTHROPIC_API_KEY\",\n \"MERCURY_ANTHROPIC_OAUTH_TOKEN\",\n \"MERCURY_AWS_BEARER_TOKEN_BEDROCK\",\n \"MERCURY_AZURE_OPENAI_API_KEY\",\n \"MERCURY_CEREBRAS_API_KEY\",\n \"MERCURY_DEEPSEEK_API_KEY\",\n \"MERCURY_GEMINI_API_KEY\",\n \"MERCURY_GITHUB_COPILOT_OAUTH_TOKEN\",\n \"MERCURY_GOOGLE_CLOUD_API_KEY\",\n \"MERCURY_GROQ_API_KEY\",\n \"MERCURY_HF_TOKEN\",\n \"MERCURY_KIMI_API_KEY\",\n \"MERCURY_MINIMAX_API_KEY\",\n \"MERCURY_MINIMAX_CN_API_KEY\",\n \"MERCURY_MISTRAL_API_KEY\",\n \"MERCURY_OPENAI_API_KEY\",\n \"MERCURY_OPENROUTER_API_KEY\",\n \"MERCURY_XAI_API_KEY\",\n \"MERCURY_ZAI_API_KEY\",\n]);\n\nexport type EnvPassthroughMode = \"all\" | \"claimed\";\n\n/**\n * Host `MERCURY_*` var names that are neither blocked nor claimed by an\n * extension — the ones the blind passthrough carries into every container.\n *\n * Sorted, and names only: callers log these, and the values are the secrets.\n */\nexport function listUnclaimedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return Object.keys(env)\n .filter(\n (key) =>\n key.startsWith(\"MERCURY_\") &&\n env[key] !== undefined &&\n !BLOCKED_ENV_VARS.has(key) &&\n !claimed?.has(key),\n )\n .sort();\n}\n\n/**\n * Undeclared vars that are not model-provider plumbing — the ones worth\n * surfacing at startup. Provider keys are expected to be here and would only\n * add noise; a var in this list is one nobody scoped to anything.\n */\nexport function listUnexpectedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return listUnclaimedPassthroughVars(env, claimed).filter(\n (key) => !MODEL_PROVIDER_ENV_VARS.has(key),\n );\n}\n\n/**\n * The blind-passthrough pairs for a container spawn, with `MERCURY_` stripped.\n *\n * Extension-declared vars are excluded here on purpose — runtime.ts injects\n * those separately, behind the permission check. In \"claimed\" mode only\n * model-provider credentials pass, so the agent can still reach a model.\n */\nexport function selectPassthroughEnv(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n mode: EnvPassthroughMode,\n): Array<{ key: string; value: string }> {\n const names =\n mode === \"claimed\"\n ? listUnclaimedPassthroughVars(env, claimed).filter((key) =>\n MODEL_PROVIDER_ENV_VARS.has(key),\n )\n : listUnclaimedPassthroughVars(env, claimed);\n\n return names.map((key) => ({\n key: key.replace(\"MERCURY_\", \"\"),\n // listUnclaimedPassthroughVars already filtered out undefined values.\n value: env[key] as string,\n }));\n}\n"}, "70a5ce333d6718ff36ad29b72542dc167d749f1f": {"t": 1786068280.6227007, "n": 4938, "h": "d10a51c1072a8588", "c": "/**\n * Which host environment variables reach agent containers.\n *\n * Mercury passes `MERCURY_*` vars into containers with the prefix stripped.\n * That is convenient but blunt: a secret added to `.env` for one purpose is\n * exposed to every space's container, whoever triggered the turn. Vars an\n * extension declares via `mercury.env()` avoid this — they are injected in\n * runtime.ts behind the extension's permission gate instead.\n *\n * `containerEnvPassthrough: \"claimed\"` narrows the blunt path to nothing, so\n * declared vars are the only way in.\n */\n\n/** Vars that must never reach a container, regardless of passthrough mode. */\nexport const BLOCKED_ENV_VARS = new Set([\n \"MERCURY_API_SECRET\",\n // Host-only: signs per-turn caller tokens. Injecting it would let the\n // agent forge a token for any caller, defeating the whole mechanism.\n \"MERCURY_CALLER_TOKEN_KEY\",\n // Host-only: the inner→outer API socket path is set by code per spawn;\n // never let an agent override which socket mrctl targets.\n \"MERCURY_API_SOCKET\",\n \"MERCURY_CHAT_API_KEY\",\n \"MERCURY_ADMINS\",\n // Host-only: affects `docker run` flags, not the agent process inside the container\n \"MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT\",\n // Host-only: selects the OCI runtime for `docker run --runtime`; not meaningful inside the container\n \"MERCURY_CONTAINER_RUNTIME\",\n // Host-only: resolved volume mountpoint on the host; inner containers don't need it\n \"MERCURY_HOST_DATA_DIR\",\n \"MERCURY_SLACK_BOT_TOKEN\",\n \"MERCURY_SLACK_SIGNING_SECRET\",\n \"MERCURY_DISCORD_BOT_TOKEN\",\n \"MERCURY_DISCORD_GATEWAY_SECRET\",\n \"MERCURY_TELEGRAM_BOT_TOKEN\",\n \"MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN\",\n \"MERCURY_TEAMS_APP_ID\",\n \"MERCURY_TEAMS_APP_PASSWORD\",\n \"MERCURY_WHATSAPP_AUTH_DIR\",\n]);\n\n/**\n * Model-provider credentials. pi reads these inside the container, so they are\n * runtime plumbing rather than user secrets — and no extension declares them,\n * which means \"claimed\" mode would otherwise leave the agent with no way to\n * reach a model at all. Exempt from the claimed filter for that reason; still\n * subject to BLOCKED_ENV_VARS.\n *\n * Mirrors the provider list in core/routes/dashboard.ts (drift is covered by a\n * test), plus MERCURY_ANTHROPIC_OAUTH_TOKEN, which console provisioning sets\n * instead of an API key.\n */\nexport const MODEL_PROVIDER_ENV_VARS = new Set([\n \"MERCURY_AI_GATEWAY_API_KEY\",\n \"MERCURY_ANTHROPIC_API_KEY\",\n \"MERCURY_ANTHROPIC_OAUTH_TOKEN\",\n \"MERCURY_AWS_BEARER_TOKEN_BEDROCK\",\n \"MERCURY_AZURE_OPENAI_API_KEY\",\n \"MERCURY_CEREBRAS_API_KEY\",\n \"MERCURY_DEEPSEEK_API_KEY\",\n \"MERCURY_GEMINI_API_KEY\",\n \"MERCURY_GITHUB_COPILOT_OAUTH_TOKEN\",\n \"MERCURY_GOOGLE_CLOUD_API_KEY\",\n \"MERCURY_GROQ_API_KEY\",\n \"MERCURY_HF_TOKEN\",\n \"MERCURY_KIMI_API_KEY\",\n \"MERCURY_MINIMAX_API_KEY\",\n \"MERCURY_MINIMAX_CN_API_KEY\",\n \"MERCURY_MISTRAL_API_KEY\",\n \"MERCURY_OPENAI_API_KEY\",\n \"MERCURY_OPENROUTER_API_KEY\",\n \"MERCURY_XAI_API_KEY\",\n \"MERCURY_ZAI_API_KEY\",\n]);\n\nexport type EnvPassthroughMode = \"all\" | \"claimed\";\n\n/**\n * Host `MERCURY_*` var names that are neither blocked nor claimed by an\n * extension — the ones the blind passthrough carries into every container.\n *\n * Sorted, and names only: callers log these, and the values are the secrets.\n */\nexport function listUnclaimedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return Object.keys(env)\n .filter(\n (key) =>\n key.startsWith(\"MERCURY_\") &&\n env[key] !== undefined &&\n !BLOCKED_ENV_VARS.has(key) &&\n !claimed?.has(key),\n )\n .sort();\n}\n\n/**\n * Undeclared vars that are not model-provider plumbing — the ones worth\n * surfacing at startup. Provider keys are expected to be here and would only\n * add noise; a var in this list is one nobody scoped to anything.\n */\nexport function listUnexpectedPassthroughVars(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n): string[] {\n return listUnclaimedPassthroughVars(env, claimed).filter(\n (key) => !MODEL_PROVIDER_ENV_VARS.has(key),\n );\n}\n\n/**\n * The blind-passthrough pairs for a container spawn, with `MERCURY_` stripped.\n *\n * Extension-declared vars are excluded here on purpose — runtime.ts injects\n * those separately, behind the permission check. In \"claimed\" mode only\n * model-provider credentials pass, so the agent can still reach a model.\n */\nexport function selectPassthroughEnv(\n env: NodeJS.ProcessEnv,\n claimed: Set | undefined,\n mode: EnvPassthroughMode,\n): Array<{ key: string; value: string }> {\n const names =\n mode === \"claimed\"\n ? listUnclaimedPassthroughVars(env, claimed).filter((key) =>\n MODEL_PROVIDER_ENV_VARS.has(key),\n )\n : listUnclaimedPassthroughVars(env, claimed);\n\n return names.map((key) => ({\n key: key.replace(\"MERCURY_\", \"\"),\n // listUnclaimedPassthroughVars already filtered out undefined values.\n value: env[key] as string,\n }));\n}\n"}}}}