@sema-agent/server 7.98.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.
- package/MIGRATION.md +6 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +12 -9
- package/deploy/sema-up/lib/envf.sh +110 -42
- package/deploy/sema-up/sema-up.sh +76 -34
- package/dist/boot/config-center.js +167 -22
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +0 -8
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +8 -4
- package/dist/config.js +11 -13
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/observability/secret-env-scrub.d.ts +2 -26
- package/dist/observability/secret-env-scrub.js +2 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.js +0 -2
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +41 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/tool-approval.d.ts +48 -1
- package/dist/tool-approval.js +10 -3
- package/package.json +4 -3
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
8
|
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
9
|
|
|
10
|
+
## 7.99.0 —— 三条 BREAKING(行为面 / 类型面 / 运行时下限)
|
|
11
|
+
|
|
12
|
+
- **显式签发方不再算托管证据(7.99.0,S-671 / B-10;按 clay 裁定「显式签发方不算托管证据」)**:`PRINCIPAL_JWT_*` / `AUTH_BRIDGE_ISSUER` 在场而 `REQUIRE_PRINCIPAL` 未设的部署,7.95–7.98 拒启,7.99.0 起服;托管 ⇔ 运维显式声明 `REQUIRE_PRINCIPAL=true`。**谁受伤**:靠这条推断顶着没设 `REQUIRE_PRINCIPAL` 的多租机器,升级后不再被拦(召回缺口如实登记)。**迁移**:多租机器自己设 `REQUIRE_PRINCIPAL=true`(+ `OPERATOR_PRINCIPALS`),见 `docs/DEPLOY-PREREQS.md` 托管形前置。
|
|
13
|
+
- **`ServiceConfig.configCenter.token` 删除,换成 `credential`(7.99.0,S-651 波 A-1,类型面)**:凭证改为一只每次请求取值的活对象;新旋钮 `SEMA_REGISTRY_TOKEN_FILE`(与 `SEMA_REGISTRY_TOKEN` 二选一,都设 ⇒ 拒启)。手铸 `ServiceConfig` 的消费方 tsc 即红。**盘上数据**:旧 LKG 文件(`config-lkg-<worker>.json`,formatVersion 1)按凭证身份分箱后一次性迁移 —— 此刻凭证是该 worker 的拉取令牌且新箱无文件 ⇒ 迁进新箱、旧文件改名 `.migrated-v1`;其它形响亮拒一行(点名旧路径与出路);迁移代码到期删除(S-683,10-31)。旧 prompt 活动状态目录暂不迁移(S-685)。
|
|
14
|
+
- **Node 运行时下限改成实测值 `^20.19.0 || >=22.12.0`(7.99.0)**:此前 `>=20` 是假话 —— 7.98.0 在 Node 20.0–20.18 与 22.0–22.11 上本就起不来(依赖 e2b 的 CJS 入口撞 chalk@5 纯 ESM,S-682);开了 engine-strict 的安装在范围外会失败。**迁移**:Node 升到 20.19+ 或 22.12+。
|
|
15
|
+
|
|
10
16
|
## SQL 存储面 BREAKING(3.0.0 之后的三个窗)
|
|
11
17
|
|
|
12
18
|
**常设口径**:本仓**不出 `ALTER TABLE` 增量迁移**(成文裁定:预生产期零存量用户窗口,schema 变更
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ model gateways, and cloud agent execution behind an HTTP/SSE contract.
|
|
|
78
78
|
|
|
79
79
|
## Quick start
|
|
80
80
|
|
|
81
|
-
Requirements: Node
|
|
81
|
+
Requirements: Node 20.19+ or 22.12+ (npm path; `engines.node` = `^20.19.0 || >=22.12.0`) and an OpenAI-compatible model gateway.
|
|
82
82
|
|
|
83
83
|
> **A note on the `glob@11` deprecation warning at install time.** `npm install` prints a deprecation
|
|
84
84
|
> warning for `glob@11.1.0`, pulled in transitively by `e2b` (the E2B sandbox SDK). It is **install-time
|
package/README.zh-CN.md
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
## 快速开始
|
|
74
74
|
|
|
75
|
-
环境要求:Node
|
|
75
|
+
环境要求:Node 20.19+ 或 22.12+(npm 路径;`engines.node` = `^20.19.0 || >=22.12.0`)+ 一个 OpenAI 兼容模型网关。
|
|
76
76
|
|
|
77
77
|
> **关于安装时那条 `glob@11` 弃用警告。** `npm install` 会为 `glob@11.1.0` 打一条 deprecated 警告,它由
|
|
78
78
|
> `e2b`(E2B 沙箱 SDK)间接引入。这是**安装期噪声,在这里没有任何运行时曝露面**:`glob` 在 `e2b` 里的
|
package/USAGE.md
CHANGED
|
@@ -492,6 +492,9 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
|
|
|
492
492
|
# 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 在场即 boot 拒启,只认 SEMA_REGISTRY_*
|
|
493
493
|
SEMA_REGISTRY_URL=http://<config-center-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
|
|
494
494
|
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自配置控制面主机 .env;只读拉取令牌
|
|
495
|
+
# SEMA_REGISTRY_TOKEN_FILE=/path/to/token # 或:凭证文件(与上一行二选一,都设=拒启)。每条中心请求都重读 ⇒ 轮换 / 换账号改写文件即可,
|
|
496
|
+
# 不重启;文件不可读 = error 级 center_credential_unreadable,不当作无凭证。
|
|
497
|
+
# 用户 JWT 形(CLI 登录)⇒ skill 正文 / prompt 产物走 /api/v1/me/…;盘上 LKG 与 prompt 状态按「worker + 凭证身份」分箱。
|
|
495
498
|
SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
|
|
496
499
|
SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
|
|
497
500
|
FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 fleet 上报腿(announce/heartbeat+usage 批报到中心)。
|
|
@@ -782,20 +785,20 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
|
|
|
782
785
|
| `allow` | 任意 | **放行**(部署允许任务层开写面) |
|
|
783
786
|
| `refuse` | 任意 | **422 拒**(运维显式表态**无条件**恒赢,不挂任何客户端派生腿) |
|
|
784
787
|
| 未设 | 否(单用户 turnkey) | **放行** —— 与 7.94 及以前逐字节相同 |
|
|
785
|
-
| 未设 | **是** | **422 拒**,拒因点名 `TASK_WRITE_FACE_ESCALATION=allow` 与判成托管的证据 |
|
|
788
|
+
| 未设 | **是**(`REQUIRE_PRINCIPAL=true`) | **422 拒**,拒因点名 `TASK_WRITE_FACE_ESCALATION=allow` 与判成托管的证据 |
|
|
786
789
|
|
|
787
790
|
- 🔴 **`REMOTE_EXEC=device` 车道上本门不适用**(任何 `REQUIRE_PRINCIPAL` 值):那条车道的路径落在
|
|
788
791
|
**发起者自己的设备**上,归属由 `device_session` 绑定行 + 准入链的 owner 谓词验过 —— host 闸防的
|
|
789
792
|
跨租户宿主穿越在那儿**结构性不存在**(成文裁定,与 `settings.env` / `cwd` 两键在该车道上的既有臂同源),
|
|
790
793
|
也叠加「device 车道不额外加一道审批」这条既定纪律。⚠️ 运维的**显式** `refuse` 在 device 上照样恒赢(旋钮不挂车道派生腿)。
|
|
791
|
-
- **「托管形」怎么判**(判据单点 `src/hosted-posture.ts
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
`
|
|
794
|
+
- **「托管形」怎么判**(判据单点 `src/hosted-posture.ts`;7.99.0 起 / S-671):
|
|
795
|
+
**托管 ⇔ `REQUIRE_PRINCIPAL=true`**,只认运维自己的声明,零推断。
|
|
796
|
+
🔴 **签发方配置不算证据**:`PRINCIPAL_JWT_PUBKEYS` / `_ISS` / `_AUD` 与 `AUTH_BRIDGE_ISSUER` 在不在场都不改变判定
|
|
797
|
+
(7.95.0–7.98.x 曾把它们当证据,并对「配了签发方却没设 `REQUIRE_PRINCIPAL`」的机器拒启;那条拒启随之删除)。
|
|
798
|
+
auth-bridge 那条 `?? configCenter.baseUrl` 回退、`SEMA_REGISTRY_*`(含 worker 名)同样不读。
|
|
799
|
+
⚠️ 反面是**全量召回缺口**:忘设 `REQUIRE_PRINCIPAL` 的团队机器按单用户运行、本门放行 —— 多租机器必须自己设它,
|
|
800
|
+
后果与被排除的候选(`OPERATOR_PRINCIPALS`、`SERVICE_AUTH_TOKENS` 条目数、`DB_BACKEND`、`BIND_HOST`)逐条写在
|
|
801
|
+
`docs/DEPLOY-PREREQS.md`。
|
|
799
802
|
- **同族的 `approverPosture` 是「忽略 + 留声」,不是拒**:提交体的 `approverPosture` 同样是一句宿主姿态
|
|
800
803
|
声明(引擎据此把 bypass 指令渲给模型 —— 那段文字指示模型改用 Bash 改文件而不是用文件工具),
|
|
801
804
|
托管形上它被**忽略**并留一条 `task_approver_posture_ignored`(与 `settings.hooks` / `cwd` /
|
|
@@ -10,50 +10,74 @@
|
|
|
10
10
|
# has_env / `. "$ENVF"` / `printf '%s=%s' >> "$ENVF"` / `grep … "$ENVF"` —— 第二套写读规则迟早分叉
|
|
11
11
|
# (test/deploy-lanes-contract.test.ts「S-618 续」两道机器钉)。
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
13
|
+
# **一条文法,两半共用。** 文法的依据是 compose-go dotenv 的**真实**读法(车MV / S-646,k3s8 docker compose 2.40.3
|
|
14
|
+
# 逐形实跑的语料,金样钉在 test/deploy-lanes-contract.test.ts「S-656 + S-646」):一行 `KEY=<右半>` 的右半有三种形,
|
|
15
|
+
# 每种形里只有少数几个字符会让 compose **改写**读到的值 —— 不碰这几个字符,compose 读到的就是字面量:
|
|
16
|
+
# · 裸值 `KEY=v`:插值起点(`$` 后跟 `$` / `{` / 字母或 `_`,见 envf_has_interp)、「空格 + `#`」(行内注释;只认 ASCII 空格,`\t#` 是字面)、首尾空白(修剪 —— 开头按
|
|
17
|
+
# compose-go 自己的 isSpace = ASCII 空白 + U+0085 + U+00A0,结尾按 Go `unicode.IsSpace`,多出全角空格等 16 个)、
|
|
18
|
+
# 首字符是 `'` / `"`(转去引号形)。**`\` 在裸值里是字面量**(`k\x` ⇒ `k\x`,`abc\` ⇒ `abc\`,不续行);
|
|
19
|
+
# 空白、`;`、反引号、引号(不在首位)也都是字面量。
|
|
20
|
+
# · 单引号 `KEY='c'`:只有 `\'` 是转义(转义引号)。`\\` `\x` 原样。于是 c 里不能有 `'`,且 c 不能以**奇数个** `\`
|
|
21
|
+
# 结尾(最后那个 `\` 会把闭引号吃成转义引号:`unterminated quoted value`,或把后面几行吞进这个值、后续键静默落缺省)。
|
|
22
|
+
# 以偶数个 `\` 结尾照常闭合(`'abc\\'` ⇒ `abc\\`)。
|
|
23
|
+
# · 双引号 `KEY="c"`:`\` 走一张转义表(`\n` `\\` `\"` `\$` `\0NNN` …)、插值起点同裸值。c 里不碰 `"` `\`、没有插值起点
|
|
24
|
+
# 就是字面量。
|
|
25
|
+
# (刻意不认带 `\` 的双引号值:认它就得在这里抄 compose 的转义表。)
|
|
26
|
+
# 任何形之后同一行不许再有东西(闭引号后的二次赋值 = SL;行尾注释):规范行的右半整段就是这一形。
|
|
27
27
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# `x;
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
28
|
+
# · 读(`envf_canonical_line` + `envf_decode`,读口 envf_read 的判据):右半是上面三形之一**且不碰改写字符** ⇒ 认,
|
|
29
|
+
# 按字面解(裸值原样、引号形剥一对引号)—— 这正是 compose 读到的值;否则响亮拒并点名行号(不猜 compose 会改成什么)。
|
|
30
|
+
# 所以 ≤7.97.0 写口原样落盘的值(`p%ss;x!`、`x; touch …`、`k\x`、以 `\` 结尾……)只要 compose 按字面读,重跑就读得回。
|
|
31
|
+
# · 写(`envf_representable` + `envf_encode`):只写**更窄**的一形 —— 两个读者(compose 与 bash `source`)都读成同一字面量
|
|
32
|
+
# 的形。本脚本族 7.98.0 起已不 source `.env`,但运维自己的脚本、没升级的旧版 sema-up / kube-up 仍可能 source 它,写口
|
|
33
|
+
# 不替它们开执行面:安全裸词(ENVF_WORD)原样裸写;其余一律整值单引号(单引号里两个读者都字面:`$()` / 反引号 / `;`
|
|
34
|
+
# / 空白 / `\` 都不执行不展开)。**确实不可表示**的只有:换行 / 回车(一行一条赋值)、单引号本身(整值单引号里放不下;
|
|
35
|
+
# 裸写则 bash 语法错)、以奇数个 `\` 结尾(单引号形 compose 闭引号失效;裸写则 bash 当续行吞下一行)⇒ **写之前**响亮拒,
|
|
36
|
+
# 点名键、不回显值,「下一步」按拒因给。写口产物恒在读口文法内(安全裸词 ⊂ 裸值形,单引号编码形 ⊂ 单引号形)⇒ 往返成立。
|
|
34
37
|
#
|
|
35
38
|
# 收编(S-618):7.96.0「写口只拒换行 / 回车」的判据、`envf_get` 的物理行假设,连同曾经的「另立
|
|
36
39
|
# 一件做渲染对账」都收进来——写口判据 = 本文件的 `envf_representable`;物理行假设的兜底 = 生成 `.env`
|
|
37
40
|
# 之后、`compose up` 之前那一道 `docker compose config` 渲染对账(sema-up.sh `reconcile_render`,
|
|
38
41
|
# 以 compose 自己为唯一属主)。不再有第二套文法。
|
|
42
|
+
#
|
|
43
|
+
# 🔴 安全裸词是**正向白名单**(不是「排除危险字符」的负向类):`;` `&` `|` `<` `>` `(` `)` 这些 shell 命令分隔符 / 重定向
|
|
44
|
+
# 若留在写口的裸词里,任何 `source` 这份 .env 的读者会把 `x;id` 执行掉。决策键的合法值(true/false/host/e2b/k8s)、
|
|
45
|
+
# 端口(数字)、十六进制口令、URL(`https://h:port/p`)、镜像引用(`ghcr.io/x/y:1.0`)全在白名单内 ⇒ 仍裸写,.env 升级零 churn。
|
|
39
46
|
ENVF_WORD='[A-Za-z0-9_./:+=@-]'
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# (
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
# compose 修剪裸值首尾时认的空白,逐字列出(UTF-8 字节形),不用 `[[:space:]]`:那个类随区域变(C 区域只认 ASCII,UTF-8
|
|
48
|
+
# 区域可能多认全角空格)⇒ 同一份 .env 换个 LANG 判定就变。k3s8 compose 2.40.3 实测(车MV 语料):开头只修剪 compose-go 的
|
|
49
|
+
# isSpace(ASCII 空白 + U+0085 + U+00A0;开头的全角空格 / U+2000 是字面);结尾修剪 Go `unicode.IsSpace` 全集。7.98.0 的
|
|
50
|
+
# 读口没列非 ASCII 这几个 ⇒ `KEY=a<全角空格>` 读口读成 `a<全角空格>`、compose 读成 `a`(静默分叉,车MV 金样实测)。
|
|
51
|
+
ENVF_SPACE_LEAD=(' ' $'\t' $'\v' $'\f' $'\r' $'\xc2\x85' $'\xc2\xa0')
|
|
52
|
+
ENVF_SPACE_TRAIL=("${ENVF_SPACE_LEAD[@]}" $'\xe1\x9a\x80' $'\xe2\x80\x80' $'\xe2\x80\x81' $'\xe2\x80\x82' $'\xe2\x80\x83' \
|
|
53
|
+
$'\xe2\x80\x84' $'\xe2\x80\x85' $'\xe2\x80\x86' $'\xe2\x80\x87' $'\xe2\x80\x88' $'\xe2\x80\x89' $'\xe2\x80\x8a' \
|
|
54
|
+
$'\xe2\x80\xa8' $'\xe2\x80\xa9' $'\xe2\x80\xaf' $'\xe2\x81\x9f' $'\xe3\x80\x80')
|
|
55
|
+
|
|
56
|
+
# envf_odd_trailing_bs <s> —— s 以**奇数个** `\` 结尾 ⇒ 0。读口(单引号形)与写口(representable)同用这一只。
|
|
57
|
+
envf_odd_trailing_bs() {
|
|
58
|
+
local s="$1" n=0
|
|
59
|
+
while [ "${s%\\}" != "$s" ]; do s="${s%\\}"; n=$((n + 1)); done
|
|
60
|
+
[ $((n % 2)) -eq 1 ]
|
|
61
|
+
}
|
|
46
62
|
|
|
47
|
-
# envf_representable <value> ——
|
|
48
|
-
# 不能 ⇒ 非零 +
|
|
63
|
+
# envf_representable <value> —— 值能否写成写口的窄形(安全裸词或整值单引号)。
|
|
64
|
+
# 不能 ⇒ 非零 + 拒因写进 `ENVF_REJECT`、这一拒因的出路写进 `ENVF_REJECT_NEXT`(两者都**不含键、不含值**)。
|
|
49
65
|
envf_representable() {
|
|
50
66
|
case "$1" in
|
|
51
|
-
*$'\n'*) ENVF_REJECT='值里有换行'; return 1 ;;
|
|
52
|
-
*$'\r'*) ENVF_REJECT='值里有回车'; return 1 ;;
|
|
53
|
-
*\'*)
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
*$'\n'*) ENVF_REJECT='值里有换行'; ENVF_REJECT_NEXT='去掉值里的换行(.env 一行一条赋值,多行值写不进去)'; return 1 ;;
|
|
68
|
+
*$'\r'*) ENVF_REJECT='值里有回车'; ENVF_REJECT_NEXT='去掉值里的回车(.env 一行一条赋值)'; return 1 ;;
|
|
69
|
+
*\'*)
|
|
70
|
+
ENVF_REJECT='值里有单引号(整值单引号编码形里放不下单引号;裸写则任何 source 这份 .env 的 shell 会语法错)'
|
|
71
|
+
ENVF_REJECT_NEXT='换一个不含单引号的值(凭据可在签发方重新生成)'
|
|
72
|
+
return 1 ;;
|
|
56
73
|
esac
|
|
74
|
+
if envf_odd_trailing_bs "$1"; then
|
|
75
|
+
ENVF_REJECT='值以奇数个反斜杠结尾(单引号编码形里 compose 把最后的反斜杠与闭引号读成转义引号、闭引号失效;裸写则任何 source 这份 .env 的 shell 把它当续行)'
|
|
76
|
+
ENVF_REJECT_NEXT='换一个不以反斜杠结尾的值(结尾是偶数个反斜杠的值写得进;凭据可在签发方重新生成)'
|
|
77
|
+
return 1
|
|
78
|
+
fi
|
|
79
|
+
ENVF_REJECT='' ENVF_REJECT_NEXT=''
|
|
80
|
+
return 0
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
# envf_encode <value> —— 序列化出 `.env` 一行赋值的**右半**(值部分)。调用方须先过
|
|
@@ -67,15 +91,59 @@ envf_encode() {
|
|
|
67
91
|
fi
|
|
68
92
|
}
|
|
69
93
|
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
94
|
+
# envf_has_interp <s> —— s 里有 compose 插值的起点 ⇒ 0。compose-go 的 template 只认三种:`$$`(转义)、`${`(花括号;
|
|
95
|
+
# 里面不合法就整行报 Invalid template)、`$` + 变量名首字符。变量名首字符是 `[_a-z]` 在 Go `(?i)` 下的大小写折叠闭包 =
|
|
96
|
+
# ASCII 字母 + `_` + U+212A(开尔文符号,折叠到 k)+ U+017F(长 s,折叠到 s)—— 后两个是 codex 修复验证轮 [medium] 指出、
|
|
97
|
+
# k3s8 compose 2.40.3 22:14 实测确认的(`a$K` ⇒ `a`);`İ` `ı` `µ` `Å` 不在其列。其余的 `$`(后跟数字 / 标点 / 空白 /
|
|
98
|
+
# 其他非 ASCII / 串尾)是字面量(`p$9x` ⇒ `p$9x`,`p$` ⇒ `p$`,`a$é` ⇒ `a$é`;codex 一审 [medium])。
|
|
99
|
+
# 字母逐字列出、不写 `[a-z]` 区间:区间在 bash 3.2 的 UTF-8 区域按排序规则展开,会把 `é` 这类也算进去;两个折叠字符按
|
|
100
|
+
# UTF-8 字节串匹配,与区域无关。
|
|
101
|
+
ENVF_FOLD_KELVIN=$'\xe2\x84\xaa' ENVF_FOLD_LONG_S=$'\xc5\xbf'
|
|
102
|
+
envf_has_interp() {
|
|
103
|
+
case "$1" in
|
|
104
|
+
*'$$'* | *'${'* | *'$'[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_]*) return 0 ;;
|
|
105
|
+
*'$'"$ENVF_FOLD_KELVIN"* | *'$'"$ENVF_FOLD_LONG_S"*) return 0 ;;
|
|
106
|
+
esac
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# envf_rhs_literal <rhs> —— 右半是否是 compose 按字面读的三形之一(见文件头)。0 = 是(`envf_decode` 解出的就是
|
|
111
|
+
# compose 读到的值)。纯 bash 模式匹配,不起子进程;bash 3.2 可用。
|
|
112
|
+
envf_rhs_literal() {
|
|
113
|
+
local v="$1" c u
|
|
114
|
+
case "$v" in
|
|
115
|
+
'') return 0 ;;
|
|
116
|
+
\'*\')
|
|
117
|
+
c="${v:1:${#v}-2}"
|
|
118
|
+
case "$c" in *\'*) return 1 ;; esac
|
|
119
|
+
envf_odd_trailing_bs "$c" && return 1
|
|
120
|
+
return 0 ;;
|
|
121
|
+
\"*\")
|
|
122
|
+
c="${v:1:${#v}-2}"
|
|
123
|
+
case "$c" in *\"* | *\\*) return 1 ;; esac
|
|
124
|
+
envf_has_interp "$c" && return 1
|
|
125
|
+
return 0 ;;
|
|
126
|
+
\'* | \"* | *" #"*) return 1 ;;
|
|
127
|
+
esac
|
|
128
|
+
envf_has_interp "$v" && return 1
|
|
129
|
+
for u in "${ENVF_SPACE_LEAD[@]}"; do
|
|
130
|
+
case "$v" in "$u"*) return 1 ;; esac
|
|
131
|
+
done
|
|
132
|
+
for u in "${ENVF_SPACE_TRAIL[@]}"; do
|
|
133
|
+
case "$v" in *"$u") return 1 ;; esac
|
|
134
|
+
done
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# envf_canonical_line <key> <line> —— 该**整行**是否是 <key> 的规范赋值行:行首顶格 `KEY=`(无 export、键旁无空格)
|
|
139
|
+
# + 右半是 compose 按字面读的一形(envf_rhs_literal)。0 = 是。
|
|
74
140
|
envf_canonical_line() {
|
|
75
|
-
|
|
141
|
+
case "$2" in "$1="*) ;; *) return 1 ;; esac
|
|
142
|
+
envf_rhs_literal "${2#"$1="}"
|
|
76
143
|
}
|
|
77
144
|
|
|
78
|
-
# envf_decode <rhs> ——
|
|
145
|
+
# envf_decode <rhs> —— 规范行右半的字面值:引号形剥掉一对同型引号,裸值原样(空 = 未设)。只对过了 envf_rhs_literal
|
|
146
|
+
# 的右半调用 —— 那时这就是 compose 读到的值(裸值不会以引号开头,所以不会被误剥)。
|
|
79
147
|
envf_decode() {
|
|
80
148
|
local v="$1"
|
|
81
149
|
case "$v" in \"*\") v="${v#\"}"; v="${v%\"}" ;; \'*\') v="${v#\'}"; v="${v%\'}" ;; esac
|
|
@@ -112,7 +180,7 @@ envf_read() {
|
|
|
112
180
|
line="${hits#*:}"
|
|
113
181
|
if ! envf_canonical_line "$k" "$line"; then
|
|
114
182
|
envf_log "⛔ $ENVF 第 $nums 行给 $k 赋值,但不是本脚本认得的规范形"
|
|
115
|
-
envf_log " ↳ 下一步:改成 $k=<值>(行首顶格、不带 export
|
|
183
|
+
envf_log " ↳ 下一步:改成 $k=<值>(行首顶格、不带 export、键旁无空格、行尾无注释;值是 compose 按字面读的一形:裸写 = 不以空白或引号开头、不以空白结尾、不含插值(\$ 后跟 \$ / { / 字母或 _)、不含「空格+#」;整值单引号 = 里面没有单引号、不以奇数个反斜杠结尾;整值双引号 = 里面没有双引号 / 反斜杠 / 插值),或删掉这一行"
|
|
116
184
|
envf_log " ↳ 为什么不猜:这一行 compose 怎么读(注释、空白、展开、闭引号后的二次赋值)与本脚本怎么读一旦不一致,容器拿到的就不是这里读到的值"
|
|
117
185
|
return 1
|
|
118
186
|
fi
|
|
@@ -137,8 +205,8 @@ envf_load() {
|
|
|
137
205
|
# envf_guard <key> <value> —— 值无法表示 ⇒ 写之前响亮拒(点名键、不回显值),退整个脚本。
|
|
138
206
|
envf_guard() {
|
|
139
207
|
envf_representable "$2" && return 0
|
|
140
|
-
envf_log "⛔ 要写进 $ENVF 的 $1 的值无法安全写成一条 .env 赋值($ENVF_REJECT)
|
|
141
|
-
envf_log " ↳
|
|
208
|
+
envf_log "⛔ 要写进 $ENVF 的 $1 的值无法安全写成一条 .env 赋值($ENVF_REJECT)"
|
|
209
|
+
envf_log " ↳ 下一步:$ENVF_REJECT_NEXT —— 查 $1 对应的命令行参数或 -f 配置项后重跑;拒因不回显值"
|
|
142
210
|
exit 1
|
|
143
211
|
}
|
|
144
212
|
# envf_put —— `.env` 的**唯一**追加口:一次写入恰好一个物理行,值按文法序列化。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
-
# `.env` 文法(写口
|
|
18
|
+
# `.env` 文法(写口 envf_representable + envf_encode / 读口 envf_canonical_line)的唯一定义 —— 写读两半从这一处取判据。
|
|
19
19
|
. "$HERE/lib/envf.sh"
|
|
20
20
|
# 执行车道表的唯一读者(S-599)。定义在**最前面**:`--help` 的词表也从表渲(手抄的词表会在加一条
|
|
21
21
|
# 车道时静默分叉),而 `-h` 在参数循环里就被处理,那时后面的函数还没定义。
|
|
@@ -169,11 +169,20 @@ log() { printf '[sema-up] %s\n' "$*" >&2; }
|
|
|
169
169
|
|
|
170
170
|
# compose 入口解析:优先 docker compose(插件),缺了退 docker-compose(独立版)——
|
|
171
171
|
# homebrew 裸 docker CLI 不带插件是真场景;都没有=报错带下一步
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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)
|
|
175
176
|
else log "⛔ 没有可用的 compose(docker compose 插件/docker-compose 都缺)。下一步:装 Docker Desktop/OrbStack,或 brew install docker-compose"; exit 1; fi
|
|
176
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
|
+
}
|
|
177
186
|
|
|
178
187
|
# ── 跨平台探针(Linux 目标机为主;macOS 开发机跑 docker 档也是真场景——npx 分发)────────────
|
|
179
188
|
ram_mb() { if [ -r /proc/meminfo ]; then awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo
|
|
@@ -237,22 +246,23 @@ if $TEARDOWN; then
|
|
|
237
246
|
cd "$HERE/stack"
|
|
238
247
|
if [ -f "$ENVF" ]; then
|
|
239
248
|
# 读 .env 一律经 lib 的严格读口(S-618 续):裸 grep|cut 会连编码形的单引号一起读出来。
|
|
240
|
-
|
|
249
|
+
# 数据根以 .env 登记的为准(卸载的是首装那一份,不看本次 --data-root);全局参数与 up 同一张表(compose_globals)。
|
|
250
|
+
DATA_ROOT="$(envf_get DATA_ROOT_DIR)" || exit 1
|
|
241
251
|
# 全 profile down 会对未启用服务也做 config 校验——.env 缺 SEMA_*_IMAGE(该面没开过)时
|
|
242
252
|
# 整个 down 失败(真踩:假「已卸载」)。补占位符只为过校验;shell env 优先级高于 --env-file
|
|
243
253
|
envf_has SEMA_SERVER_IMAGE || export SEMA_SERVER_IMAGE="teardown-placeholder"
|
|
244
254
|
envf_has SEMA_REGISTRY_IMAGE || export SEMA_REGISTRY_IMAGE="teardown-placeholder"
|
|
245
|
-
|
|
255
|
+
compose_globals
|
|
256
|
+
if ! dc "${COMPOSE_GLOBALS[@]}" \
|
|
246
257
|
--profile data --profile pg --profile s3 --profile agent --profile registry --profile git \
|
|
247
258
|
down $($TEARDOWN_VOLUMES && echo -v); then
|
|
248
259
|
log "⛔ 卸载未完全成功。下一步:docker ps -a --filter label=com.docker.compose.project=$PROJECT 手查残留"
|
|
249
260
|
exit 1
|
|
250
261
|
fi
|
|
251
262
|
log "已卸载 project=$PROJECT $($TEARDOWN_VOLUMES && echo '(含数据卷)' || echo '(数据卷保留:pgdata/miniodata/registrydata/gitdata)')"
|
|
252
|
-
if $TEARDOWN_VOLUMES && [ -n "$
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
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"
|
|
256
266
|
fi
|
|
257
267
|
else log "没有找到 $ENVF,无可卸载"; fi
|
|
258
268
|
exit 0
|
|
@@ -566,8 +576,8 @@ PG_TUNE_FLAGS="-c max_connections=100 -c shared_buffers=${SB}MB -c effective_cac
|
|
|
566
576
|
genpw() { openssl rand -hex 24; }
|
|
567
577
|
touch "$ENVF"; chmod 600 "$ENVF"
|
|
568
578
|
# 写口 = lib/envf.sh 的 envf_put(setk / setk_new / resetk 都落到它):值按同一文法序列化 —— 安全裸词原样,
|
|
569
|
-
# 含空白 /
|
|
570
|
-
# 回车 / 单引号 ⇒
|
|
579
|
+
# 含空白 / 特殊字符 / 反斜杠的走整值单引号编码形(compose 读成字面量、闭引号后不续解析;被 source 也不执行);换行 /
|
|
580
|
+
# 回车 / 单引号 / 以奇数个反斜杠结尾 ⇒ 写之前响亮拒,点名键、不回显值,「下一步」按拒因给。setk 判「键在不在」也走严格读口:旧文件里该键那一行不是
|
|
571
581
|
# 规范形(比如同物理行闭引号带出的二次赋值)⇒ 响亮拒,不再「见行在就跳过」把毒行原样留着(test srv796 RT1)。
|
|
572
582
|
setk SEMA_PROJECT "$PROJECT"
|
|
573
583
|
setk PG_PASSWORD "$(genpw)"
|
|
@@ -683,7 +693,9 @@ fi
|
|
|
683
693
|
envf_load SERVER_PORT PG_PORT MINIO_PORT MINIO_CONSOLE_PORT REGISTRY_PORT GIT_HTTP_PORT GIT_SSH_PORT
|
|
684
694
|
|
|
685
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; }
|
|
686
|
-
compose() { ( cd "$HERE/stack" && dc
|
|
696
|
+
compose() { compose_globals; ( cd "$HERE/stack" && dc "${COMPOSE_GLOBALS[@]}" "$@" ); }
|
|
697
|
+
# shq —— 单引号引用一个词(复现命令要能原样粘贴:路径里可能有空格)。
|
|
698
|
+
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
|
687
699
|
|
|
688
700
|
# ── D:渲染对账(S-618 的类闭环;生成 .env 之后、compose up 之前)──────────────────────────────────
|
|
689
701
|
# .env 的写口 / 读口都按物理行判,认不出 compose dotenv 的跨行引号值等边角;唯一能确知「容器真正拿到
|
|
@@ -692,22 +704,23 @@ compose() { ( cd "$HERE/stack" && dc --project-name "$PROJECT" --env-file "$ENVF
|
|
|
692
704
|
# REQUIRE_EXECUTION_LANE,与本次决策逐值对比,不一致 ⇒ 拒并点名渲染值与决策值。**以 compose 为唯一属主**。
|
|
693
705
|
# docker 不可达在上面 `command -v docker` 那道已 fail-loud 拦下;config 本身失败(YAML/插值坏)也拒,
|
|
694
706
|
# 不新铸降级臂:未经对账就不 up。只有 agent 面有这三个决策键,别的档跳过。
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
exit 1
|
|
705
|
-
}
|
|
706
|
-
RECON_JSON="$rendered" python3 - "$exp_rp" "$exp_re" "$exp_rl" <<'PY' || {
|
|
707
|
-
import json, os, sys
|
|
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
|
|
708
716
|
want = {"REQUIRE_PRINCIPAL": sys.argv[1], "REMOTE_EXEC": sys.argv[2], "REQUIRE_EXECUTION_LANE": sys.argv[3]}
|
|
709
|
-
|
|
710
|
-
|
|
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 服务
|
|
711
724
|
bad = []
|
|
712
725
|
for k, w in want.items():
|
|
713
726
|
got = env.get(k)
|
|
@@ -716,13 +729,42 @@ for k, w in want.items():
|
|
|
716
729
|
bad.append("%s:渲染=%r 决策=%r" % (k, got, w))
|
|
717
730
|
if bad:
|
|
718
731
|
sys.stderr.write("[sema-up] ↳ 明细:" + " | ".join(bad) + "\n")
|
|
719
|
-
raise SystemExit(3)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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 行"
|
|
723
749
|
exit 1
|
|
724
|
-
|
|
725
|
-
|
|
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")"
|
|
726
768
|
}
|
|
727
769
|
|
|
728
770
|
# ── 外接端点连接检测(装配前 fail-loud;只验兼容可用,不校验证书——内网自签是常态)────────────
|