@sema-agent/server 7.95.1 → 7.97.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 (82) hide show
  1. package/USAGE.md +10 -5
  2. package/deploy/sema-up/README.md +8 -1
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -0
  4. package/deploy/sema-up/chart/templates/server.yaml +3 -0
  5. package/deploy/sema-up/lib/lanes.json +56 -0
  6. package/deploy/sema-up/lib/lanes.py +179 -0
  7. package/deploy/sema-up/lib/parse-machines.py +7 -1
  8. package/deploy/sema-up/machines.example.yaml +5 -2
  9. package/deploy/sema-up/preflight.sh +7 -4
  10. package/deploy/sema-up/sema-up.sh +201 -24
  11. package/deploy/sema-up/stack/compose.yaml +4 -0
  12. package/deploy/sema-up/stack/sema-stack-cluster.yaml +2 -0
  13. package/dist/approval.d.ts +28 -0
  14. package/dist/approval.js +24 -0
  15. package/dist/boot/budget-tracing.d.ts +1 -0
  16. package/dist/boot/budget-tracing.js +3 -2
  17. package/dist/boot/config-center.js +2 -5
  18. package/dist/boot/coordinators.js +2 -2
  19. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -2
  20. package/dist/boot/deferred-sandbox-path-env.js +3 -1
  21. package/dist/boot/memory-boundary.d.ts +17 -2
  22. package/dist/boot/memory-boundary.js +11 -0
  23. package/dist/boot/reapers.d.ts +3 -1
  24. package/dist/boot/reapers.js +2 -2
  25. package/dist/boot/resolve-spec.js +6 -6
  26. package/dist/boot/runner-deps.d.ts +2 -1
  27. package/dist/boot/session-faces.d.ts +4 -1
  28. package/dist/boot/session-faces.js +5 -5
  29. package/dist/boot/stage-04-stores.d.ts +1 -0
  30. package/dist/boot/stage-04-stores.js +2 -2
  31. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  32. package/dist/boot/stage-06-runners.d.ts +1 -0
  33. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  34. package/dist/boot/stage-07-capability-layer.js +10 -14
  35. package/dist/boot/stage-08-reapers.d.ts +2 -3
  36. package/dist/boot/stage-08-reapers.js +2 -2
  37. package/dist/boot/stage-09-leader.d.ts +1 -0
  38. package/dist/boot/stage-09-leader.js +2 -2
  39. package/dist/boot/stage-10-http-server.d.ts +1 -0
  40. package/dist/boot/stores.js +5 -11
  41. package/dist/capabilities/execution-lane.d.ts +5 -7
  42. package/dist/capabilities/execution-lane.js +2 -1
  43. package/dist/capabilities/memory-notice.d.ts +30 -12
  44. package/dist/capabilities/memory-notice.js +8 -1
  45. package/dist/capabilities/skills.d.ts +14 -0
  46. package/dist/capabilities/skills.js +4 -0
  47. package/dist/config-catalog.js +1 -0
  48. package/dist/config-types.d.ts +8 -1
  49. package/dist/config.d.ts +16 -0
  50. package/dist/config.js +13 -12
  51. package/dist/env-facts.js +2 -4
  52. package/dist/execution-lane-caps.d.ts +36 -6
  53. package/dist/execution-lane-caps.js +4 -0
  54. package/dist/file-history-capture.d.ts +65 -0
  55. package/dist/file-history-capture.js +21 -0
  56. package/dist/hosted-posture.d.ts +1 -1
  57. package/dist/http/resume-legs.d.ts +14 -4
  58. package/dist/http/resume-legs.js +26 -17
  59. package/dist/http/route-ctx.d.ts +35 -9
  60. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  61. package/dist/http/routes/approvals-assistant.js +16 -6
  62. package/dist/http/routes/capabilities.js +4 -1
  63. package/dist/http/routes/runs.js +2 -2
  64. package/dist/http/routes/session-sync.js +6 -1
  65. package/dist/http/routes/tasks.js +2 -2
  66. package/dist/http/server.d.ts +4 -1
  67. package/dist/http/server.js +1 -1
  68. package/dist/http/verify-rounds.d.ts +1 -1
  69. package/dist/memory-scope.d.ts +6 -2
  70. package/dist/memory-scope.js +3 -2
  71. package/dist/observability/fail-open.d.ts +4 -0
  72. package/dist/observability/fail-open.js +4 -0
  73. package/dist/observability/run-terminal.js +1 -0
  74. package/dist/run-local.js +4 -5
  75. package/dist/session-sync.d.ts +4 -1
  76. package/dist/session-sync.js +2 -2
  77. package/dist/task-cwd.d.ts +9 -3
  78. package/dist/task-cwd.js +3 -1
  79. package/dist/trace/core-keyset-guard.d.ts +1 -1
  80. package/dist/trace/project.d.ts +11 -0
  81. package/dist/trace/project.js +1 -0
  82. package/package.json +4 -3
package/USAGE.md CHANGED
@@ -24,7 +24,7 @@ npm run dev # 或 bun run src/main.ts → http://<
24
24
  ```bash
25
25
  cd sema-server
26
26
  MODEL_GATEWAY_BASEURL=https://<your-gateway>/v1 MODEL_ID=<your-model-name> \
27
- SESSION_BACKEND=memory \
27
+ SESSION_BACKEND=memory REMOTE_EXEC=host \
28
28
  docker compose up --build
29
29
  # 起来后 OA 只认 http://<host>:8090(LB)
30
30
  ```
@@ -40,10 +40,12 @@ docker compose up --build
40
40
  ```bash
41
41
  IMAGE_REGISTRY=<registry-namespace> \
42
42
  DB_BACKEND=mysql MYSQL_HOST=… MYSQL_USER=… MYSQL_PASSWORD=… MYSQL_DATABASE=sema_server \
43
- MODEL_GATEWAY_BASEURL=… MODEL_ID=<your-model-name> \
43
+ MODEL_GATEWAY_BASEURL=… MODEL_ID=<your-model-name> REMOTE_EXEC=e2b E2B_API_KEY=… \
44
44
  docker compose up --build --scale service=4 # 4 个无状态 worker,nginx LB 扇发,TiDB 协调
45
45
  ```
46
46
 
47
+ > 🔴 7.96.0 起根 `docker-compose.yml` 的 `REMOTE_EXEC`(执行车道)是**必填**:不设 ⇒ `docker compose config` 当场失败(此前静默起成一个没有手的 worker)。单人自用 `REMOTE_EXEC=host`(手落在 worker 容器里);多人 / 生产选隔离车道 `e2b`(配 `E2B_API_KEY`)或 `k8s`(配 `K8S_SANDBOX_IMAGE`)。
48
+
47
49
  > 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
48
50
 
49
51
  **可选 — 多网关 failover + Anthropic 路由(消除单网关 SPOF)**
@@ -439,7 +441,9 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
439
441
  —— 没有任何东西把 B 的路径映到 A。要么把服务(或 `run-local`)跑在放着文件的那台机器上,要么走容器车道
440
442
  (`e2b`/`k8s`/`local-docker`),那里的工作区是沙箱自己的,`cwd` 被忽略。
441
443
  - **读面(S-426 起)**:`GET /v1/capabilities` 的 `executionLane` 把本部署的车道广告成
442
- `{provider:<REMOTE_EXEC 闭集词>, toolsOnThisHost:boolean}`(`REMOTE_EXEC` 未设 ⇒ `provider:"host"`)。
444
+ `{provider:<执行车道八词>, toolsOnThisHost:boolean}`。S-602 起 `REMOTE_EXEC` 未设 ⇒
445
+ `provider:"in-process"`、`toolsOnThisHost:false`(本部署没有执行车道 = 无手;改前折报 `"host"`/`true`)。
446
+ 要本机有手,显式设 `REMOTE_EXEC=host`(sema-up 产物已显式写车道)。
443
447
  壳/客户端判「工具跑不跑在本机」**读这一位**,不要再从自己这一侧的 `REMOTE_EXEC` 猜 —— 连远端引擎时
444
448
  那个 env 说的是**壳自己这台**的事。⚠️ 它是**文件系统同一性**位,**不是**权限位:`cwd` 兑不兑现仍由
445
449
  `projectContext` / `callerCwd` 回答(那两位的判据多一条单用户约束,多租户 host 部署上两者会分叉)。
@@ -471,6 +475,7 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
471
475
  - **词表优先级**:内网端点 `MINIO_ENDPOINT` 优先,`S3_ENDPOINT` 是外接 S3 兼容位(内网/公网同址场景可只给它,同时充当公网面);公网端点 `S3_PUBLIC_ENDPOINT` 优先,缺席回落 `S3_ENDPOINT`。空串一律按未设处理。
472
476
  - **🔴 云形(`DB_BACKEND=mysql|pg`)快照 blob 强制对象存储(1.295+,clay 拍)**:缺 MinIO 三键 ⇒ **boot 拒启**——单行 SQL blob 写会撞 TiDB `txn-entry-size-limit`(默认 6MiB,真库实测矮墙)/ mysql 协议 `max_allowed_packet`,大字节归对象存储(与 D-1 附件面同裁定)。单机/测试台显式逃生:`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true`(SQL 店此时带 per-blob 帽,tidb 默认 6MiB,超限 PUT 413 `blob_too_large_for_sql`;帽可用 `SNAPSHOT_BLOB_SQL_MAX_BYTES` 按部署真实限值覆写,两方言生效;pg 默认无帽)。
473
477
  - **per-file rewind 历史的边界上限**(与 workspace 浏览面同一个整树快照纪元退役批留下的保留条款):`FILE_HISTORY_RETENTION_KEEP` —— 每个 scope(会话)存活的**最新** boundary 数,**每次 boundary 提交后自剪**(最旧者先亡,刚提交者恒存活;失引用的版本行级联删,blob 字节走异步 grace 窗 GC;v1 基线永存)。未设=core 缺省 **100**(由 core 的 `resolveFileHistoryRetention` 兑现,server 不复制那个数);`unbounded` 是**唯一**的「不剪」拼法(整店自剪关闭,GC 归部署自排 `reap`);`0`/负数/小数/非数 ⇒ **boot 拒启**(码 `config.retention_policy_invalid`,与 core 内置后端同码同判——**省略这个键不等于不剪**,它选的是缺省 100)。三条车道(TiDB / PG / local 文件店)同旋钮同判。⚠️ 升级即生效:存量超 100 boundary 的 scope 在下一次提交时被剪到 100;要保全量先写 `unbounded`。被剪 entry 的键**已花掉**(再次 snapshot 同 entry 典型拒,不复铸)。排序=DB 分配的 per-scope 发布序号 `file_history_boundary.publish_seq`(不是墙钟):**自 7.50.x 有存量 `file_history_boundary` 的部署升级本版会在 boot 期被 `assertFileHistorySchema` 拒启并指明 `DROP TABLE file_history_boundary`**(本仓无 ALTER;丢的是 per-file rewind 便利态,会话与其他持久面不动)。
478
+ - **文件历史捕获总开关**(S-589):`FILE_HISTORY_CAPTURE`(缺省 `true`;布尔词表 true/false/1/0/yes/no/on/off,未知词 ⇒ boot 拒启;TiDB / PG / local 三条车道同义)。`false` ⇒ 不把历史店交给引擎:改动文件的首次快照**一个都不捕获**;交换是同时没有 rewind(`resumeAt`+`restoreFiles` / `rewindFilesTo` 的 run 以 `rewind.store_unconfigured` 失败,文件不动)与 2c session-sync(整族 `501 capability.snapshot_store_required`)。`GET /v1/capabilities` 的 `fileHistoryCapture` 报 `"off"`,`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync` 同源翻 `false`(缺省部署报 `"on-always"` = 捕获中且无法按会话关闭)。⚠️ **关掉 ≠ 删掉**:翻旋钮不批量删除存量历史,存量也不再被读用;它照旧随 `DELETE /v1/sessions/:id` 与留存车道清理(擦除面不受本旋钮管;留存车道只在 SQL 后端,local 车道上删会话走 `reap` —— 边界删光、v1 基线按 core 契约留存,既有残余)—— 要清存量走会话删除或留存旋钮。关着时 `FILE_HISTORY_RETENTION_KEEP` 没有效果(不再有边界提交,自剪不会触发),值仍照常校验。翻旋钮后每个进程首个带手的 run 会有一条引擎的 `runner_config_advisory` warn(「file history is not live for this run」),是预期。
474
479
  - **🔴 公桶策略必须 GetObject-only**:`mc anonymous set download` 会**连带打开 ListBucket**——匿名 `GET /<bucket>/?list-type=2` 能枚举全部不可猜 key,能力链接设计即告失效。正确姿势=`mc anonymous set-json`,policy 只含 `Action:["s3:GetObject"]` on `arn:aws:s3:::<bucket>/*`(验证:对象 GET 200、桶 LIST 403)。
475
480
  - **两条轨**:ttl=0(默认)→ 匿名 GET 公桶下 `uuidv7/<name>` 不可猜 key,链接永久、可回收(删对象);0<ttl≤7 天 → 私桶 + SigV4 限时签名链接(7 天是 SigV4 物理上限,更久用 ttl=0)。
476
481
  - **执行 lane 与租户门**:e2b/k8s 沙箱 lane 任意租户可用(沙箱文件系统=租户边界,沙箱内 `curl -T` 直传、字节不中转、凭据不进沙箱);host/ssh lane 仅单用户部署(`REQUIRE_PRINCIPAL` 未开)时挂载。
@@ -793,8 +798,8 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
793
798
  - **续跑腿**:一条在托管形机器上带着 `bypassPermissions` 的 parked 任务,赎回时被同一道门挡住,
794
799
  上层折成 `409 resume_blocked_by_policy`(`blockedBy: request.host_posture_refused`)。
795
800
  运维的解法是 `TASK_WRITE_FACE_ESCALATION=allow`。
796
- - **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422)——
797
- 壳不必靠踩一次 422 才学会。
801
+ - **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422;或 —— 在
802
+ `AUTONOMY=read-only|plan` 的部署上(7.96.0 起)—— 受理但写面被钳回 roots)——壳不必靠踩一次才学会。
798
803
  - **`AUTONOMY=read-only|plan` 一行未动**。⚠️ 两者叠加时**本门先说话**:托管形 + `AUTONOMY=read-only`
799
804
  + `bypassPermissions` 的请求现在是 422,而 7.94 及以前它会跑起来(写被 autonomy 禁掉)。
800
805
  要保留旧结局就 `TASK_WRITE_FACE_ESCALATION=allow`(写面仍被 autonomy 压回 roots)。
@@ -62,7 +62,7 @@ summary 会打印手动接法。
62
62
  | `--gateway=URL` | — | 模型网关 baseURL(OpenAI 兼容);带 agent 的档必需 |
63
63
  | `--model=ID` | `deepseek-v4-flash` | 模型 id |
64
64
  | `--model-key=K` | — | 模型 API key(也可 -f 里 MODEL_API_KEY) |
65
- | `--sandbox=LANE` | kvm 有→`kata` | `kata`/`e2b-cloud`/`runc`(full-single) |
65
+ | `--sandbox=LANE` | single→`host`;multi→`e2b-cloud` | `host`/`e2b-cloud`(full-single / local-plus)。**执行车道**=agent 的手落在哪里,不点名就是没有手;词表与必需键的单一属主=`lib/lanes.json`。`kata`/`runc`/`e2b-self` 已退役(compose 路径对它们零接线),收到即非零退出并指路。kube 路径(full-cluster)**不选词**:runtime 按 runtimeClass `kata-qemu` 在不在自动定,车道恒 `REMOTE_EXEC=k8s` |
66
66
  | `--registry=BOOL` | `true`(local-plus `false`) | 配置中心网站开关 |
67
67
  | `--registry-image=REF` | — | sema-registry 镜像(registry=true 必需) |
68
68
  | `--git=MODE` | `none` | `none`/`provision`/`connect` |
@@ -105,6 +105,13 @@ data-center:2c/2G/20G)以下 BLOCK——确实无法稳定部署,提前警告不
105
105
  全部凭据生成一次、落 `~/.sema-up/<project>/.env`(0600),重跑**绝不轮换**;
106
106
  summary 里管理员密码只显示一次(新生成那次),不进日志不进 git。
107
107
 
108
+ 重跑时租户形与车道同一条规则:**决策 = 显式旗 ∨ 部署事实 ∨ 表缺省**。部署事实 = `.env`(与 shell / `-f` 环境)里
109
+ 上一次落下的 `REQUIRE_PRINCIPAL` / `REMOTE_EXEC` —— **不带旗重跑即沿用它**(多租部署、e2b 部署都不必再带旗);显式
110
+ `--users` / `--sandbox` 与事实不一致 ⇒ 非零退出并点名两处(不静默覆盖;要换就先清掉旧值)。`.env` 与 shell / `-f`
111
+ 两处都给了且不同 ⇒ 同样拒。读 `.env` 的这几个键(连同所选车道的必需键,如 `E2B_API_KEY`)只认 sema-up 自己写出的
112
+ **规范形**:一行、行首顶格 `K=<值>`,值是一个词或一对同型引号包住的一个词;`export ` 前缀、键旁空格、`K: v`、
113
+ 行内注释、重复行 ⇒ 非零退出并点名行号(不去猜 compose 会怎么读那一行)。`REQUIRE_PRINCIPAL` 只认 `true` / `false`。
114
+
108
115
  ## 验收
109
116
 
110
117
  ```bash
@@ -57,6 +57,9 @@ spec:
57
57
  - { name: MODEL_VISION, value: {{ .Values.server.modelVision | quote }} }
58
58
  - { name: SANDBOX_PKG_SOURCE, value: {{ .Values.server.sandboxPkgSource | quote }} }
59
59
  - { name: REMOTE_EXEC, value: k8s }
60
+ {{/* S-599:车道点名了就显式表态「这台有手」——REQUIRE_EXECUTION_LANE 让「车道被谁改没了」
61
+ 变成拒启而不是安静的无手 worker。server 侧默认仍 off,sema-up 产物一律写 true。 */}}
62
+ - { name: REQUIRE_EXECUTION_LANE, value: "true" }
60
63
  - { name: K8S_API_URL, value: "https://kubernetes.default.svc" }
61
64
  {{- if .Values.server.k8sInsecureTls }}
62
65
  - { name: K8S_INSECURE_TLS, value: "true" }
@@ -70,6 +70,9 @@ spec:
70
70
  - { name: REQUIRE_PRINCIPAL, value: {{ .Values.server.requirePrincipal | default "false" | quote }} }
71
71
  - { name: SANDBOX_PKG_SOURCE, value: {{ .Values.server.sandboxPkgSource | quote }} }
72
72
  - { name: REMOTE_EXEC, value: k8s }
73
+ {{/* S-599:车道点名了就显式表态「这台有手」——REQUIRE_EXECUTION_LANE 让「车道被谁改没了」
74
+ 变成拒启而不是安静的无手 worker。server 侧默认仍 off,sema-up 产物一律写 true。 */}}
75
+ - { name: REQUIRE_EXECUTION_LANE, value: "true" }
73
76
  - { name: K8S_API_URL, value: "https://kubernetes.default.svc" }
74
77
  {{- if .Values.server.k8sInsecureTls }}
75
78
  - { name: K8S_INSECURE_TLS, value: "true" }
@@ -0,0 +1,56 @@
1
+ {
2
+ "lanes": [
3
+ {
4
+ "path": "compose",
5
+ "choice": "host",
6
+ "users": ["single"],
7
+ "env": {
8
+ "REMOTE_EXEC": "host",
9
+ "REQUIRE_EXECUTION_LANE": "true"
10
+ },
11
+ "isolation": "unsupported",
12
+ "note": "server 容器本身就是沙箱:agent 的手落在这只容器里(worktree 隔离在、OS 级隔离不在)。只给单用户 —— 多租户下一个租户的输入会把手指向另一个租户的文件与进程。"
13
+ },
14
+ {
15
+ "path": "compose",
16
+ "choice": "e2b-cloud",
17
+ "users": ["single", "multi"],
18
+ "env": {
19
+ "REMOTE_EXEC": "e2b",
20
+ "REQUIRE_EXECUTION_LANE": "true",
21
+ "E2B_API_KEY": null
22
+ },
23
+ "isolation": "supported",
24
+ "note": "E2B 云沙箱:每任务一只 microVM。E2B_API_KEY 由运维自备(sema-up 只登记键名,绝不写值)。"
25
+ },
26
+ {
27
+ "path": "kube",
28
+ "choice": null,
29
+ "users": ["single", "multi"],
30
+ "env": {
31
+ "REMOTE_EXEC": "k8s",
32
+ "REQUIRE_EXECUTION_LANE": "true",
33
+ "K8S_SANDBOX_IMAGE": null
34
+ },
35
+ "isolation": "supported",
36
+ "note": "kube 路径**不选词**:runtime 由 kube-up.sh 按 runtimeClass `kata-qemu` 在不在自动定(在 ⇒ kata VM,不在 ⇒ runc 容器),沙箱镜像由 chart/清单给。本行只为对表(能力表 / 必需键 / REQUIRE_EXECUTION_LANE 三面),不进 --sandbox 词表。"
37
+ }
38
+ ],
39
+ "retired": [
40
+ {
41
+ "choice": "kata",
42
+ "why": "compose 路径对 kata 零接线:选了它 .env 里不会出现任何 REMOTE_EXEC,core 落 StubExecutionEnv ⇒ 起来的 worker 一只手都没有。有 KVM 的机器上它还曾是默认值,所以这是最贵的一个死词。",
43
+ "goto": "要 VM 级隔离走 kube 路径:sema-up.sh --profile=full-cluster(runtimeClass kata-qemu 在场即自动用 kata,不在场退 runc);留在单机 compose 上就选 host 或 e2b-cloud。"
44
+ },
45
+ {
46
+ "choice": "runc",
47
+ "why": "同 kata:compose 路径对它零接线,选了等于没选车道(无手)。",
48
+ "goto": "单机 compose 上「容器隔离」的真形就是 host(server 容器自己);要独立沙箱容器走 kube 路径 --profile=full-cluster。"
49
+ },
50
+ {
51
+ "choice": "e2b-self",
52
+ "why": "自托管 E2B 端点从未接线:它只在 preflight 文案与 machines.yaml 模板里出现过,没有任何一处把它翻成 REMOTE_EXEC。",
53
+ "goto": "先用 e2b-cloud(填 E2B_API_KEY);自托管端点的接线尚未排期。"
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """lanes.py — `lanes.json`(执行车道表)的唯一读者。
3
+
4
+ ## 为什么是一个文件而不是 sema-up.sh 里的几行 bash
5
+
6
+ 车道这件事此前被五份产物各写一遍(`sema-up.sh` 一行 `setk REMOTE_EXEC e2b`、三份 kube 清单的
7
+ 硬编码 `k8s`、根/review compose 的零车道白名单),于是「有 KVM 的机器默认选 kata ⇒ 零接线 ⇒
8
+ 起来的 worker 没有手」这件事没有任何一处会红。收成一张表之后,**表是唯一属主**:脚本里不许再
9
+ 出现任何一个车道词字面量,选项串、默认值、退役词指路、落 .env 的键值**全部**从这里渲。
10
+
11
+ ## 子命令(全部以非零退出表示「拒」,理由写 stderr)
12
+
13
+ choices 打印 compose 路径的在役词(每行一个;--help / README 的词表渲它)
14
+ default <users> 打印该租户形下的缺省 choice
15
+ options <users> 每行一个 `ask` 选项串 `value:label[:infeasible=原因|建议]`
16
+ resolve <users> <choice> 校验并打印该车道要落 .env 的 `KEY=VALUE` 行(仅有值的键);
17
+ 必需但由运维自备的键名打印成 `#REQUIRED <NAME>` 注释行(「选了却没给」的早拒读它)
18
+ choice-of <REMOTE_EXEC 值> 部署事实 → `--sandbox` 词的反查(决策 = 显式旗 ∨ 部署事实 ∨ 表缺省):上一次落下的 `REMOTE_EXEC=<值>`
19
+ 是哪条 compose 车道;表里没有这个落盘值 / 反查不唯一 ⇒ 拒
20
+
21
+ ## 拒的三种形(全部 fail-closed,没有一条静默降级)
22
+
23
+ ① 退役词(`retired`):非零 + why + 指路;
24
+ ② 不在表里的词:非零 + 列出在役词表;
25
+ ③ 该租户形不许选的词(`users` 不含它):非零 + 列出该租户形可选的词。
26
+ """
27
+ import json
28
+ import os
29
+ import sys
30
+
31
+ HERE = os.path.dirname(os.path.abspath(__file__))
32
+ TABLE_PATH = os.path.join(HERE, "lanes.json")
33
+
34
+
35
+ def die(msg):
36
+ sys.stderr.write("[sema-up] ⛔ %s\n" % msg)
37
+ raise SystemExit(1)
38
+
39
+
40
+ def table():
41
+ with open(TABLE_PATH, encoding="utf-8") as fh:
42
+ return json.load(fh)
43
+
44
+
45
+ def compose_rows(t):
46
+ return [r for r in t["lanes"] if r["path"] == "compose"]
47
+
48
+
49
+ def offered(t, users):
50
+ """该租户形下**可选**的 compose 行(顺序 = 表的声明序)。"""
51
+ return [r for r in compose_rows(t) if users in r["users"]]
52
+
53
+
54
+ def find(t, choice):
55
+ for r in compose_rows(t):
56
+ if r["choice"] == choice:
57
+ return r
58
+ return None
59
+
60
+
61
+ def retired_of(t, choice):
62
+ for r in t["retired"]:
63
+ if r["choice"] == choice:
64
+ return r
65
+ return None
66
+
67
+
68
+ def cmd_choices():
69
+ return "\n".join(r["choice"] for r in compose_rows(table()))
70
+
71
+
72
+ def cmd_default(users):
73
+ t = table()
74
+ rows = offered(t, users)
75
+ if not rows:
76
+ die("users=%s 在车道表里没有任何可选车道 —— 表坏了" % users)
77
+ return rows[0]["choice"]
78
+
79
+
80
+ def cmd_options(users):
81
+ """`ask` 的选项串。该租户形不许选的车道**仍然列出**,但标 infeasible ——
82
+ 运维要看见「这条路存在、但对你这个形不行、该怎么办」,而不是一个凭空少了一项的菜单
83
+ (`ask` 自己会在选中 infeasible 项时 fail-loud 退出,见 sema-up.sh 的 ask())。"""
84
+ t = table()
85
+ lines = []
86
+ for r in compose_rows(t):
87
+ label = r["note"].split("。")[0]
88
+ if users in r["users"]:
89
+ lines.append("%s:%s" % (r["choice"], label))
90
+ else:
91
+ lines.append(
92
+ "%s:%s:infeasible=users=%s 不许选它(隔离性 %s)|%s"
93
+ % (
94
+ r["choice"],
95
+ label,
96
+ users,
97
+ r["isolation"],
98
+ "改选 %s,或走 kube 路径 --profile=full-cluster"
99
+ % " / ".join(x["choice"] for x in offered(t, users)),
100
+ )
101
+ )
102
+ return "\n".join(lines)
103
+
104
+
105
+ def cmd_choice_of(remote_exec):
106
+ """部署事实 → `--sandbox` 词的**反查**(sema-up 的决策规则:决策 = 显式旗 ∨ 部署事实 ∨ 表缺省)。
107
+
108
+ `.env` / shell 里已有的 `REMOTE_EXEC=<值>` 是上一次落下的哪条 compose 车道 —— 表是唯一属主,脚本不抄词。
109
+ 表里没有这个落盘值 ⇒ 拒并列在役词(不猜、不把它讲成「冲突」);两行落同一个值 ⇒ 反查不唯一,表坏了
110
+ (`test/deploy-lanes-contract.test.ts` ⑥ 同时咬住「compose 行的 REMOTE_EXEC 两两不同」)。"""
111
+ t = table()
112
+ rows = [r for r in compose_rows(t) if r["env"].get("REMOTE_EXEC") == remote_exec]
113
+ if len(rows) == 1:
114
+ return rows[0]["choice"]
115
+ if len(rows) > 1:
116
+ die("车道表里有 %d 条 compose 车道都落 REMOTE_EXEC=%s —— 反查不唯一,表坏了" % (len(rows), remote_exec))
117
+ die(
118
+ "部署事实 REMOTE_EXEC=%s 不是这条部署路径上任何一条车道的落盘值 —— 在役:%s\n"
119
+ " ↳ 下一步:清掉 .env / shell 里的 REMOTE_EXEC 后重跑(不带 --sandbox 即取表缺省),或带 --sandbox 选一条在役车道;"
120
+ "k8s 车道走 kube 路径 --profile=full-cluster"
121
+ % (
122
+ remote_exec,
123
+ " / ".join("%s(REMOTE_EXEC=%s)" % (r["choice"], r["env"]["REMOTE_EXEC"]) for r in compose_rows(t)),
124
+ )
125
+ )
126
+
127
+
128
+ def cmd_resolve(users, choice):
129
+ t = table()
130
+ gone = retired_of(t, choice)
131
+ if gone is not None:
132
+ die(
133
+ "--sandbox=%s 已退役:%s\n ↳ 下一步:%s"
134
+ % (choice, gone["why"], gone["goto"])
135
+ )
136
+ row = find(t, choice)
137
+ if row is None:
138
+ die(
139
+ "--sandbox=%s 不是这条部署路径上的车道 —— 可选:%s\n"
140
+ " ↳ kube 路径(--profile=full-cluster)**不选词**:runtime 按 runtimeClass 自动定"
141
+ % (choice, " / ".join(r["choice"] for r in compose_rows(t)))
142
+ )
143
+ if users not in row["users"]:
144
+ die(
145
+ "--sandbox=%s 不能用在 --users=%s 上(该车道隔离性 %s)—— 改选:%s,或走 kube 路径 "
146
+ "--profile=full-cluster(k8s 车道,每任务一只 pod)"
147
+ % (
148
+ choice,
149
+ users,
150
+ row["isolation"],
151
+ " / ".join(r["choice"] for r in offered(t, users)),
152
+ )
153
+ )
154
+ out = []
155
+ for k, v in row["env"].items():
156
+ out.append("%s=%s" % (k, v) if v is not None else "#REQUIRED %s" % k)
157
+ return "\n".join(out)
158
+
159
+
160
+ def main(argv):
161
+ if len(argv) < 2:
162
+ die("用法: lanes.py choices|default|options|resolve|choice-of …")
163
+ verb = argv[1]
164
+ if verb == "choices" and len(argv) == 2:
165
+ print(cmd_choices())
166
+ elif verb == "default" and len(argv) == 3:
167
+ print(cmd_default(argv[2]))
168
+ elif verb == "options" and len(argv) == 3:
169
+ print(cmd_options(argv[2]))
170
+ elif verb == "choice-of" and len(argv) == 3:
171
+ print(cmd_choice_of(argv[2]))
172
+ elif verb == "resolve" and len(argv) == 4:
173
+ print(cmd_resolve(argv[2], argv[3]))
174
+ else:
175
+ die("用法: lanes.py choices | default <users> | options <users> | resolve <users> <choice> | choice-of <REMOTE_EXEC 值>")
176
+
177
+
178
+ if __name__ == "__main__":
179
+ main(sys.argv)
@@ -101,7 +101,13 @@ def main(path):
101
101
  emit("DP_S3", dp.get("s3", "deploy"))
102
102
  emit("DP_BACKUP", dp.get("backup", "true"))
103
103
  sb = sections.get("sandbox", {})
104
- emit("SANDBOX_LANE", sb.get("lane"))
104
+ # S-599:`sandbox.lane` 是**死词**,已删。machines.yaml 只走 kube 路径,而 kube-up.sh 的 runtime
105
+ # 是按 runtimeClass `kata-qemu` 在不在自动定的(kube-up.sh:94-96),从来不读 SANDBOX_LANE ——
106
+ # 发射它只会让运维以为自己选到了什么。**响亮拒**而不是静默忽略:一个设了却没有任何效果的旋钮,
107
+ # 正是本批要消灭的那类病(留着它,下一个人还会照模板填)。
108
+ if sb.get("lane") is not None:
109
+ die("machines.yaml 的 sandbox.lane 已退役(kube 路径的 runtime 按 runtimeClass kata-qemu "
110
+ "在不在自动定,不读该键)——删掉那一行即可;compose 路径的车道见 sema-up.sh --help 的 --sandbox")
105
111
  emit("E2B_ENDPOINT", sb.get("e2b_endpoint"))
106
112
  emit("E2B_API_KEY", sb.get("e2b_api_key"))
107
113
  rg = sections.get("registry", {})
@@ -34,8 +34,11 @@ data_plane:
34
34
  backup: true # PG 定时备份到 S3(data-center/full 默认 true)
35
35
 
36
36
  sandbox:
37
- lane: kata # kata(默认,需 KVM,预检不过不给)| e2b-cloud | e2b-self | runc
38
- # e2b_endpoint: … # lane=e2b-self 时必填(校验连通+模板可用)
37
+ # ⚠️ 这里**不选沙箱词**:machines.yaml 走的是 kube 路径,runtime 由 kube-up.sh 按 runtimeClass
38
+ # `kata-qemu` 在不在自动定(在 ⇒ kata VM,不在 ⇒ runc 容器),执行车道恒 REMOTE_EXEC=k8s。
39
+ # 旧模板里的 `lane:` 是死词(从来没有任何一处读它),已退役——还写着它的 yaml 会被 fail-loud 拒。
40
+ # 单机 compose 路径的车道选择见 sema-up.sh --help 的 --sandbox。
41
+ # e2b_endpoint: … # 自托管 E2B 端点(尚未接线,登记为 v2 候选)
39
42
  # e2b_api_key: …
40
43
 
41
44
  registry:
@@ -97,12 +97,15 @@ if [ -n "$BEST_MNT" ] && [ "$BEST_MNT" != "/" ] && [ "$BEST_GB" -gt "${DISK_GB:-
97
97
  add WARN disk-alt "$BEST_MNT ${BEST_GB}G avail" "存在比 / 更大的数据盘(多盘机)" "--data-root=$BEST_MNT/sema-data(sema-up 未显式指定时会自动选它)"
98
98
  fi
99
99
 
100
- # ── KVM(kata lane 前提;无=不给选 kata,建议替代,不算 BLOCK——lane 可换) ───────────────────
100
+ # ── KVM(**kube 路径**的 kata runtimeClass 前提;compose 路径已无需要 KVM 的车道,S-599)────────
101
+ # 有 KVM ⇒ 走 full-cluster 时 kata-deploy 能装上,沙箱得 VM 级隔离;没有 ⇒ kube 路径退 runc 容器
102
+ # 隔离(仍是真隔离,只是弱于 VM)。两种都不是 BLOCK:单机 compose 路径的车道(host / e2b-cloud)
103
+ # 与 KVM 无关。
101
104
  if [ -e /dev/kvm ] && grep -qE 'vmx|svm' /proc/cpuinfo 2>/dev/null; then
102
- add OK kvm "available" "kata lane 可用(默认建议)" ""
105
+ add OK kvm "available" "kube 路径可用 kata runtimeClass(VM 级沙箱隔离)" ""
103
106
  else
104
- add WARN kvm "unavailable" "无虚拟化支持,kata lane 不可部署" \
105
- "①云上开嵌套虚拟化/换 bare metal ②e2b-cloud(填 key 即用)或 e2b-self ③runc(容器隔离,弱于 VM,不建议生产)"
107
+ add WARN kvm "unavailable" "无虚拟化支持:走 kube 路径时沙箱退 runc 容器隔离(弱于 VM)" \
108
+ "①云上开嵌套虚拟化/换 bare metal ②单机 compose 路径选 --sandbox=e2b-cloud(E2B 云沙箱,每任务一只 microVM)"
106
109
  fi
107
110
 
108
111
  # ── 端口(按 --ports/profile 决定检查面) ──────────────────────────────────────────────────────