# deploy/.env 模板 —— 复制成 .env 后逐项填写,并 chmod 600。
# 本文件入库,.env 永不入库(见 .gitignore)。
# 正在部署的镜像 tag。必须是 git 短 SHA;compose 与 migrate.sh 都会硬校验,
# 禁止 latest 等可变 tag(CLAUDE.md 规则 10)。
IMAGE_TAG=
# 镜像仓库前缀。save/load 文件传输流程留空即可(默认 local)。
#IMAGE_REGISTRY=
# Postgres 密码。首次部署时生成,之后不要改(改了要同步改库里的角色口令)。
POSTGRES_PASSWORD=
# ── MCP 管理面(R6)────────────────────────────────────────────────
# 注意:这两项**都不是可直接使用的凭据**,泄漏它们不等于拿到管理权或 LLM key。
#
# ⚠️ 两处都必须用**密码学安全**的随机源。PowerShell 5.1 的 `Get-Random` 背后是
# System.Random(非密码学 PRNG),用它生成 token 或密钥等于让「高熵」这条要求
# (docs/security.md §4)名存实亡。下面用 RandomNumberGenerator。
# 同理不要用 .NET 5+ 才有的 `SHA256::HashData` —— PS 5.1 跑在 .NET Framework 上,
# 那个静态方法不存在,命令会直接报错(实测)。
#
# 管理面 bearer token 的 sha256(hex,小写)。原 token 只在所有者手里,服务端只存哈希。
# 生成(PowerShell 5.1,整段一起跑):
# $b = New-Object byte[] 32
# [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b)
# $t = [Convert]::ToBase64String($b); $t # ← 这个是 token,存进你的密码管理器
# $sha = [System.Security.Cryptography.SHA256]::Create()
# [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($t))).Replace("-","").ToLower()
# # ← 这个填下面
MCP_AUTH_TOKEN_HASH=
# llm_config 里 LLM key 的 AES-256-GCM 密钥,32 字节的 base64。
# 生成(PowerShell 5.1):
# $k = New-Object byte[] 32
# [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($k)
# [Convert]::ToBase64String($k)
# ⚠️ 换掉它,库里既有的 LLM key 密文全部解不开,agent 立刻停摆,
# 必须经 MCP 的 llm_provider_upsert 把各 provider 的 key 重写一遍。
CONFIG_ENCRYPTION_KEY=
# ⚠️ R6 起没有 LLM 引导密钥(原 DEEPSEEK_API_KEY 已按所有者裁定移除)。
# 运行期 LLM 凭据的唯一来源是 llm_config 表:**首次部署完成后,必须先经
# MCP 的 llm_provider_upsert 写入一个 provider,agent 对话才可用**
# (在那之前 /agent/ask 回 503)。流程见 docs/deploy-environments.md。
# ── 访问统计(R8)────────────────────────────────────────────────
# 访客标识哈希的盐。`docs/security.md` §6:IP 加盐哈希后落库,**不存原始 IP**;
# 这个盐就是「加盐」那一半的全部安全性来源 —— 泄漏它,一份 visits 表可以被
# 2^32 次 sha256 反推回 IPv4 地址。
# 生成(PowerShell 5.1,与上面两项同一套 CSPRNG,勿用 Get-Random):
# $b = New-Object byte[] 32
# [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b)
# [Convert]::ToBase64String($b)
# ⚠️ 换掉它 = 之后的访客与之前的对不上号(旧数据不受影响,但跨换盐时点的 UV
# 会被算成两拨人)。没配的话打点整个停用(不会退化成不加盐哈希),
# 表现是服务端日志一行 error + 统计 tool 永远回 0。
METRICS_IP_SALT=
# ── ICP 备案(仅生产;R11 备案通过后填)─────────────────────────
# 备案号原文,如「沪ICP备12345678号-1」。填了才会在页面底部渲染一条链到
# 工信部的底栏(docs/deploy-cn-lightweight.md §1 第 6 步);留空则整块不渲染,
# 版式与设计稿一致 —— 130 预发不备案,保持留空。
ICP_BEIAN=
# 公安联网备案号原文,如「苏公网安备32011402012723号」(2026-09-10 起支持)。
# 网站开通后 30 个工作日内必须办并挂在底部:填了才在 ICP 号右边渲染「备案编号图标 +
# 号码」,号码链到 `https://beian.mps.gov.cn/#/query/webSearch?code=<号里的数字串>`
# ——code 由组件从号码里取,只填这一项即可。留空则那一半不渲染(130 预发保持留空)。
MPS_BEIAN=
# Caddy 监听的站点地址(R11)。**留空 = `:80`**,与本项存在之前的行为一字不差。
# 130 预发 留空 ← 明文 :80,与既有部署零差异
# 生产 www.kzgai.cloud, kzgai.cloud ← 填域名即自动 TLS
# 多域名逗号分隔;列进来的每个名字都要能解析到本机,否则那个名字的 ACME 签发会反复失败刷日志。
# **裸域要列进来**:不列的话它连证书都没有,连不上更谈不上跳到 www(见下面 SITE_REDIRECT_FROM)。
SITE_ADDRESS=
# 规范主机名重定向(R11):这里列的主机名一律 301 到 SITE_ORIGIN,路径与 query 原样带过去。
# 130 预发 留空 ← 规则是死的
# 生产 kzgai.cloud ← 裸域跳 https://www.kzgai.cloud
# 【为什么跳转目标不另开变量】直接用 SITE_ORIGIN,「跳到哪」与「RSS 里写出去的绝对链接是哪」
# 于是永远是同一个值,不可能配歪。
# ⚠️ compose 里这一项的默认值是**哨兵** `__unset__` 而不是空串:Caddy 的 `{$VAR:default}`
# 只在变量**不存在**时取默认值,变量存在但为空时取空 —— 那会让 matcher 变成 `host `
# (缺参数),Caddyfile 直接解析失败、caddy 起不来(实测:module value cannot be null)。
SITE_REDIRECT_FROM=
# 对外访问地址,**必须含 scheme,必要时含端口**。RSS 条目里的绝对链接、
# 订阅弹层展示与复制的地址都取它。
# 生产 https://www.kzgai.cloud ← 站点对外的**唯一**地址;裸域由 SITE_REDIRECT_FROM 跳过来
# 130 预发 http://192.168.100.130 ← R9 按所有者裁定用了 80,不是 8080
# 【与 SITE_ADDRESS 的分工】那个决定 Caddy **听**什么,这个是写出去的**对外地址**。
# 两者必须指向同一个站:写错的后果不是报错,而是订阅源里的链接全指向一个没在听的端点。
SITE_ORIGIN=
# ── 高危工具双闸之一(R7 留的闸,R-SKILLS-2 起有了用武之地;默认不设)────────────
# XRAY_UNLOCK_DANGEROUS_TOOLS=1 只是第一闸,另一闸是 tool_config 里那一行的 enabled。
# 注册表里唯一的 dangerous 工具是 `skill_run`(R-SKILLS-2;高危身份写在代码里,表里的 dangerous 位改成 false 也拆不掉这道 env 闸):它在**独立的 skill-runner 容器**里
# (network_mode: none、只读、非 root、rlimit)跑镜像里预置、清单里点名的 Python 脚本 —— 访客给不了
# 代码 / 路径 / 命令行。bash / write / 任意代码执行类工具仍永久禁止进 in-process 进程
# (docs/security.md §1 第 1 层),设了它也长不出来。
#
# 打开顺序(每一步可单独回退;rounds/round-skills/round-skills-2.md「运维」段):
# 发版(runner 镜像 + api + compose)→ 生产冒烟 4 条 → MCP `tool_config_set skill_load true`
# → `skills_agent_set <name> true` 逐个(先 `skills_agent_status` 看 consistency 是 ok)
# → 这里设 1 并 `docker compose up -d api` 重建(env 变了 restart 不生效)→ `tool_config_set skill_run true`。
# 止损:`tool_config_set skill_run false` 当场停用,不用动这里;`docker compose stop skill-runner` 后
# 工具以固定文案失败、站点其余照常。
#
# 另注:R7 的 agent_ro 只读角色**不需要任何变量** —— 它是 NOLOGIN 角色,
# 由应用连接在事务里 SET LOCAL ROLE 临时降权,没有第二套连接串、也没有第二份口令。
# 执行容器的地址也**不需要任何变量**:api 走命名卷里的 unix socket(代码默认值),每档一条 ——
# none 档 runner_sock → /run/runner/runner.sock,egress 档 runner_egress_sock → /run/runner-egress/runner.sock(R-WEBFETCH)。
# XRAY_SKILL_RUNNER_URL / XRAY_SKILL_RUNNER_EGRESS_URL 是本机开发的 TCP 覆盖项(只认 http://127.0.0.1:<port>),生产不设。
# egress 实例还有一道**宿主**过滤(iptables DOCKER-USER)不在 .env 里:sudo ./egress-filter.sh --install-unit(docs/security.md §5)。
#XRAY_UNLOCK_DANGEROUS_TOOLS=
# ── 联网搜索的目标域白名单追加项(R-WEBSEARCH,默认不设)──────────────
# 内置白名单硬编码在 apps/api/shared/websearch-hosts.ts(只有 api.deepseek.com;
# 自建 / 代理网关一律从这里追加,不进代码),改它要发版。本变量**只能追加、不能替换**:
# 写错或清空它拿不掉内置约束,后果最多是「少了一个可用端点」。
# 逗号分隔,只写 host(不带 scheme / 端口 / 路径)。
#
# 白名单之外还有一层:websearch provider 的 baseUrl 必须是 https、
# 不带 query/fragment、不内嵌凭据(docs/security.md §1 外呼组约束 2)。
# 凭据本身**不在这里** —— 它经 MCP 的 websearch_provider_upsert 加密入库,
# 与 LLM key 同一把 CONFIG_ENCRYPTION_KEY。
#XRAY_WEBSEARCH_EXTRA_HOSTS=
# ── 生图的目标域白名单追加项(R-IMAGEGEN,默认不设)──────────────────────
# 与上面那条同一套规则,但是**另一份清单**:内置白名单在 apps/api/shared/imagegen-hosts.ts
# (只有 api.openai.com),一个域被列进搜索白名单不等于自动能当生图端点。
# 所有者的 CLIProxyAPI 网关(域名随 IP 变)两个变量都要写一次。
# 凭据本身**不在这里** —— 它经 MCP 的 imagegen_provider_upsert 加密入库。
#XRAY_IMAGEGEN_EXTRA_HOSTS=