@sema-agent/server 7.97.0 → 7.98.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.
- package/MIGRATION.md +5 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +6 -0
- package/deploy/sema-up/cluster-up.sh +8 -4
- package/deploy/sema-up/kube-up.sh +40 -15
- package/deploy/sema-up/lib/envf.sh +173 -0
- package/deploy/sema-up/sema-up.sh +113 -94
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/boot/config-center.js +2 -2
- package/dist/boot/resolve-spec.js +9 -4
- package/dist/brain.js +28 -18
- package/dist/config-catalog.js +4 -4
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -1
- package/dist/config-types.d.ts +12 -2
- package/dist/config.d.ts +9 -0
- package/dist/config.js +42 -5
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- package/dist/memory-scope.d.ts +62 -11
- package/dist/memory-scope.js +32 -3
- package/dist/model-provider.d.ts +13 -0
- package/dist/model-provider.js +3 -0
- package/dist/model-route-endpoint.d.ts +82 -0
- package/dist/model-route-endpoint.js +55 -0
- package/dist/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +8 -5
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
# `.env` 文法(写口 encode / 读口 ENVF_WORD 与规范行判据)的唯一定义 —— 写读两半从这一处取判据。
|
|
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
|
-
# 凭据/配置文件的坐标
|
|
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
|
-
|
|
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 " ↳ 下一步:两处只留一处(或改成同一个值)"
|
|
@@ -266,13 +235,14 @@ emit() { [ "$MODE_INTERACTIVE" = "json" ] && printf '%s\n' "$1" >&3 || log "$2";
|
|
|
266
235
|
# ── teardown ─────────────────────────────────────────────────────────────────────────────────
|
|
267
236
|
if $TEARDOWN; then
|
|
268
237
|
cd "$HERE/stack"
|
|
269
|
-
if [ -f "$
|
|
270
|
-
|
|
238
|
+
if [ -f "$ENVF" ]; then
|
|
239
|
+
# 读 .env 一律经 lib 的严格读口(S-618 续):裸 grep|cut 会连编码形的单引号一起读出来。
|
|
240
|
+
OV=""; envf_has DATA_ROOT_DIR && OV="-f compose.dataroot.yaml"
|
|
271
241
|
# 全 profile down 会对未启用服务也做 config 校验——.env 缺 SEMA_*_IMAGE(该面没开过)时
|
|
272
242
|
# 整个 down 失败(真踩:假「已卸载」)。补占位符只为过校验;shell env 优先级高于 --env-file
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if ! dc --project-name "$PROJECT" --env-file "$
|
|
243
|
+
envf_has SEMA_SERVER_IMAGE || export SEMA_SERVER_IMAGE="teardown-placeholder"
|
|
244
|
+
envf_has SEMA_REGISTRY_IMAGE || export SEMA_REGISTRY_IMAGE="teardown-placeholder"
|
|
245
|
+
if ! dc --project-name "$PROJECT" --env-file "$ENVF" -f compose.yaml $OV \
|
|
276
246
|
--profile data --profile pg --profile s3 --profile agent --profile registry --profile git \
|
|
277
247
|
down $($TEARDOWN_VOLUMES && echo -v); then
|
|
278
248
|
log "⛔ 卸载未完全成功。下一步:docker ps -a --filter label=com.docker.compose.project=$PROJECT 手查残留"
|
|
@@ -280,11 +250,11 @@ if $TEARDOWN; then
|
|
|
280
250
|
fi
|
|
281
251
|
log "已卸载 project=$PROJECT $($TEARDOWN_VOLUMES && echo '(含数据卷)' || echo '(数据卷保留:pgdata/miniodata/registrydata/gitdata)')"
|
|
282
252
|
if $TEARDOWN_VOLUMES && [ -n "$OV" ]; then
|
|
283
|
-
DR="$(
|
|
253
|
+
DR="$(envf_get DATA_ROOT_DIR)" || exit 1
|
|
284
254
|
# ${DR} 必须带花括号:后面紧跟中文破折号,bash 3.2(macOS 系统壳)会把多字节并进变量名(真踩)
|
|
285
255
|
log "⚠ 数据根在大盘 bind 目录 ${DR}——卷对象已删,盘上文件还在;确认不要了再手动 rm -rf"
|
|
286
256
|
fi
|
|
287
|
-
else log "没有找到 $
|
|
257
|
+
else log "没有找到 $ENVF,无可卸载"; fi
|
|
288
258
|
exit 0
|
|
289
259
|
fi
|
|
290
260
|
|
|
@@ -507,7 +477,10 @@ if [ "$REGION" = "auto" ]; then
|
|
|
507
477
|
REGION=$(awk -v a="$t_cn" -v b="$t_gl" 'BEGIN{print (a<b)?"cn":"global"}')
|
|
508
478
|
fi
|
|
509
479
|
HOSTIP=$(host_ip)
|
|
510
|
-
|
|
480
|
+
# E(S-618):进度日志只打**已校验的决策词**,不回显未过校验的原始输入。REGISTRY_ENABLED 到这里还是运维
|
|
481
|
+
# argv 的原样值(真正的校验在 resetk 写口),曾把带标记的 --registry 值抢先泄进 stderr(test srv796 实测 NEW-5)——
|
|
482
|
+
# 收成 on/off 决策词。region/profile/users/lane/git 到这里都已定死(车道块 / git 词校验都在上面)。
|
|
483
|
+
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
484
|
|
|
512
485
|
# ── 数据盘选择(多盘机自动选大容量盘;--data-root 显式优先;macOS 不自动——bind 要经 VM 挂载,
|
|
513
486
|
# 显式给了才用)。首装落 .env 后不漂移:换盘=先 --teardown-volumes 迁数据再重装。──────────────
|
|
@@ -539,12 +512,12 @@ case "$PROFILE" in full-single|local-plus) COMPOSE_PROFILES="$COMPOSE_PROFILES,a
|
|
|
539
512
|
[ "$GIT_MODE" = "provision" ] && COMPOSE_PROFILES="$COMPOSE_PROFILES,git"
|
|
540
513
|
COMPOSE_PROFILES="${COMPOSE_PROFILES#,}"; COMPOSE_PROFILES="${COMPOSE_PROFILES%,}"
|
|
541
514
|
case ",$COMPOSE_PROFILES," in *,agent,*)
|
|
542
|
-
if [ -z "${SEMA_SERVER_IMAGE:-}" ] && !
|
|
515
|
+
if [ -z "${SEMA_SERVER_IMAGE:-}" ] && ! envf_has SEMA_SERVER_IMAGE; then
|
|
543
516
|
log "⛔ $PROFILE 档需要 sema-server 镜像。下一步:-f 配置里加 SEMA_SERVER_IMAGE=<registry>/sema-server:<tag>(默认=ghcr.io/sema-agent/sema-server:<tag> 公开仓匿名可拉;CN 兜底=内网 Gitea 仓;SWR 已弃用);镜像分发=D4"
|
|
544
517
|
exit 1
|
|
545
518
|
fi ;;
|
|
546
519
|
esac
|
|
547
|
-
if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "${SEMA_REGISTRY_IMAGE:-}" ] && !
|
|
520
|
+
if [ "$REGISTRY_ENABLED" = "true" ] && [ -z "${SEMA_REGISTRY_IMAGE:-}" ] && ! envf_has SEMA_REGISTRY_IMAGE; then
|
|
548
521
|
log "⛔ registry=true(现在是各档默认)需要 sema-registry 镜像。下一步(三选一):"
|
|
549
522
|
log " ① --registry-image=ghcr.io/clayboby/sema-registry:<tag>(GHCR/Hub 公开仓,匿名可拉;SWR 已弃用)"
|
|
550
523
|
log " ② -f 配置里加 SEMA_REGISTRY_IMAGE=<你的仓>/sema-registry:<tag>"
|
|
@@ -592,25 +565,10 @@ PG_TUNE_FLAGS="-c max_connections=100 -c shared_buffers=${SB}MB -c effective_cac
|
|
|
592
565
|
# ── .env 生成(幂等:已存在则只补缺,凭据绝不轮换;0600) ───────────────────────────────────
|
|
593
566
|
genpw() { openssl rand -hex 24; }
|
|
594
567
|
touch "$ENVF"; chmod 600 "$ENVF"
|
|
595
|
-
#
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
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"; }
|
|
568
|
+
# 写口 = lib/envf.sh 的 envf_put(setk / setk_new / resetk 都落到它):值按同一文法序列化 —— 安全裸词原样,
|
|
569
|
+
# 含空白 / 特殊字符的走整值单引号编码形(compose 读成字面量、闭引号后不续解析;被 source 也不执行);换行 /
|
|
570
|
+
# 回车 / 单引号 ⇒ 写之前响亮拒,点名键、不回显值。setk 判「键在不在」也走严格读口:旧文件里该键那一行不是
|
|
571
|
+
# 规范形(比如同物理行闭引号带出的二次赋值)⇒ 响亮拒,不再「见行在就跳过」把毒行原样留着(test srv796 RT1)。
|
|
614
572
|
setk SEMA_PROJECT "$PROJECT"
|
|
615
573
|
setk PG_PASSWORD "$(genpw)"
|
|
616
574
|
setk MINIO_PASSWORD "$(genpw)"
|
|
@@ -621,8 +579,9 @@ setk MINIO_CONSOLE_PORT "$MINIO_CONSOLE_PORT"
|
|
|
621
579
|
setk SERVER_PORT "$SERVER_PORT"
|
|
622
580
|
# data-center 数据面对外服务(agent 在别处连);full-single 数据面仅回环、只有 server 对外
|
|
623
581
|
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 每次重算(规格可能变;非凭据,轮换无害)
|
|
625
|
-
|
|
582
|
+
# PG_TUNE_FLAGS 每次重算(规格可能变;非凭据,轮换无害)。值带空白 ⇒ 写口按文法自动整值单引号
|
|
583
|
+
# (原来手工加双引号=在写口之外自己拼一套引用,正是本批要消灭的第二套文法);compose 读成同一个字面量。
|
|
584
|
+
resetk PG_TUNE_FLAGS "$PG_TUNE_FLAGS"
|
|
626
585
|
if [ "$PROFILE" = "full-single" ] || [ "$PROFILE" = "local-plus" ]; then
|
|
627
586
|
setk MODEL_GATEWAY_BASEURL "$GATEWAY"
|
|
628
587
|
setk MODEL_ID "$MODEL"
|
|
@@ -703,9 +662,10 @@ elif [ "$GIT_MODE" = "connect" ]; then
|
|
|
703
662
|
resetk GIT_TOKEN "$GIT_TOKEN"
|
|
704
663
|
fi
|
|
705
664
|
|
|
706
|
-
# DATA_ROOT 落定(首装写 .env 后为准——重跑换值会造出一套空卷,数据看似丢失)
|
|
707
|
-
|
|
708
|
-
|
|
665
|
+
# DATA_ROOT 落定(首装写 .env 后为准——重跑换值会造出一套空卷,数据看似丢失)。首装值经严格读口解码回读
|
|
666
|
+
# (S-618 续:路径含空格时写口落整值单引号形,旧的裸 grep|cut 会把引号当成路径的一部分)。
|
|
667
|
+
_stored="$(envf_get DATA_ROOT_DIR)" || exit 1
|
|
668
|
+
if [ -n "$_stored" ]; then
|
|
709
669
|
if [ -n "$DATA_ROOT" ] && [ "$DATA_ROOT" != "$_stored" ]; then
|
|
710
670
|
log "⚠ --data-root=$DATA_ROOT 与首装选定的 $_stored 不同——沿用首装值(换盘流程:--teardown-volumes 后迁数据重装)"
|
|
711
671
|
fi
|
|
@@ -717,9 +677,54 @@ if [ -n "$DATA_ROOT" ]; then
|
|
|
717
677
|
for d in pgdata miniodata registrydata gitdata; do mkdir -p "$DATA_ROOT/$d"; done
|
|
718
678
|
fi
|
|
719
679
|
|
|
680
|
+
# A(S-618):`.env` 生成完毕 —— 把 setk 持久化的**生效端口**回读进 shell 变量(替代旧 source 的这半)。
|
|
681
|
+
# 重跑省略端口旗时,容器绑定的是首装端口(.env),本脚本后续的 API 调用 / 健康检查 / 汇总必须用同一份;
|
|
682
|
+
# 首装(或 .env 无该键)时回读跳过、保留 shell 缺省。凭据在各自用点单独回读(见下 envf_load)。
|
|
683
|
+
envf_load SERVER_PORT PG_PORT MINIO_PORT MINIO_CONSOLE_PORT REGISTRY_PORT GIT_HTTP_PORT GIT_SSH_PORT
|
|
684
|
+
|
|
720
685
|
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
686
|
compose() { ( cd "$HERE/stack" && dc --project-name "$PROJECT" --env-file "$ENVF" -f compose.yaml ${DATA_ROOT:+-f compose.dataroot.yaml} "$@" ); }
|
|
722
687
|
|
|
688
|
+
# ── D:渲染对账(S-618 的类闭环;生成 .env 之后、compose up 之前)──────────────────────────────────
|
|
689
|
+
# .env 的写口 / 读口都按物理行判,认不出 compose dotenv 的跨行引号值等边角;唯一能确知「容器真正拿到
|
|
690
|
+
# 什么」的是 compose 自己。所以在 up 之前跑一次 `compose config`——与 up **同一组 -f / profile / 同一 shell
|
|
691
|
+
# 环境**(于是运维导出的空串等也算进来)——从渲染结果取 agent 服务的 REQUIRE_PRINCIPAL / REMOTE_EXEC /
|
|
692
|
+
# REQUIRE_EXECUTION_LANE,与本次决策逐值对比,不一致 ⇒ 拒并点名渲染值与决策值。**以 compose 为唯一属主**。
|
|
693
|
+
# docker 不可达在上面 `command -v docker` 那道已 fail-loud 拦下;config 本身失败(YAML/插值坏)也拒,
|
|
694
|
+
# 不新铸降级臂:未经对账就不 up。只有 agent 面有这三个决策键,别的档跳过。
|
|
695
|
+
reconcile_render() {
|
|
696
|
+
case ",$COMPOSE_PROFILES," in *,agent,*) : ;; *) return 0 ;; esac
|
|
697
|
+
local exp_rp="$REQUIRE_PRINCIPAL_WANT" exp_re="$LANE_REMOTE_EXEC" exp_rl rendered
|
|
698
|
+
exp_rl="$(printf '%s\n' "$LANE_ENV" | sed -n 's/^REQUIRE_EXECUTION_LANE=//p')"
|
|
699
|
+
rendered="$(COMPOSE_PROFILES="$COMPOSE_PROFILES" compose config --format json 2>&1)" || {
|
|
700
|
+
local rc=$?
|
|
701
|
+
# 7.98.0 合并复审 F3:compose 的报文**不回显** —— 它解析 .env 失败时会原样引用那一行(凭据就在行里)。只报退出码 + 出路。
|
|
702
|
+
log "⛔ 渲染对账:docker compose config 失败(退出码 $rc),无法确认容器会拿到什么 —— 拒绝在未对账的情况下 up"
|
|
703
|
+
log " ↳ 下一步:在本机亲跑 docker compose --project-name $PROJECT --env-file $ENVF -f $HERE/stack/compose.yaml config 看报文(本脚本不转印它:.env 解析失败的报文会原样引用那一行,可能带凭据);常见成因是手改过的 .env 行"
|
|
704
|
+
exit 1
|
|
705
|
+
}
|
|
706
|
+
RECON_JSON="$rendered" python3 - "$exp_rp" "$exp_re" "$exp_rl" <<'PY' || {
|
|
707
|
+
import json, os, sys
|
|
708
|
+
want = {"REQUIRE_PRINCIPAL": sys.argv[1], "REMOTE_EXEC": sys.argv[2], "REQUIRE_EXECUTION_LANE": sys.argv[3]}
|
|
709
|
+
d = json.loads(os.environ["RECON_JSON"])
|
|
710
|
+
env = ((d.get("services") or {}).get("server") or {}).get("environment") or {}
|
|
711
|
+
bad = []
|
|
712
|
+
for k, w in want.items():
|
|
713
|
+
got = env.get(k)
|
|
714
|
+
got = "" if got is None else str(got)
|
|
715
|
+
if got != w:
|
|
716
|
+
bad.append("%s:渲染=%r 决策=%r" % (k, got, w))
|
|
717
|
+
if bad:
|
|
718
|
+
sys.stderr.write("[sema-up] ↳ 明细:" + " | ".join(bad) + "\n")
|
|
719
|
+
raise SystemExit(3)
|
|
720
|
+
PY
|
|
721
|
+
log "⛔ 渲染对账失败:compose 渲染出的容器决策值与本次决策不一致 —— 以 compose 为唯一属主,拒绝 up"
|
|
722
|
+
log " ↳ 下一步:检查 .env 里 REQUIRE_PRINCIPAL / REMOTE_EXEC / REQUIRE_EXECUTION_LANE,以及 shell / -f 里是否**导出**了同名变量(compose 插值 shell 优先于 --env-file,导出为空会盖掉 .env)"
|
|
723
|
+
exit 1
|
|
724
|
+
}
|
|
725
|
+
log "渲染对账通过:REQUIRE_PRINCIPAL=$exp_rp REMOTE_EXEC=$exp_re REQUIRE_EXECUTION_LANE=$exp_rl(compose 渲染逐值一致)"
|
|
726
|
+
}
|
|
727
|
+
|
|
723
728
|
# ── 外接端点连接检测(装配前 fail-loud;只验兼容可用,不校验证书——内网自签是常态)────────────
|
|
724
729
|
if [ -n "$PG_URL" ]; then
|
|
725
730
|
log "外接 PG 连接检测(host=$EXT_PG_HOST:$EXT_PG_PORT db=$EXT_PG_DB)"
|
|
@@ -740,7 +745,8 @@ GIT_MANUAL_NOTES=()
|
|
|
740
745
|
if [ "$GIT_MODE" = "provision" ]; then
|
|
741
746
|
log "git 面:先起 Gitea(引导管理员/token/OAuth 要在 registry 之前)"
|
|
742
747
|
COMPOSE_PROFILES=git compose up -d --wait git || { log "⛔ Gitea 起不来。下一步:docker compose -p $PROJECT logs git;镜像拉不动时 --gitea-image=<可达镜像仓>/gitea:1.24"; exit 1; }
|
|
743
|
-
.
|
|
748
|
+
# A(S-618):不 source .env —— 回读 sema-up 自己生成、没进 shell 变量的凭据(管理员账号/口令)。
|
|
749
|
+
envf_load GIT_ADMIN_USER GIT_ADMIN_PASSWORD
|
|
744
750
|
GIT_CTR="${PROJECT}-git-1"
|
|
745
751
|
GIT_API="http://127.0.0.1:${GIT_HTTP_PORT}/api/v1"
|
|
746
752
|
# 引导管理员(幂等:已存在即跳过;user 1000=镜像内 git 用户)
|
|
@@ -752,7 +758,7 @@ if [ "$GIT_MODE" = "provision" ]; then
|
|
|
752
758
|
log "Gitea 管理员 $GIT_ADMIN_USER 已建"
|
|
753
759
|
fi
|
|
754
760
|
# 管理员 access token(只生成一次;丢了 .env 就换个名再生成,旧 token 在 Gitea 里可手动清)
|
|
755
|
-
if !
|
|
761
|
+
if ! envf_has GIT_ADMIN_TOKEN; then
|
|
756
762
|
TOK_NAME="sema-up-$(openssl rand -hex 3)"
|
|
757
763
|
TOK=$(curl -m10 -s -u "$GIT_ADMIN_USER:$GIT_ADMIN_PASSWORD" -X POST "$GIT_API/users/$GIT_ADMIN_USER/tokens" \
|
|
758
764
|
-H 'Content-Type: application/json' -d "{\"name\":\"$TOK_NAME\",\"scopes\":[\"all\"]}" \
|
|
@@ -762,9 +768,13 @@ except Exception: print("")')
|
|
|
762
768
|
[ -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
769
|
setk GIT_ADMIN_TOKEN "$TOK"
|
|
764
770
|
fi
|
|
765
|
-
.
|
|
771
|
+
envf_load GIT_ADMIN_TOKEN # A:回读刚生成/已存的 token,不 source .env
|
|
772
|
+
# OAuth 接线只对它被创建时的 git 目标有效(锚 = REGISTRY_GITEA_BASE_URL):目标变了 ⇒ 上一目标的接线作废、
|
|
773
|
+
# 本次重建(建不成 = registry 走表单登录),不把旧身份源静默带进这一次(车MH codex r2 [high],两条路径同一条规则)。
|
|
774
|
+
envf_invalidate_unless REGISTRY_GITEA_BASE_URL "http://$HOSTIP:$GIT_HTTP_PORT" REGISTRY_GITEA_OAUTH_ID REGISTRY_GITEA_OAUTH_SECRET
|
|
775
|
+
if [ "$ENVF_INVALIDATED" = 1 ]; then log "git 面:上一次登记的 OAuth 接线属于另一个 git 目标 —— 已作废,本次重建"; fi
|
|
766
776
|
# registry 登录集成:自动建 OAuth app(真自动化,不是纸面;client secret 只在创建响应里给一次)
|
|
767
|
-
if [ "$REGISTRY_ENABLED" = "true" ] && !
|
|
777
|
+
if [ "$REGISTRY_ENABLED" = "true" ] && ! envf_has REGISTRY_GITEA_OAUTH_ID; then
|
|
768
778
|
REDIRECT="http://$HOSTIP:$REGISTRY_PORT/api/auth/oauth/callback"
|
|
769
779
|
OAUTH=$(curl -m10 -s -H "Authorization: token $GIT_ADMIN_TOKEN" -X POST "$GIT_API/user/applications/oauth2" \
|
|
770
780
|
-H 'Content-Type: application/json' \
|
|
@@ -776,27 +786,33 @@ except Exception: print("")')
|
|
|
776
786
|
try: print(json.load(sys.stdin).get("client_secret",""))
|
|
777
787
|
except Exception: print("")')
|
|
778
788
|
if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
789
|
+
resetk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
|
|
790
|
+
resetk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
|
|
791
|
+
resetk REGISTRY_GITEA_BASE_URL "http://$HOSTIP:$GIT_HTTP_PORT"
|
|
782
792
|
log "Gitea→registry OAuth 登录已接(app=sema-registry)"
|
|
783
793
|
else
|
|
784
794
|
GIT_MANUAL_NOTES+=("Gitea OAuth app 自动创建失败——registry 仍可表单登录;手动接法:Gitea 设置→应用→新建 OAuth2(回调 $REDIRECT),把 client id/secret 写进 $ENVF 的 REGISTRY_GITEA_OAUTH_ID/SECRET+REGISTRY_GITEA_BASE_URL 后重跑本命令")
|
|
785
795
|
fi
|
|
786
796
|
fi
|
|
787
797
|
elif [ "$GIT_MODE" = "connect" ]; then
|
|
788
|
-
.
|
|
798
|
+
# A(S-618):不 source .env —— GIT_URL / GIT_TOKEN 就是本次命令行/‑f 传进来的 shell 变量,直接用。
|
|
799
|
+
# (旧代码 source 是历史遗留:那时 source 会拿 .env 旧值盖掉命令行新值,故先 resetk 再 source 找回;
|
|
800
|
+
# 不 source 之后新值本就在 shell 里,连这道找回都不需要了。)
|
|
789
801
|
# 连通验证(fail-loud:接不上就别继续,别部署完才发现 git 面是死的)。
|
|
790
802
|
# 任意三方 git 都支持:探测链 Gitea API → GitHub API → 通用 git(ls-remote/HTTP 可达);
|
|
791
803
|
# 证书一律不校验(-k / http.sslVerify=false)——内网自签是常态,验的是兼容可用性。
|
|
792
804
|
GIT_KIND=""
|
|
805
|
+
# OAuth 接线只对它被创建时的 git 目标有效(锚 = REGISTRY_GITEA_BASE_URL):目标变了 ⇒ 上一目标的接线作废、
|
|
806
|
+
# 本次重建(建不成 = registry 走表单登录),不把旧身份源静默带进这一次(车MH codex r2 [high],两条路径同一条规则)。
|
|
807
|
+
envf_invalidate_unless REGISTRY_GITEA_BASE_URL "$GIT_URL" REGISTRY_GITEA_OAUTH_ID REGISTRY_GITEA_OAUTH_SECRET
|
|
808
|
+
if [ "$ENVF_INVALIDATED" = 1 ]; then log "git=connect:上一次登记的 OAuth 接线属于另一个 git 目标 —— 已作废,本次按新目标重建"; fi
|
|
793
809
|
GT_VER=$(curl -k -m10 -s -H "Authorization: token $GIT_TOKEN" "$GIT_URL/api/v1/version" | python3 -c 'import json,sys
|
|
794
810
|
try: print(json.load(sys.stdin).get("version",""))
|
|
795
811
|
except Exception: print("")')
|
|
796
812
|
if [ -n "$GT_VER" ]; then
|
|
797
813
|
GIT_KIND="gitea"
|
|
798
814
|
log "git=connect:Gitea $GT_VER 验证通过"
|
|
799
|
-
if [ "$REGISTRY_ENABLED" = "true" ] && !
|
|
815
|
+
if [ "$REGISTRY_ENABLED" = "true" ] && ! envf_has REGISTRY_GITEA_OAUTH_ID; then
|
|
800
816
|
REDIRECT="http://$HOSTIP:$REGISTRY_PORT/api/auth/oauth/callback"
|
|
801
817
|
OAUTH=$(curl -k -m10 -s -H "Authorization: token $GIT_TOKEN" -X POST "$GIT_URL/api/v1/user/applications/oauth2" \
|
|
802
818
|
-H 'Content-Type: application/json' \
|
|
@@ -808,9 +824,9 @@ except Exception: print("")')
|
|
|
808
824
|
try: print(json.load(sys.stdin).get("client_secret",""))
|
|
809
825
|
except Exception: print("")')
|
|
810
826
|
if [ -n "$OA_ID" ] && [ -n "$OA_SECRET" ]; then
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
827
|
+
resetk REGISTRY_GITEA_OAUTH_ID "$OA_ID"
|
|
828
|
+
resetk REGISTRY_GITEA_OAUTH_SECRET "$OA_SECRET"
|
|
829
|
+
resetk REGISTRY_GITEA_BASE_URL "$GIT_URL"
|
|
814
830
|
log "已有 Gitea→registry OAuth 登录已接(app=sema-registry)"
|
|
815
831
|
else
|
|
816
832
|
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 +862,20 @@ except Exception: print("")')
|
|
|
846
862
|
fi
|
|
847
863
|
|
|
848
864
|
# ── 装配(Layer 1 compose;profiles 映射形态) ────────────────────────────────────────────────
|
|
865
|
+
# D(S-618):up 之前先 `compose config` 渲染对账 —— 容器真正拿到的决策键与本次决策逐值对齐才 up。
|
|
866
|
+
reconcile_render
|
|
849
867
|
log "装配:docker compose profiles=$COMPOSE_PROFILES project=$PROJECT"
|
|
850
868
|
COMPOSE_PROFILES="$COMPOSE_PROFILES" compose up -d --wait --remove-orphans \
|
|
851
869
|
|| { log "⛔ compose up 失败。下一步:docker compose -p $PROJECT logs(常见:镜像拉取失败→检查镜像引用/docker login;端口占用→--port 换口)"; exit 1; }
|
|
852
870
|
# 建桶(幂等,一次性动作归 Layer 0;只对本地 MinIO——外接 S3 已在连接检测时确保)
|
|
853
|
-
.
|
|
871
|
+
envf_load MINIO_PASSWORD # A:回读生成的 MinIO 口令(建桶用),不 source .env
|
|
854
872
|
case ",$COMPOSE_PROFILES," in *,s3,*)
|
|
855
873
|
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
874
|
esac
|
|
857
875
|
|
|
858
876
|
# ── 验证(装后即验,§3d smoke 的子集;全量随时可跑 smoke.sh --project=$PROJECT)──────────────
|
|
859
|
-
.
|
|
877
|
+
# A:回读验证 / 汇总要用的凭据与登记值(不 source .env);缺的键回读成空,由下游 :- 缺省处置。
|
|
878
|
+
envf_load MINIO_PASSWORD REGISTRY_ADMIN_USER REGISTRY_ADMIN_PASSWORD REGISTRY_GITEA_BASE_URL GIT_ADMIN_USER GIT_ADMIN_PASSWORD
|
|
860
879
|
V_PG="n/a" V_BK="n/a"
|
|
861
880
|
case ",$COMPOSE_PROFILES," in *,pg,*)
|
|
862
881
|
V_PG=$(docker exec "${PROJECT}-pg-1" psql -U sema -d sema -tAc "SHOW shared_buffers" 2>/dev/null || echo FAIL) ;;
|
package/deploy/sema-up/smoke.sh
CHANGED
|
@@ -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
|
-
.
|
|
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
|
|
@@ -10,7 +10,7 @@ import { applyCenterPlugins } from "../capabilities/center-plugins.js";
|
|
|
10
10
|
import { applyCatalogToSource, withPromptArtifactBackfill } from "../capabilities/center-prompts.js";
|
|
11
11
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
|
|
12
12
|
import { centerScenarios, centerScenarioDetails } from "../capabilities/scenarios.js";
|
|
13
|
-
import { configLkgEnabled } from "../config.js";
|
|
13
|
+
import { configLkgEnabled, isRosterGateArmed } from "../config.js";
|
|
14
14
|
import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../config-lkg.js";
|
|
15
15
|
import { centerManagedConfigKeys } from "../config-center/apply-effective.js";
|
|
16
16
|
import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../config-provider.js";
|
|
@@ -74,7 +74,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
74
74
|
...(config.configProvider === "local" ? { localDir: localRoot } : {}),
|
|
75
75
|
})
|
|
76
76
|
: undefined;
|
|
77
|
-
const rosterGateArmed = cc
|
|
77
|
+
const rosterGateArmed = isRosterGateArmed(cc);
|
|
78
78
|
const modelReadyState = { ready: !rosterGateArmed };
|
|
79
79
|
const markRosterLanded = (eff) => {
|
|
80
80
|
if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
|
|
@@ -25,7 +25,7 @@ import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../rou
|
|
|
25
25
|
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
26
26
|
import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
|
|
27
27
|
import { HttpError, encodeCheckpointScope } from "../security.js";
|
|
28
|
-
import { memorySpecForRequest } from "../memory-scope.js";
|
|
28
|
+
import { memoryProjectKeyFor, memorySpecForRequest } from "../memory-scope.js";
|
|
29
29
|
import { singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
30
30
|
import { a2aForScenario, mcpForScenario } from "../config-center/facade.js";
|
|
31
31
|
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
|
|
@@ -43,6 +43,12 @@ export function createResolveSpec(ctx) {
|
|
|
43
43
|
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
|
|
44
44
|
assertGuardPatternsUsable(config);
|
|
45
45
|
const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, { durableEnabled, singleUserAutoAcceptBaseline });
|
|
46
|
+
const realTaskRootOf = (auth) => {
|
|
47
|
+
if (isSandboxPathAdjudicationLane(config.remoteExec?.provider))
|
|
48
|
+
return undefined;
|
|
49
|
+
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
50
|
+
return sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
|
|
51
|
+
};
|
|
46
52
|
if (onlySensitiveBaseline)
|
|
47
53
|
logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
|
|
48
54
|
const resolveSpec = async (body, req, auth, opts) => {
|
|
@@ -391,7 +397,7 @@ export function createResolveSpec(ctx) {
|
|
|
391
397
|
})(),
|
|
392
398
|
...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
|
|
393
399
|
memory: memoryEngine
|
|
394
|
-
? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
|
|
400
|
+
? memorySpecForRequest(auth?.memoryScope, memoryProjectKeyFor(config, auth?.memoryScope, realTaskRootOf(auth)), body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
|
|
395
401
|
: undefined,
|
|
396
402
|
...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(effectiveMemoryPersistenceCapable(config)),
|
|
397
403
|
tools: ((base) => {
|
|
@@ -450,8 +456,7 @@ export function createResolveSpec(ctx) {
|
|
|
450
456
|
};
|
|
451
457
|
const pathAdjudication = hostSemanticsLane
|
|
452
458
|
? (() => {
|
|
453
|
-
const
|
|
454
|
-
const realRoot = sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
|
|
459
|
+
const realRoot = realTaskRootOf(auth);
|
|
455
460
|
const gateCwd = realRoot ?? join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted");
|
|
456
461
|
return {
|
|
457
462
|
env: new NodeExecutionEnv({ cwd: gateCwd }),
|
package/dist/brain.js
CHANGED
|
@@ -2,7 +2,8 @@ import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain,
|
|
|
2
2
|
import { routeKeyOf } from "@sema-agent/core";
|
|
3
3
|
import { buildDefaultResilience } from "./config-types.js";
|
|
4
4
|
import { resolveModelApiKey } from "./key-resolver.js";
|
|
5
|
-
import {
|
|
5
|
+
import { dispatchBrainLane } from "./model-provider.js";
|
|
6
|
+
import { openaiRouteBaseUrl } from "./model-route-endpoint.js";
|
|
6
7
|
import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
|
|
7
8
|
function isOffDeploymentRoot(baseUrl, config) {
|
|
8
9
|
if (!baseUrl)
|
|
@@ -10,11 +11,17 @@ function isOffDeploymentRoot(baseUrl, config) {
|
|
|
10
11
|
const entry = routeKeyOf(baseUrl);
|
|
11
12
|
if (entry === "")
|
|
12
13
|
return false;
|
|
13
|
-
return !
|
|
14
|
+
return !deploymentGatewayRoots(config).some((u) => routeKeyOf(u) === entry);
|
|
15
|
+
}
|
|
16
|
+
function deploymentGatewayRoots(config) {
|
|
17
|
+
return config.gatewayBaseUrl !== undefined ? [config.gatewayBaseUrl, ...config.gatewayFallbackUrls] : [];
|
|
18
|
+
}
|
|
19
|
+
function deploymentGatewayKey(config) {
|
|
20
|
+
return config.gatewayBaseUrl !== undefined ? config.gatewayApiKey : undefined;
|
|
14
21
|
}
|
|
15
22
|
function degradeDeclaredRoot(fallbackBaseUrl, config) {
|
|
16
|
-
const armed = Boolean(config
|
|
17
|
-
return armed ? config.gatewayBaseUrl : (fallbackBaseUrl
|
|
23
|
+
const armed = Boolean(deploymentGatewayKey(config)) && isOffDeploymentRoot(fallbackBaseUrl, config);
|
|
24
|
+
return armed ? config.gatewayBaseUrl : openaiRouteBaseUrl(fallbackBaseUrl, config.gatewayBaseUrl);
|
|
18
25
|
}
|
|
19
26
|
export const FAST_FAIL_MAX_RETRIES = 2;
|
|
20
27
|
function constructRouteBrain(label, sourceUrls, construct) {
|
|
@@ -41,12 +48,12 @@ export function createBrain(config, deps = {}) {
|
|
|
41
48
|
: fastFailTopology
|
|
42
49
|
? { maxRetries: FAST_FAIL_MAX_RETRIES }
|
|
43
50
|
: {};
|
|
44
|
-
const routes =
|
|
51
|
+
const routes = deploymentGatewayRoots(config);
|
|
45
52
|
const openaiBrains = routes.map((baseUrl, i) => {
|
|
46
53
|
const brain = constructRouteBrain(i === 0 ? "gateway route" : `gateway fallback route[${i - 1}]`, [baseUrl], () => createOpenAIBrain({
|
|
47
54
|
baseUrl,
|
|
48
55
|
get apiKey() {
|
|
49
|
-
return config
|
|
56
|
+
return deploymentGatewayKey(config);
|
|
50
57
|
},
|
|
51
58
|
...retryCap,
|
|
52
59
|
fetchImpl,
|
|
@@ -62,7 +69,11 @@ export function createBrain(config, deps = {}) {
|
|
|
62
69
|
})
|
|
63
70
|
: brain;
|
|
64
71
|
});
|
|
65
|
-
const gatewayBrain = openaiBrains.length
|
|
72
|
+
const gatewayBrain = openaiBrains.length === 0
|
|
73
|
+
? constructRouteBrain("openai route (no gateway declared)", [], () => createOpenAIBrain({ ...retryCap, fetchImpl, ...timeouts }))
|
|
74
|
+
: openaiBrains.length > 1
|
|
75
|
+
? createFailoverBrain(openaiBrains)
|
|
76
|
+
: openaiBrains[0];
|
|
66
77
|
const anthropicCfg = config.anthropic;
|
|
67
78
|
const anthropicBrain = anthropicCfg
|
|
68
79
|
? constructRouteBrain("anthropic route", [anthropicCfg.baseUrl], () => createAnthropicBrain({
|
|
@@ -86,9 +97,8 @@ export function createBrain(config, deps = {}) {
|
|
|
86
97
|
if (fallbackModel) {
|
|
87
98
|
const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
|
|
88
99
|
resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const fallback = fallbackLane === "anthropic" && anthropicBrain
|
|
100
|
+
const anthropicLaneHop = dispatchBrainLane(fallbackModel.provider, anthropicBrain !== undefined) === "anthropic";
|
|
101
|
+
const fallback = anthropicLaneHop && anthropicBrain
|
|
92
102
|
?
|
|
93
103
|
anthropicBrain
|
|
94
104
|
: constructRouteBrain(`degrade target "${degradeTo}"`, [fallbackModel.baseUrl, config.gatewayBaseUrl], () => createOpenAIBrain({
|
|
@@ -96,7 +106,7 @@ export function createBrain(config, deps = {}) {
|
|
|
96
106
|
return degradeDeclaredRoot(fallbackModel.baseUrl, config);
|
|
97
107
|
},
|
|
98
108
|
get apiKey() {
|
|
99
|
-
return config
|
|
109
|
+
return deploymentGatewayKey(config) || undefined;
|
|
100
110
|
},
|
|
101
111
|
...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
|
|
102
112
|
fetchImpl,
|
|
@@ -129,8 +139,10 @@ export function brainSummary(config) {
|
|
|
129
139
|
const r = config.resilience ?? DEFAULT_RESILIENCE;
|
|
130
140
|
const hasFailover = config.gatewayFallbackUrls.length > 0;
|
|
131
141
|
const breakerActive = r.circuitBreaker && hasFailover;
|
|
142
|
+
const degradeTarget = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
|
|
143
|
+
const degradeEndpoint = degradeTarget ? openaiRouteBaseUrl(degradeTarget.baseUrl, config.gatewayBaseUrl) : undefined;
|
|
132
144
|
return {
|
|
133
|
-
gateway: buildDisplaySafeUrl(config.gatewayBaseUrl),
|
|
145
|
+
gateway: buildDisplaySafeUrl(config.gatewayBaseUrl ?? "") || null,
|
|
134
146
|
gatewayFallbacks: config.gatewayFallbackUrls.length,
|
|
135
147
|
failover: hasFailover,
|
|
136
148
|
anthropicRoute: Boolean(config.anthropic),
|
|
@@ -147,14 +159,12 @@ export function brainSummary(config) {
|
|
|
147
159
|
: "engine-default",
|
|
148
160
|
circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
|
|
149
161
|
reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
|
|
150
|
-
reactiveDegradeBaseUrl:
|
|
151
|
-
? buildDisplaySafeUrl(config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl)
|
|
152
|
-
: null,
|
|
162
|
+
reactiveDegradeBaseUrl: buildDisplaySafeUrl(degradeEndpoint ?? "") || null,
|
|
153
163
|
...((() => {
|
|
154
164
|
const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
|
|
155
165
|
if (!target)
|
|
156
166
|
return { reactiveDegradeRoutePairing: null, reactiveDegradeRouteRefusal: null };
|
|
157
|
-
if (
|
|
167
|
+
if (dispatchBrainLane(target.provider, config.anthropic !== undefined) === "anthropic") {
|
|
158
168
|
return { reactiveDegradeRoutePairing: "anthropic-lane", reactiveDegradeRouteRefusal: null };
|
|
159
169
|
}
|
|
160
170
|
const declaresOwnCredential = config.modelApiKeyEnv?.[target.name] !== undefined ||
|
|
@@ -165,12 +175,12 @@ export function brainSummary(config) {
|
|
|
165
175
|
if (declaresOwnCredential) {
|
|
166
176
|
return { reactiveDegradeRoutePairing: "per-model-declared", reactiveDegradeRouteRefusal: null };
|
|
167
177
|
}
|
|
168
|
-
const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential(
|
|
178
|
+
const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential(((key) => (key ? { configApiKey: key } : {}))(deploymentGatewayKey(config))), { baseUrl: degradeDeclaredRoot(target.baseUrl, config) });
|
|
169
179
|
return verdict.ok
|
|
170
180
|
? { reactiveDegradeRoutePairing: `ok:${verdict.posture}`, reactiveDegradeRouteRefusal: null }
|
|
171
181
|
: {
|
|
172
182
|
reactiveDegradeRoutePairing: `broken:${verdict.code}`,
|
|
173
|
-
reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config)]),
|
|
183
|
+
reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config) ?? ""]),
|
|
174
184
|
};
|
|
175
185
|
})()),
|
|
176
186
|
};
|
package/dist/config-catalog.js
CHANGED
|
@@ -245,14 +245,14 @@ export const CONFIG_CATALOG = [
|
|
|
245
245
|
r("MEMORY_SYNC_MAX_PUSH_ENTRIES", "memory", "number", "TOC 同步单轮 push 条数帽(设了必须 ≥1 整数)", { derivedDefaultNote: "缺席 ⇒ 键缺席,core 默认" }),
|
|
246
246
|
r("MEMORY_SYNC_SCOPE", "memory", "string", "TOC 同步 scope 覆写", { derivedDefaultNote: "缺席 ⇒ 从 MEMORY_SCOPE 铸 user 盘键" }),
|
|
247
247
|
r("MEMORY_SYNC_TOKEN", "memory", "secret", "TOC 同步中心面的 Bearer(URL 设了它必须在,半配拒启)", { danger: SEC }),
|
|
248
|
-
r("MEMORY_SYNC_URL", "memory", "url", "TOC 同步中心地址(设了=开;file 记忆形专用,DB
|
|
248
|
+
r("MEMORY_SYNC_URL", "memory", "url", "TOC 同步中心地址(设了=开;file 记忆形专用,DB 后端配了拒启;同步只覆盖一层 ⇒ 单用户任务不按项目键控、写家落全局层 MEMORY_SCOPE(S-512 让位,启动通知 `memory_project_scope_yields_to_sync`))"),
|
|
249
249
|
r("METRICS_TOKEN", "observability", "secret", "/metrics 只读令牌", { danger: SEC }),
|
|
250
250
|
r("MINIO_ACCESS_KEY", "store", "secret", "对象存储 access key(快照 blob/leader 工作区/公链签发共用)", { danger: SEC }),
|
|
251
251
|
r("MINIO_BUCKET", "store", "string", "leader 工作区快照桶", { derivedDefaultNote: "缺席 ⇒ workspaces" }),
|
|
252
252
|
r("MINIO_ENDPOINT", "store", "url", "对象存储 endpoint(三键齐 ⇒ 快照 blob 走 MinIO,否则 SQL 行)", { danger: DATA }),
|
|
253
253
|
r("MINIO_REGION", "store", "string", "对象存储 region"),
|
|
254
254
|
r("MINIO_SECRET_KEY", "store", "secret", "对象存储 secret key", { danger: SEC }),
|
|
255
|
-
r("MODEL_API_KEY", "modelPlane", "secret", "网关腿共享 API key(per-model 覆写在 center
|
|
255
|
+
r("MODEL_API_KEY", "modelPlane", "secret", "网关腿共享 API key(per-model 覆写在 center 侧;S-623:只与 MODEL_GATEWAY_BASEURL 配对 —— 网关未设时不装载、不发往任何路由,启动通知 `model_gateway_key_unpaired`)", { danger: [...SEC, ...BILL] }),
|
|
256
256
|
r("MODEL_AUTO_COMPACT_TOKENS", "modelPlane", "number", "主槽 dual-window 触发阈(1M 窗不再隐式 967000,缺席则用普通触发几何)", { derivedDefaultNote: "缺席 ⇒ 不铸键(core W-33000 几何)" }),
|
|
257
257
|
r("MODEL_CASCADE_LADDER", "modelPlane", "csv", "质量门级联梯(目录名 cheapest→strongest;opt-in per-task cascade:true)", { danger: BILL, configKey: "cascadeLadder" }),
|
|
258
258
|
r("MODEL_CB_COOLDOWN_MS", "modelPlane", "number", "熔断冷却窗(ms)", { staticDefault: String(RESILIENCE_DEFAULTS.cooldownMs.value) }),
|
|
@@ -286,8 +286,8 @@ export const CONFIG_CATALOG = [
|
|
|
286
286
|
r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
|
|
287
287
|
r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
|
|
288
288
|
r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
|
|
289
|
-
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(
|
|
290
|
-
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
|
|
289
|
+
r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(S-623:**无出厂缺省** —— 配置能派发到的每条 openai 兼容路由都必须解析到显式端点(条目自己的 baseUrl,或本键),否则拒启 / 热路径整批拒,机读码 `config.model_route_endpoint_missing`,拒因点名条目并给出本机写法 `MODEL_GATEWAY_BASEURL=<本机网关>/v1`;anthropic 单路由部署不设本键合法;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { derivedDefaultNote: "缺席 ⇒ 本部署没有网关(不铸键);有 openai 路由落不到端点 ⇒ 拒启", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
|
|
290
|
+
r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-623:主网关 MODEL_GATEWAY_BASEURL 未设而本键设了 ⇒ 拒启,机读码 `config.gateway_fallback_without_primary` —— 备用是主网关的镜像;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
|
|
291
291
|
r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
|
|
292
292
|
r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
|
|
293
293
|
r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
|