@sema-agent/server 7.97.0 → 7.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/MIGRATION.md +11 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/USAGE.md +18 -9
  5. package/deploy/sema-up/cluster-up.sh +8 -4
  6. package/deploy/sema-up/kube-up.sh +40 -15
  7. package/deploy/sema-up/lib/envf.sh +241 -0
  8. package/deploy/sema-up/sema-up.sh +162 -101
  9. package/deploy/sema-up/smoke.sh +8 -1
  10. package/dist/boot/config-center.js +169 -24
  11. package/dist/boot/org-memory.d.ts +2 -1
  12. package/dist/boot/org-memory.js +9 -5
  13. package/dist/boot/resolve-spec.js +9 -4
  14. package/dist/boot/runtime-caps.js +4 -4
  15. package/dist/brain.js +28 -18
  16. package/dist/capabilities/center-prompts.d.ts +3 -0
  17. package/dist/capabilities/center-prompts.js +1 -1
  18. package/dist/center-credential.d.ts +112 -0
  19. package/dist/center-credential.js +131 -0
  20. package/dist/config-catalog.js +7 -6
  21. package/dist/config-center/apply-effective.js +7 -1
  22. package/dist/config-center/center-request.d.ts +60 -0
  23. package/dist/config-center/center-request.js +71 -0
  24. package/dist/config-center/facade.d.ts +2 -1
  25. package/dist/config-center/http-client.d.ts +17 -9
  26. package/dist/config-center/http-client.js +27 -36
  27. package/dist/config-center/skills-mcp.d.ts +4 -1
  28. package/dist/config-center/skills-mcp.js +2 -2
  29. package/dist/config-invariants.d.ts +2 -2
  30. package/dist/config-invariants.js +9 -9
  31. package/dist/config-lkg.d.ts +47 -8
  32. package/dist/config-lkg.js +107 -20
  33. package/dist/config-provider.d.ts +8 -7
  34. package/dist/config-provider.js +5 -5
  35. package/dist/config-types.d.ts +20 -6
  36. package/dist/config.d.ts +9 -0
  37. package/dist/config.js +53 -18
  38. package/dist/fleet-client.d.ts +8 -8
  39. package/dist/fleet-client.js +7 -7
  40. package/dist/fleet-lease.d.ts +11 -11
  41. package/dist/fleet-lease.js +17 -13
  42. package/dist/hooks/hook-llm.d.ts +1 -1
  43. package/dist/hooks/hook-llm.js +3 -1
  44. package/dist/hosted-posture.d.ts +49 -93
  45. package/dist/hosted-posture.js +1 -25
  46. package/dist/memory-scope.d.ts +62 -11
  47. package/dist/memory-scope.js +32 -3
  48. package/dist/model-provider.d.ts +13 -0
  49. package/dist/model-provider.js +3 -0
  50. package/dist/model-route-endpoint.d.ts +82 -0
  51. package/dist/model-route-endpoint.js +55 -0
  52. package/dist/observability/secret-env-scrub.d.ts +2 -26
  53. package/dist/observability/secret-env-scrub.js +2 -2
  54. package/dist/plugins/remote-env-host.d.ts +3 -1
  55. package/dist/plugins/remote-env-host.js +3 -2
  56. package/dist/project-identity.d.ts +29 -0
  57. package/dist/project-identity.js +120 -0
  58. package/dist/project-memory.d.ts +28 -1
  59. package/dist/project-memory.js +22 -22
  60. package/dist/run-local.d.ts +14 -4
  61. package/dist/run-local.js +8 -7
  62. package/dist/runtime-caps-resolver.d.ts +8 -8
  63. package/dist/runtime-caps-resolver.js +16 -14
  64. package/dist/sealed-key.js +3 -7
  65. package/dist/server-secret-env.d.ts +41 -0
  66. package/dist/server-secret-env.js +18 -0
  67. package/dist/tool-approval.d.ts +48 -1
  68. package/dist/tool-approval.js +10 -3
  69. package/dist/trace/redact.d.ts +1 -1
  70. package/package.json +4 -3
@@ -15,6 +15,8 @@
15
15
  set -euo pipefail
16
16
 
17
17
  HERE="$(cd "$(dirname "$0")" && pwd)"
18
+ # `.env` 文法(写口 envf_representable + envf_encode / 读口 envf_canonical_line)的唯一定义 —— 写读两半从这一处取判据。
19
+ . "$HERE/lib/envf.sh"
18
20
  # 执行车道表的唯一读者(S-599)。定义在**最前面**:`--help` 的词表也从表渲(手抄的词表会在加一条
19
21
  # 车道时静默分叉),而 `-h` 在参数循环里就被处理,那时后面的函数还没定义。
20
22
  lanes() { python3 "$HERE/lib/lanes.py" "$@"; }
@@ -120,60 +122,27 @@ if [ -n "$CONF" ]; then
120
122
  fi
121
123
 
122
124
  WORK="$HOME/.sema-up/$PROJECT"; mkdir -p "$WORK"; chmod 700 "$HOME/.sema-up" "$WORK"
123
- # 凭据/配置文件的坐标 —— 与 `has_env` 一起提到这里(S-599):车道决策要在模型面之前读既有 .env
124
- # (冲突检测 + 「必需键已给过」判定),而生成段在几百行之后。纯搬家,零语义变。
125
+ # 凭据/配置文件的坐标(S-599 提到这里:车道决策要在模型面之前读既有 .env)。**读写原语全在 lib/envf.sh**
126
+ # (envf_get / envf_has / envf_load / envf_put / setk / setk_new / resetk,S-618):决策键严格读口、写口序列化、
127
+ # 数据版回读都是那一处的同一套文法;本脚本只调用,不再自带任何一份。物理行假设由 up 之前的
128
+ # `reconcile_render`(`docker compose config` 渲染对账,compose 唯一属主)兜底。
125
129
  ENVF="$WORK/.env"
126
- has_env() { [ -f "$ENVF" ] && grep -q "^$1=..*" "$ENVF"; }
127
- # ── 决策键的 `.env` 严格读口(7.96.0 合并复审)────────────────────────────────────────────────────
128
- # 决策(租户形 / 车道)读到的每个键 —— `REQUIRE_PRINCIPAL`、`REMOTE_EXEC`、所选车道的必需键 —— 只经这一只读口。
129
- # 它**不去猜** compose 会怎么读 `.env`:compose 的 dotenv 方言认 `export ` 前缀、键两侧空格、`K: v`、行内注释、
130
- # 重复键末次生效、`$VAR` 展开……在 bash 里复刻其中任何一部分都是一份手抄方言,漏一处就是「读口看到
131
- # false、容器拿到 true」(合并复审实证:`=false` 之后再来一行 `export …=true` ⇒ 本脚本落 host,容器拿到 true)。
132
- # 所以本读口是一条**闭文法**,fail-closed:
133
- # ① 凡是在任一方言下**可能**给 <键> 赋值的行(行首空白?+ `export `? + <键> + 空白? + `=` 或 `:`)至多一行;
134
- # ② 那一行必须是规范形 `<键>=<值>`:值 = 空 | 一个不含空白 / 引号 / `#` / `$` / 反斜杠 / 反引号的词 |
135
- # 一对同型引号包住的这样一个词(sema-up 自己写出的形全在这里面)。
136
- # 违反任一条 ⇒ 响亮拒并点名行号(**不回显行内容**:必需键里有凭据)。通过 ⇒ 打印值(剥掉那对引号;空 = 未设)。
137
- # 调用形恒为 `v="$(envf_get K)" || exit 1`(拒因走 stderr,退出码带回调用方)。
138
- # ⚠️ **已知未闭环(如实披露,不是本读口的承诺范围)**:本读口按**物理行**判,不认引号跨行 —— 另一个键的跨行
139
- # 单/双引号值里若有一行恰好长成 `K=<值>`,本读口会把它当成 K 的那一行。生成段的 setk / has_env / resetk
140
- # 同样按物理行判(早于本读口就是这样),两者对这种输入的误读方向一致:容器拿到的是 compose 的缺省值。
141
- # 统一形(另立一件):生成 .env 之后用 `docker compose config` 渲染一次,拿容器真正拿到的值与决策对账 ——
142
- # 那一步以 compose 自己为唯一属主,本读口与 setk 的物理行假设可以一起退役。
143
- ENVF_WORD='[^[:space:]"'"'"'#$`\\]'
144
- envf_get() {
145
- [ -f "$ENVF" ] || return 0
146
- local k="$1" hits n nums line v
147
- hits="$(grep -nE "^[[:space:]]*(export[[:space:]]+)?${k}[[:space:]]*[=:]" "$ENVF" || true)"
148
- [ -n "$hits" ] || return 0
149
- n="$(printf '%s\n' "$hits" | wc -l | tr -d '[:space:]')"
150
- nums="$(printf '%s\n' "$hits" | cut -d: -f1 | paste -sd, -)"
151
- if [ "$n" -gt 1 ]; then
152
- log "⛔ $ENVF 里可能给 $k 赋值的行有 $n 行(第 $nums 行)—— 决策键只许一行"
153
- log " ↳ 下一步:只留一行规范形 $k=<值>,其余删掉"
154
- log " ↳ 为什么不挑一行用:compose 读 .env 的规则(export 前缀、键旁空格、重复键末次生效)与本脚本不同,挑哪一行都可能不是容器真正拿到的那一行"
155
- return 1
156
- fi
157
- line="${hits#*:}"
158
- if ! printf '%s\n' "$line" | grep -qE "^${k}=(${ENVF_WORD}*|\"${ENVF_WORD}*\"|'${ENVF_WORD}*')\$"; then
159
- log "⛔ $ENVF 第 $nums 行给 $k 赋值,但不是本脚本认得的规范形"
160
- log " ↳ 下一步:改成 $k=<值>(行首顶格、不带 export、键旁无空格、值是一个词或一对同型引号包住的一个词、行尾无注释),或删掉这一行"
161
- log " ↳ 为什么不猜:这一行 compose 怎么读(注释、空白、展开)与本脚本怎么读一旦不一致,容器拿到的就不是决策读到的值"
162
- return 1
163
- fi
164
- v="${line#*=}"
165
- case "$v" in \"*\") v="${v#\"}"; v="${v%\"}" ;; \'*\') v="${v#\'}"; v="${v%\'}" ;; esac
166
- printf '%s' "$v"
167
- }
168
130
  # ── 部署事实(租户形与车道两个决策共用)────────────────────────────────────────────────
169
131
  # 事实 = `.env`(严格读口)∪ shell / `-f` 环境。compose 插值里 shell **优先于** --env-file(本脚本 teardown 段同一
170
132
  # 事实),所以两处都非空而不同 ⇒ 拒并点名两处 —— 不「取第一个非空」:放行的唯一形是两处至多一个值,无论谁赢,
171
133
  # 赢的都是它。设 FACT_VAL(可空 = 没有事实)与 FACT_SRC(来源描述)。直接调用,不进命令替换(拒要退整个脚本)。
172
134
  deploy_fact() {
173
- local k="$1" fenv fsh shsrc
135
+ local k="$1" fenv fsh shsrc shset
174
136
  fenv="$(envf_get "$k")" || exit 1
175
- eval "fsh=\${$k:-}"
137
+ # C(S-618):shell **导出为空串** ≠ shell 未设。compose 插值里 shell(即便是空串)优先于 --env-file,
138
+ # 所以导出空会**盖掉** .env 的值 —— 必须按「有值 = 空」参与冲突判定,不许当缺席。`${k+x}` 区分两者。
139
+ if eval "[ \"\${$k+x}\" = x ]"; then shset=1; eval "fsh=\"\${$k}\""; else shset=0; fsh=""; fi
176
140
  shsrc="${CONF:+-f $CONF / }shell 环境"
141
+ if [ "$shset" = 1 ] && [ -z "$fsh" ] && [ -n "$fenv" ]; then
142
+ log "⛔ 部署事实冲突:$ENVF 里 $k=$fenv,但 $shsrc 里把 $k **导出成了空串** —— compose 插值里 shell 优先于 --env-file,空串会盖掉 .env 的值(容器拿到空 = 走 compose 缺省,不是 $fenv)"
143
+ log " ↳ 下一步:unset $k(用 .env 里的 $fenv),或把 $k 显式设成你要的值"
144
+ exit 1
145
+ fi
177
146
  if [ -n "$fenv" ] && [ -n "$fsh" ] && [ "$fenv" != "$fsh" ]; then
178
147
  log "⛔ 部署事实冲突:$ENVF 里 $k=$fenv,$shsrc 里 $k=$fsh —— 两处不一致"
179
148
  log " ↳ 下一步:两处只留一处(或改成同一个值)"
@@ -200,11 +169,20 @@ log() { printf '[sema-up] %s\n' "$*" >&2; }
200
169
 
201
170
  # compose 入口解析:优先 docker compose(插件),缺了退 docker-compose(独立版)——
202
171
  # homebrew 裸 docker CLI 不带插件是真场景;都没有=报错带下一步
203
- dc() {
204
- if docker compose version >/dev/null 2>&1; then docker compose "$@"
205
- elif command -v docker-compose >/dev/null 2>&1; then docker-compose "$@"
172
+ # dc_argv 设 DC_ARGV(选中的 compose 入口);dc 调它 —— 渲染对账给运维的复现命令也从 DC_ARGV 渲,不另写一份。
173
+ dc_argv() {
174
+ if docker compose version >/dev/null 2>&1; then DC_ARGV=(docker compose)
175
+ elif command -v docker-compose >/dev/null 2>&1; then DC_ARGV=(docker-compose)
206
176
  else log "⛔ 没有可用的 compose(docker compose 插件/docker-compose 都缺)。下一步:装 Docker Desktop/OrbStack,或 brew install docker-compose"; exit 1; fi
207
177
  }
178
+ dc() { dc_argv; "${DC_ARGV[@]}" "$@"; }
179
+ # compose 的全局参数(-p / --env-file / -f)只在这里拼一次(相对 `$HERE/stack`):up / config 走 compose()、teardown 的
180
+ # down、渲染对账给运维的复现命令,都从同一张 COMPOSE_GLOBALS 取(S-646 ④:复现命令曾是手抄的第二份,漏了
181
+ # -f compose.dataroot.yaml 与 profile;teardown 是第三份)。数据根由调用方先定好 DATA_ROOT。
182
+ compose_globals() {
183
+ COMPOSE_GLOBALS=(--project-name "$PROJECT" --env-file "$ENVF" -f compose.yaml)
184
+ if [ -n "$DATA_ROOT" ]; then COMPOSE_GLOBALS+=(-f compose.dataroot.yaml); fi
185
+ }
208
186
 
209
187
  # ── 跨平台探针(Linux 目标机为主;macOS 开发机跑 docker 档也是真场景——npx 分发)────────────
210
188
  ram_mb() { if [ -r /proc/meminfo ]; then awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo
@@ -266,25 +244,27 @@ emit() { [ "$MODE_INTERACTIVE" = "json" ] && printf '%s\n' "$1" >&3 || log "$2";
266
244
  # ── teardown ─────────────────────────────────────────────────────────────────────────────────
267
245
  if $TEARDOWN; then
268
246
  cd "$HERE/stack"
269
- if [ -f "$WORK/.env" ]; then
270
- OV=""; grep -q '^DATA_ROOT_DIR=..*' "$WORK/.env" && OV="-f compose.dataroot.yaml"
247
+ if [ -f "$ENVF" ]; then
248
+ # 读 .env 一律经 lib 的严格读口(S-618 续):裸 grep|cut 会连编码形的单引号一起读出来。
249
+ # 数据根以 .env 登记的为准(卸载的是首装那一份,不看本次 --data-root);全局参数与 up 同一张表(compose_globals)。
250
+ DATA_ROOT="$(envf_get DATA_ROOT_DIR)" || exit 1
271
251
  # 全 profile down 会对未启用服务也做 config 校验——.env 缺 SEMA_*_IMAGE(该面没开过)时
272
252
  # 整个 down 失败(真踩:假「已卸载」)。补占位符只为过校验;shell env 优先级高于 --env-file
273
- grep -q '^SEMA_SERVER_IMAGE=..*' "$WORK/.env" || export SEMA_SERVER_IMAGE="teardown-placeholder"
274
- grep -q '^SEMA_REGISTRY_IMAGE=..*' "$WORK/.env" || export SEMA_REGISTRY_IMAGE="teardown-placeholder"
275
- if ! dc --project-name "$PROJECT" --env-file "$WORK/.env" -f compose.yaml $OV \
253
+ envf_has SEMA_SERVER_IMAGE || export SEMA_SERVER_IMAGE="teardown-placeholder"
254
+ envf_has SEMA_REGISTRY_IMAGE || export SEMA_REGISTRY_IMAGE="teardown-placeholder"
255
+ compose_globals
256
+ if ! dc "${COMPOSE_GLOBALS[@]}" \
276
257
  --profile data --profile pg --profile s3 --profile agent --profile registry --profile git \
277
258
  down $($TEARDOWN_VOLUMES && echo -v); then
278
259
  log "⛔ 卸载未完全成功。下一步:docker ps -a --filter label=com.docker.compose.project=$PROJECT 手查残留"
279
260
  exit 1
280
261
  fi
281
262
  log "已卸载 project=$PROJECT $($TEARDOWN_VOLUMES && echo '(含数据卷)' || echo '(数据卷保留:pgdata/miniodata/registrydata/gitdata)')"
282
- if $TEARDOWN_VOLUMES && [ -n "$OV" ]; then
283
- DR="$(grep '^DATA_ROOT_DIR=' "$WORK/.env" | head -1 | cut -d= -f2-)"
284
- # ${DR} 必须带花括号:后面紧跟中文破折号,bash 3.2(macOS 系统壳)会把多字节并进变量名(真踩)
285
- log "⚠ 数据根在大盘 bind 目录 ${DR}——卷对象已删,盘上文件还在;确认不要了再手动 rm -rf"
263
+ if $TEARDOWN_VOLUMES && [ -n "$DATA_ROOT" ]; then
264
+ # ${DATA_ROOT} 必须带花括号:后面紧跟中文破折号,bash 3.2(macOS 系统壳)会把多字节并进变量名(真踩)
265
+ log "⚠ 数据根在大盘 bind 目录 ${DATA_ROOT}——卷对象已删,盘上文件还在;确认不要了再手动 rm -rf"
286
266
  fi
287
- else log "没有找到 $WORK/.env,无可卸载"; fi
267
+ else log "没有找到 $ENVF,无可卸载"; fi
288
268
  exit 0
289
269
  fi
290
270
 
@@ -507,7 +487,10 @@ if [ "$REGION" = "auto" ]; then
507
487
  REGION=$(awk -v a="$t_cn" -v b="$t_gl" 'BEGIN{print (a<b)?"cn":"global"}')
508
488
  fi
509
489
  HOSTIP=$(host_ip)
510
- log "region=$REGION profile=$PROFILE users=$USERS sandbox=${SANDBOX_LANE:-n/a} lane=${LANE_REMOTE_EXEC:-n/a} registry=$REGISTRY_ENABLED git=$GIT_MODE"
490
+ # E(S-618):进度日志只打**已校验的决策词**,不回显未过校验的原始输入。REGISTRY_ENABLED 到这里还是运维
491
+ # argv 的原样值(真正的校验在 resetk 写口),曾把带标记的 --registry 值抢先泄进 stderr(test srv796 实测 NEW-5)——
492
+ # 收成 on/off 决策词。region/profile/users/lane/git 到这里都已定死(车道块 / git 词校验都在上面)。
493
+ log "region=$REGION profile=$PROFILE users=$USERS sandbox=${SANDBOX_LANE:-n/a} lane=${LANE_REMOTE_EXEC:-n/a} registry=$([ "$REGISTRY_ENABLED" = "true" ] && echo on || echo off) git=$GIT_MODE"
511
494
 
512
495
  # ── 数据盘选择(多盘机自动选大容量盘;--data-root 显式优先;macOS 不自动——bind 要经 VM 挂载,
513
496
  # 显式给了才用)。首装落 .env 后不漂移:换盘=先 --teardown-volumes 迁数据再重装。──────────────
@@ -539,12 +522,12 @@ case "$PROFILE" in full-single|local-plus) COMPOSE_PROFILES="$COMPOSE_PROFILES,a
539
522
  [ "$GIT_MODE" = "provision" ] && COMPOSE_PROFILES="$COMPOSE_PROFILES,git"
540
523
  COMPOSE_PROFILES="${COMPOSE_PROFILES#,}"; COMPOSE_PROFILES="${COMPOSE_PROFILES%,}"
541
524
  case ",$COMPOSE_PROFILES," in *,agent,*)
542
- if [ -z "${SEMA_SERVER_IMAGE:-}" ] && ! has_env SEMA_SERVER_IMAGE; then
525
+ if [ -z "${SEMA_SERVER_IMAGE:-}" ] && ! envf_has SEMA_SERVER_IMAGE; then
543
526
  log "⛔ $PROFILE 档需要 sema-server 镜像。下一步:-f 配置里加 SEMA_SERVER_IMAGE=<registry>/sema-server:<tag>(默认=ghcr.io/sema-agent/sema-server:<tag> 公开仓匿名可拉;CN 兜底=内网 Gitea 仓;SWR 已弃用);镜像分发=D4"
544
527
  exit 1
545
528
  fi ;;
546
529
  esac
547
- if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "${SEMA_REGISTRY_IMAGE:-}" ] && ! has_env SEMA_REGISTRY_IMAGE; then
530
+ if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "${SEMA_REGISTRY_IMAGE:-}" ] && ! envf_has SEMA_REGISTRY_IMAGE; then
548
531
  log "⛔ registry=true(现在是各档默认)需要 sema-registry 镜像。下一步(三选一):"
549
532
  log " ① --registry-image=ghcr.io/clayboby/sema-registry:<tag>(GHCR/Hub 公开仓,匿名可拉;SWR 已弃用)"
550
533
  log " ② -f 配置里加 SEMA_REGISTRY_IMAGE=<你的仓>/sema-registry:<tag>"
@@ -592,25 +575,10 @@ PG_TUNE_FLAGS="-c max_connections=100 -c shared_buffers=${SB}MB -c effective_cac
592
575
  # ── .env 生成(幂等:已存在则只补缺,凭据绝不轮换;0600) ───────────────────────────────────
593
576
  genpw() { openssl rand -hex 24; }
594
577
  touch "$ENVF"; chmod 600 "$ENVF"
595
- # ── .env 的**唯一**写口:一次写入恰好一个物理行 ────────────────────────────────────────────────
596
- # 病(7.96.0 修复验证轮 [high]):`--model-key=$'k\nREQUIRE_PRINCIPAL=true'` 经旧 setk 原样写成两行 —— 第二行是
597
- # 决策检查**之后**才出现在 .env 里的一条 REQUIRE_PRINCIPAL,紧接着的 `setk REQUIRE_PRINCIPAL` 见键在就不写 ⇒
598
- # 容器拿到多租 + host。严格读口只管「读」;写口与它是同一条约定的两半:.env 一行一条赋值。值里带换行 / 回车
599
- # ⇒ **写之前**拒(先判后动:resetk 的删旧行也排在判定之后),拒因不回显值(可能是凭据)。
600
- envf_check() {
601
- case "$2" in
602
- *$'\n'*|*$'\r'*)
603
- log "⛔ 要写进 $ENVF 的 $1 的值里有换行 / 回车 —— .env 一行一条赋值,换行后的内容会被 compose 当成另一条赋值"
604
- log " ↳ 下一步:去掉值里的换行(查 $1 对应的命令行参数或 -f 配置项)后重跑"
605
- exit 1 ;;
606
- esac
607
- }
608
- envf_put() { envf_check "$1" "$2"; printf '%s=%s\n' "$1" "$2" >> "$ENVF"; }
609
- setk() { grep -q "^$1=" "$ENVF" || envf_put "$1" "$2"; }
610
- # setk_new:同 setk,但新写入时返回 0(summary 用来判断「密码只显示一次」)
611
- setk_new() { if grep -q "^$1=" "$ENVF"; then return 1; else envf_put "$1" "$2"; return 0; fi; }
612
- # resetk:非凭据键每次重写(翻转要落 .env,不然 teardown/汇总口径漂)。先判后删:拒了就一行都不动。
613
- resetk() { envf_check "$1" "$2"; grep -v "^$1=" "$ENVF" > "$ENVF.tmp" || true; mv "$ENVF.tmp" "$ENVF"; chmod 600 "$ENVF"; envf_put "$1" "$2"; }
578
+ # 写口 = lib/envf.sh 的 envf_put(setk / setk_new / resetk 都落到它):值按同一文法序列化 —— 安全裸词原样,
579
+ # 含空白 / 特殊字符 / 反斜杠的走整值单引号编码形(compose 读成字面量、闭引号后不续解析;被 source 也不执行);换行 /
580
+ # 回车 / 单引号 / 以奇数个反斜杠结尾 ⇒ 写之前响亮拒,点名键、不回显值,「下一步」按拒因给。setk 判「键在不在」也走严格读口:旧文件里该键那一行不是
581
+ # 规范形(比如同物理行闭引号带出的二次赋值)⇒ 响亮拒,不再「见行在就跳过」把毒行原样留着(test srv796 RT1)。
614
582
  setk SEMA_PROJECT "$PROJECT"
615
583
  setk PG_PASSWORD "$(genpw)"
616
584
  setk MINIO_PASSWORD "$(genpw)"
@@ -621,8 +589,9 @@ setk MINIO_CONSOLE_PORT "$MINIO_CONSOLE_PORT"
621
589
  setk SERVER_PORT "$SERVER_PORT"
622
590
  # data-center 数据面对外服务(agent 在别处连);full-single 数据面仅回环、只有 server 对外
623
591
  if [ "$PROFILE" = "data-center" ]; then setk PG_BIND "0.0.0.0"; setk MINIO_BIND "0.0.0.0"; else setk PG_BIND "127.0.0.1"; setk MINIO_BIND "127.0.0.1"; fi
624
- # PG_TUNE_FLAGS 每次重算(规格可能变;非凭据,轮换无害);引号:source 与 compose env-file 双方兼容
625
- resetk PG_TUNE_FLAGS "\"$PG_TUNE_FLAGS\""
592
+ # PG_TUNE_FLAGS 每次重算(规格可能变;非凭据,轮换无害)。值带空白 ⇒ 写口按文法自动整值单引号
593
+ # (原来手工加双引号=在写口之外自己拼一套引用,正是本批要消灭的第二套文法);compose 读成同一个字面量。
594
+ resetk PG_TUNE_FLAGS "$PG_TUNE_FLAGS"
626
595
  if [ "$PROFILE" = "full-single" ] || [ "$PROFILE" = "local-plus" ]; then
627
596
  setk MODEL_GATEWAY_BASEURL "$GATEWAY"
628
597
  setk MODEL_ID "$MODEL"
@@ -703,9 +672,10 @@ elif [ "$GIT_MODE" = "connect" ]; then
703
672
  resetk GIT_TOKEN "$GIT_TOKEN"
704
673
  fi
705
674
 
706
- # DATA_ROOT 落定(首装写 .env 后为准——重跑换值会造出一套空卷,数据看似丢失)
707
- if has_env DATA_ROOT_DIR; then
708
- _stored="$(grep '^DATA_ROOT_DIR=' "$ENVF" | head -1 | cut -d= -f2-)"
675
+ # DATA_ROOT 落定(首装写 .env 后为准——重跑换值会造出一套空卷,数据看似丢失)。首装值经严格读口解码回读
676
+ # (S-618 续:路径含空格时写口落整值单引号形,旧的裸 grep|cut 会把引号当成路径的一部分)。
677
+ _stored="$(envf_get DATA_ROOT_DIR)" || exit 1
678
+ if [ -n "$_stored" ]; then
709
679
  if [ -n "$DATA_ROOT" ] && [ "$DATA_ROOT" != "$_stored" ]; then
710
680
  log "⚠ --data-root=$DATA_ROOT 与首装选定的 $_stored 不同——沿用首装值(换盘流程:--teardown-volumes 后迁数据重装)"
711
681
  fi
@@ -717,8 +687,85 @@ if [ -n "$DATA_ROOT" ]; then
717
687
  for d in pgdata miniodata registrydata gitdata; do mkdir -p "$DATA_ROOT/$d"; done
718
688
  fi
719
689
 
690
+ # A(S-618):`.env` 生成完毕 —— 把 setk 持久化的**生效端口**回读进 shell 变量(替代旧 source 的这半)。
691
+ # 重跑省略端口旗时,容器绑定的是首装端口(.env),本脚本后续的 API 调用 / 健康检查 / 汇总必须用同一份;
692
+ # 首装(或 .env 无该键)时回读跳过、保留 shell 缺省。凭据在各自用点单独回读(见下 envf_load)。
693
+ envf_load SERVER_PORT PG_PORT MINIO_PORT MINIO_CONSOLE_PORT REGISTRY_PORT GIT_HTTP_PORT GIT_SSH_PORT
694
+
720
695
  command -v docker >/dev/null || { log "⛔ 未装 docker。下一步:curl -fsSL https://get.docker.com | sh(cn region 用镜像站,如 https://mirrors.aliyun.com/docker-ce)"; exit 1; }
721
- compose() { ( cd "$HERE/stack" && dc --project-name "$PROJECT" --env-file "$ENVF" -f compose.yaml ${DATA_ROOT:+-f compose.dataroot.yaml} "$@" ); }
696
+ compose() { compose_globals; ( cd "$HERE/stack" && dc "${COMPOSE_GLOBALS[@]}" "$@" ); }
697
+ # shq —— 单引号引用一个词(复现命令要能原样粘贴:路径里可能有空格)。
698
+ shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
699
+
700
+ # ── D:渲染对账(S-618 的类闭环;生成 .env 之后、compose up 之前)──────────────────────────────────
701
+ # .env 的写口 / 读口都按物理行判,认不出 compose dotenv 的跨行引号值等边角;唯一能确知「容器真正拿到
702
+ # 什么」的是 compose 自己。所以在 up 之前跑一次 `compose config`——与 up **同一组 -f / profile / 同一 shell
703
+ # 环境**(于是运维导出的空串等也算进来)——从渲染结果取 agent 服务的 REQUIRE_PRINCIPAL / REMOTE_EXEC /
704
+ # REQUIRE_EXECUTION_LANE,与本次决策逐值对比,不一致 ⇒ 拒并点名渲染值与决策值。**以 compose 为唯一属主**。
705
+ # docker 不可达在上面 `command -v docker` 那道已 fail-loud 拦下;config 本身失败(YAML/插值坏)也拒,
706
+ # 不新铸降级臂:未经对账就不 up。只有 agent 面有这三个决策键,别的档跳过。
707
+ #
708
+ # 拒因分两种、各给各的出路(S-656;7.98.0 曾把 stderr 并进 stdout 当 JSON 解析 ⇒ compose 的任何警告都让它 traceback,
709
+ # 拒因还冒充「不一致」):
710
+ # · 渲染结果解析失败 —— compose 退出码非零,或退出 0 但 stdout 不是可解析的 compose JSON:没拿到渲染值,谈不上比对;
711
+ # · 渲染值与决策值不一致 —— 拿到了、逐值比对不上。
712
+ # JSON 只从 stdout 取;stderr 只数行数作诊断,**不转印**(compose 解析 `.env` 失败时报文会原样引用那一行,凭据就在行里;
713
+ # 插值警告里的变量名也可能是凭据的一段)。
714
+ RECON_PY='
715
+ import json, sys
716
+ want = {"REQUIRE_PRINCIPAL": sys.argv[1], "REMOTE_EXEC": sys.argv[2], "REQUIRE_EXECUTION_LANE": sys.argv[3]}
717
+ try:
718
+ d = json.loads(sys.stdin.read())
719
+ env = d["services"]["server"].get("environment") or {}
720
+ if not isinstance(env, dict):
721
+ raise TypeError("environment")
722
+ except Exception:
723
+ raise SystemExit(4) # 渲染结果解析失败:不是 compose JSON / 没有 server 服务
724
+ bad = []
725
+ for k, w in want.items():
726
+ got = env.get(k)
727
+ got = "" if got is None else str(got)
728
+ if got != w:
729
+ bad.append("%s:渲染=%r 决策=%r" % (k, got, w))
730
+ if bad:
731
+ sys.stderr.write("[sema-up] ↳ 明细:" + " | ".join(bad) + "\n")
732
+ raise SystemExit(3) # 渲染值与决策值不一致
733
+ '
734
+ reconcile_render() {
735
+ case ",$COMPOSE_PROFILES," in *,agent,*) : ;; *) return 0 ;; esac
736
+ local exp_rp="$REQUIRE_PRINCIPAL_WANT" exp_re="$LANE_REMOTE_EXEC" exp_rl rendered errf nerr rc=0 prc=0 repro a
737
+ exp_rl="$(printf '%s\n' "$LANE_ENV" | sed -n 's/^REQUIRE_EXECUTION_LANE=//p')"
738
+ # 复现命令:与这次 config 同一个 compose 入口、同一张全局参数表、同一组 profile(从源渲,不手抄)。
739
+ dc_argv; compose_globals
740
+ repro="(cd $(shq "$HERE/stack") && COMPOSE_PROFILES=$(shq "$COMPOSE_PROFILES") ${DC_ARGV[*]}"
741
+ for a in "${COMPOSE_GLOBALS[@]}"; do repro="$repro $(shq "$a")"; done
742
+ repro="$repro 'config' '--format' 'json')"
743
+ errf="$(umask 077 && mktemp "$WORK/.recon-stderr.XXXXXX")" || { log "⛔ 渲染对账:在 $WORK 建不了临时文件,无法对账 —— 拒绝在未对账的情况下 up"; exit 1; }
744
+ rendered="$(COMPOSE_PROFILES="$COMPOSE_PROFILES" compose config --format json 2>"$errf")" || rc=$?
745
+ nerr="$(wc -l < "$errf" | tr -d '[:space:]')"; rm -f "$errf"
746
+ if [ "$rc" -ne 0 ]; then
747
+ log "⛔ 渲染对账:渲染结果解析失败 —— docker compose config 失败(退出码 $rc;它在 stderr 输出了 $nerr 行,本脚本不转印),无法确认容器会拿到什么 —— 拒绝在未对账的情况下 up"
748
+ log " ↳ 下一步:在本机亲跑 $repro 看 compose 的报文(本脚本不转印它:.env 解析失败的报文会原样引用那一行,可能带凭据);常见成因是手改过的 .env 行"
749
+ exit 1
750
+ fi
751
+ printf '%s' "$rendered" | python3 -c "$RECON_PY" "$exp_rp" "$exp_re" "$exp_rl" || prc=$?
752
+ case "$prc" in
753
+ 0) : ;;
754
+ 3)
755
+ log "⛔ 渲染对账:渲染值与决策值不一致 —— compose 渲染出的容器决策值与本次决策对不上;以 compose 为唯一属主,拒绝 up"
756
+ log " ↳ 下一步:检查 $ENVF 里 REQUIRE_PRINCIPAL / REMOTE_EXEC / REQUIRE_EXECUTION_LANE,以及 shell / -f 里是否**导出**了同名变量(compose 插值 shell 优先于 --env-file,导出为空会盖掉 .env);亲跑 $repro 可看容器 environment"
757
+ exit 1 ;;
758
+ 4)
759
+ log "⛔ 渲染对账:渲染结果解析失败 —— docker compose config 退出 0,但 stdout 不是可解析的 compose JSON(或其中没有 server 服务;compose 另在 stderr 输出了 $nerr 行,本脚本不转印),无法确认容器会拿到什么 —— 拒绝在未对账的情况下 up"
760
+ log " ↳ 下一步:亲跑 $repro 看 stdout 是不是一份 JSON;不是 ⇒ 这个 compose 入口不支持 --format json 或往 stdout 多打了东西(${DC_ARGV[*]}),换 Docker Compose v2(docker compose 插件)后重跑"
761
+ exit 1 ;;
762
+ *)
763
+ log "⛔ 渲染对账:对账脚本自身失败(python3 退出码 $prc),无法确认容器会拿到什么 —— 拒绝在未对账的情况下 up"
764
+ log " ↳ 下一步:确认本机 python3 可用(python3 -c 'import json'),然后重跑"
765
+ exit 1 ;;
766
+ esac
767
+ log "渲染对账通过:REQUIRE_PRINCIPAL=$exp_rp REMOTE_EXEC=$exp_re REQUIRE_EXECUTION_LANE=$exp_rl(compose 渲染逐值一致)$([ "$nerr" -gt 0 ] && printf ';compose 另在 stderr 输出了 %s 行(多为插值警告;本脚本不转印,up 时 compose 会自己打出来)' "$nerr")"
768
+ }
722
769
 
723
770
  # ── 外接端点连接检测(装配前 fail-loud;只验兼容可用,不校验证书——内网自签是常态)────────────
724
771
  if [ -n "$PG_URL" ]; then
@@ -740,7 +787,8 @@ GIT_MANUAL_NOTES=()
740
787
  if [ "$GIT_MODE" = "provision" ]; then
741
788
  log "git 面:先起 Gitea(引导管理员/token/OAuth 要在 registry 之前)"
742
789
  COMPOSE_PROFILES=git compose up -d --wait git || { log "⛔ Gitea 起不来。下一步:docker compose -p $PROJECT logs git;镜像拉不动时 --gitea-image=<可达镜像仓>/gitea:1.24"; exit 1; }
743
- . "$ENVF"
790
+ # A(S-618):不 source .env —— 回读 sema-up 自己生成、没进 shell 变量的凭据(管理员账号/口令)。
791
+ envf_load GIT_ADMIN_USER GIT_ADMIN_PASSWORD
744
792
  GIT_CTR="${PROJECT}-git-1"
745
793
  GIT_API="http://127.0.0.1:${GIT_HTTP_PORT}/api/v1"
746
794
  # 引导管理员(幂等:已存在即跳过;user 1000=镜像内 git 用户)
@@ -752,7 +800,7 @@ if [ "$GIT_MODE" = "provision" ]; then
752
800
  log "Gitea 管理员 $GIT_ADMIN_USER 已建"
753
801
  fi
754
802
  # 管理员 access token(只生成一次;丢了 .env 就换个名再生成,旧 token 在 Gitea 里可手动清)
755
- if ! has_env GIT_ADMIN_TOKEN; then
803
+ if ! envf_has GIT_ADMIN_TOKEN; then
756
804
  TOK_NAME="sema-up-$(openssl rand -hex 3)"
757
805
  TOK=$(curl -m10 -s -u "$GIT_ADMIN_USER:$GIT_ADMIN_PASSWORD" -X POST "$GIT_API/users/$GIT_ADMIN_USER/tokens" \
758
806
  -H 'Content-Type: application/json' -d "{\"name\":\"$TOK_NAME\",\"scopes\":[\"all\"]}" \
@@ -762,9 +810,13 @@ except Exception: print("")')
762
810
  [ -n "$TOK" ] || { log "⛔ Gitea access token 生成失败。下一步:curl -u <admin> $GIT_API/users/$GIT_ADMIN_USER/tokens 手查;或看 docker compose -p $PROJECT logs git"; exit 1; }
763
811
  setk GIT_ADMIN_TOKEN "$TOK"
764
812
  fi
765
- . "$ENVF"
813
+ envf_load GIT_ADMIN_TOKEN # A:回读刚生成/已存的 token,不 source .env
814
+ # OAuth 接线只对它被创建时的 git 目标有效(锚 = REGISTRY_GITEA_BASE_URL):目标变了 ⇒ 上一目标的接线作废、
815
+ # 本次重建(建不成 = registry 走表单登录),不把旧身份源静默带进这一次(车MH codex r2 [high],两条路径同一条规则)。
816
+ envf_invalidate_unless REGISTRY_GITEA_BASE_URL "http://$HOSTIP:$GIT_HTTP_PORT" REGISTRY_GITEA_OAUTH_ID REGISTRY_GITEA_OAUTH_SECRET
817
+ if [ "$ENVF_INVALIDATED" = 1 ]; then log "git 面:上一次登记的 OAuth 接线属于另一个 git 目标 —— 已作废,本次重建"; fi
766
818
  # registry 登录集成:自动建 OAuth app(真自动化,不是纸面;client secret 只在创建响应里给一次)
767
- if [ "$REGISTRY_ENABLED" = "true" ] && ! has_env REGISTRY_GITEA_OAUTH_ID; then
819
+ if [ "$REGISTRY_ENABLED" = "true" ] && ! envf_has REGISTRY_GITEA_OAUTH_ID; then
768
820
  REDIRECT="http://$HOSTIP:$REGISTRY_PORT/api/auth/oauth/callback"
769
821
  OAUTH=$(curl -m10 -s -H "Authorization: token $GIT_ADMIN_TOKEN" -X POST "$GIT_API/user/applications/oauth2" \
770
822
  -H 'Content-Type: application/json' \
@@ -776,27 +828,33 @@ except Exception: print("")')
776
828
  try: print(json.load(sys.stdin).get("client_secret",""))
777
829
  except Exception: print("")')
778
830
  if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
779
- setk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
780
- setk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
781
- setk REGISTRY_GITEA_BASE_URL "http://$HOSTIP:$GIT_HTTP_PORT"
831
+ resetk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
832
+ resetk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
833
+ resetk REGISTRY_GITEA_BASE_URL "http://$HOSTIP:$GIT_HTTP_PORT"
782
834
  log "Gitea→registry OAuth 登录已接(app=sema-registry)"
783
835
  else
784
836
  GIT_MANUAL_NOTES+=("Gitea OAuth app 自动创建失败——registry 仍可表单登录;手动接法:Gitea 设置→应用→新建 OAuth2(回调 $REDIRECT),把 client id/secret 写进 $ENVF 的 REGISTRY_GITEA_OAUTH_ID/SECRET+REGISTRY_GITEA_BASE_URL 后重跑本命令")
785
837
  fi
786
838
  fi
787
839
  elif [ "$GIT_MODE" = "connect" ]; then
788
- . "$ENVF"
840
+ # A(S-618):不 source .env —— GIT_URL / GIT_TOKEN 就是本次命令行/‑f 传进来的 shell 变量,直接用。
841
+ # (旧代码 source 是历史遗留:那时 source 会拿 .env 旧值盖掉命令行新值,故先 resetk 再 source 找回;
842
+ # 不 source 之后新值本就在 shell 里,连这道找回都不需要了。)
789
843
  # 连通验证(fail-loud:接不上就别继续,别部署完才发现 git 面是死的)。
790
844
  # 任意三方 git 都支持:探测链 Gitea API → GitHub API → 通用 git(ls-remote/HTTP 可达);
791
845
  # 证书一律不校验(-k / http.sslVerify=false)——内网自签是常态,验的是兼容可用性。
792
846
  GIT_KIND=""
847
+ # OAuth 接线只对它被创建时的 git 目标有效(锚 = REGISTRY_GITEA_BASE_URL):目标变了 ⇒ 上一目标的接线作废、
848
+ # 本次重建(建不成 = registry 走表单登录),不把旧身份源静默带进这一次(车MH codex r2 [high],两条路径同一条规则)。
849
+ envf_invalidate_unless REGISTRY_GITEA_BASE_URL "$GIT_URL" REGISTRY_GITEA_OAUTH_ID REGISTRY_GITEA_OAUTH_SECRET
850
+ if [ "$ENVF_INVALIDATED" = 1 ]; then log "git=connect:上一次登记的 OAuth 接线属于另一个 git 目标 —— 已作废,本次按新目标重建"; fi
793
851
  GT_VER=$(curl -k -m10 -s -H "Authorization: token $GIT_TOKEN" "$GIT_URL/api/v1/version" | python3 -c 'import json,sys
794
852
  try: print(json.load(sys.stdin).get("version",""))
795
853
  except Exception: print("")')
796
854
  if [ -n "$GT_VER" ]; then
797
855
  GIT_KIND="gitea"
798
856
  log "git=connect:Gitea $GT_VER 验证通过"
799
- if [ "$REGISTRY_ENABLED" = "true" ] && ! has_env REGISTRY_GITEA_OAUTH_ID; then
857
+ if [ "$REGISTRY_ENABLED" = "true" ] && ! envf_has REGISTRY_GITEA_OAUTH_ID; then
800
858
  REDIRECT="http://$HOSTIP:$REGISTRY_PORT/api/auth/oauth/callback"
801
859
  OAUTH=$(curl -k -m10 -s -H "Authorization: token $GIT_TOKEN" -X POST "$GIT_URL/api/v1/user/applications/oauth2" \
802
860
  -H 'Content-Type: application/json' \
@@ -808,9 +866,9 @@ except Exception: print("")')
808
866
  try: print(json.load(sys.stdin).get("client_secret",""))
809
867
  except Exception: print("")')
810
868
  if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
811
- setk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
812
- setk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
813
- setk REGISTRY_GITEA_BASE_URL "$GIT_URL"
869
+ resetk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
870
+ resetk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
871
+ resetk REGISTRY_GITEA_BASE_URL "$GIT_URL"
814
872
  log "已有 Gitea→registry OAuth 登录已接(app=sema-registry)"
815
873
  else
816
874
  GIT_MANUAL_NOTES+=("OAuth app 自动创建失败(token 可能缺 write:user scope)——手动接法:该 Gitea 设置→应用→新建 OAuth2(回调 $REDIRECT),把 client id/secret 写进 $ENVF 的 REGISTRY_GITEA_OAUTH_ID/SECRET+REGISTRY_GITEA_BASE_URL 后重跑本命令")
@@ -846,17 +904,20 @@ except Exception: print("")')
846
904
  fi
847
905
 
848
906
  # ── 装配(Layer 1 compose;profiles 映射形态) ────────────────────────────────────────────────
907
+ # D(S-618):up 之前先 `compose config` 渲染对账 —— 容器真正拿到的决策键与本次决策逐值对齐才 up。
908
+ reconcile_render
849
909
  log "装配:docker compose profiles=$COMPOSE_PROFILES project=$PROJECT"
850
910
  COMPOSE_PROFILES="$COMPOSE_PROFILES" compose up -d --wait --remove-orphans \
851
911
  || { log "⛔ compose up 失败。下一步:docker compose -p $PROJECT logs(常见:镜像拉取失败→检查镜像引用/docker login;端口占用→--port 换口)"; exit 1; }
852
912
  # 建桶(幂等,一次性动作归 Layer 0;只对本地 MinIO——外接 S3 已在连接检测时确保)
853
- . "$ENVF"
913
+ envf_load MINIO_PASSWORD # A:回读生成的 MinIO 口令(建桶用),不 source .env
854
914
  case ",$COMPOSE_PROFILES," in *,s3,*)
855
915
  docker run --rm --network "${PROJECT}_default" -e MC_HOST_x="http://sema:${MINIO_PASSWORD}@minio:9000" "${MC_IMAGE:-minio/mc:latest}" mb --ignore-existing x/workspaces x/blobs x/backups >/dev/null || { log "⛔ MinIO 建桶失败。下一步:docker compose -p $PROJECT logs minio"; exit 1; } ;;
856
916
  esac
857
917
 
858
918
  # ── 验证(装后即验,§3d smoke 的子集;全量随时可跑 smoke.sh --project=$PROJECT)──────────────
859
- . "$ENVF"
919
+ # A:回读验证 / 汇总要用的凭据与登记值(不 source .env);缺的键回读成空,由下游 :- 缺省处置。
920
+ envf_load MINIO_PASSWORD REGISTRY_ADMIN_USER REGISTRY_ADMIN_PASSWORD REGISTRY_GITEA_BASE_URL GIT_ADMIN_USER GIT_ADMIN_PASSWORD
860
921
  V_PG="n/a" V_BK="n/a"
861
922
  case ",$COMPOSE_PROFILES," in *,pg,*)
862
923
  V_PG=$(docker exec "${PROJECT}-pg-1" psql -U sema -d sema -tAc "SHOW shared_buffers" 2>/dev/null || echo FAIL) ;;
@@ -5,6 +5,9 @@
5
5
  # 远程 URL 模式: smoke.sh --url=http://host:8090 --token=T (引擎面:health/auth/真任务)
6
6
  # 每项 OK/FAIL 一行;FAIL 带原因+建议(§3d-2 报错规范);exit 0=全绿。
7
7
  set -u
8
+ HERE="$(cd "$(dirname "$0")" && pwd)"
9
+ # `.env` 的文法与读原语只在 lib/envf.sh 定义(S-618 续):本脚本读 `.env` 只经 envf_load,不 source。
10
+ . "$HERE/lib/envf.sh"
8
11
 
9
12
  PROJECT="" URL="" TOKEN=""
10
13
  for a in "$@"; do case "$a" in
@@ -20,7 +23,11 @@ fail() { printf '[FAIL] %s — %s\n ↳ 建议:%s\n' "$1" "$2" "$3"; FAILS=
20
23
  if [ -n "$PROJECT" ]; then
21
24
  ENVF="$HOME/.sema-up/$PROJECT/.env"
22
25
  [ -r "$ENVF" ] || { echo "找不到 $ENVF(先跑 sema-up.sh)"; exit 1; }
23
- . "$ENVF"
26
+ # A(S-618 续):不 source .env(值里的 $() / 反引号 / ; 是数据,不是代码)—— 只经严格读口取本脚本用到的键;
27
+ # 读到非规范行(比如旧写口留下的裸 `$(…)`)⇒ 响亮拒并点名行号,不猜、不执行。
28
+ envf_load SERVICE_AUTH_TOKEN DEPLOY_PROFILE SERVER_PORT MINIO_PASSWORD \
29
+ AGENT_PG_HOST AGENT_PG_USER PG_PASSWORD AGENT_PG_PORT AGENT_PG_DATABASE AGENT_MINIO_ENDPOINT AGENT_MINIO_USER \
30
+ REGISTRY_ENABLED REGISTRY_PORT GIT_MODE GIT_HTTP_PORT GIT_ADMIN_TOKEN GIT_ADMIN_USER
24
31
  TOKEN="${TOKEN:-${SERVICE_AUTH_TOKEN:-}}"
25
32
  # 引擎面只在带 agent 的档默认探(data-center 没有引擎,探 8090 必假红);--url 显式给了照跑
26
33
  case "${DEPLOY_PROFILE:-full-single}" in