@sema-agent/server 7.95.1 → 7.96.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/USAGE.md +6 -4
- package/deploy/sema-up/README.md +8 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -0
- package/deploy/sema-up/chart/templates/server.yaml +3 -0
- package/deploy/sema-up/lib/lanes.json +56 -0
- package/deploy/sema-up/lib/lanes.py +179 -0
- package/deploy/sema-up/lib/parse-machines.py +7 -1
- package/deploy/sema-up/machines.example.yaml +5 -2
- package/deploy/sema-up/preflight.sh +7 -4
- package/deploy/sema-up/sema-up.sh +201 -24
- package/deploy/sema-up/stack/compose.yaml +4 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +2 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +9 -9
- package/dist/http/routes/capabilities.js +2 -1
- package/dist/http/routes/tasks.js +2 -2
- package/dist/observability/run-terminal.js +1 -0
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.d.ts +11 -0
- package/dist/trace/project.js +1 -0
- 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)**
|
|
@@ -793,8 +795,8 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
|
|
|
793
795
|
- **续跑腿**:一条在托管形机器上带着 `bypassPermissions` 的 parked 任务,赎回时被同一道门挡住,
|
|
794
796
|
上层折成 `409 resume_blocked_by_policy`(`blockedBy: request.host_posture_refused`)。
|
|
795
797
|
运维的解法是 `TASK_WRITE_FACE_ESCALATION=allow`。
|
|
796
|
-
- **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422
|
|
797
|
-
|
|
798
|
+
- **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422;或 —— 在
|
|
799
|
+
`AUTONOMY=read-only|plan` 的部署上(7.96.0 起)—— 受理但写面被钳回 roots)——壳不必靠踩一次才学会。
|
|
798
800
|
- **`AUTONOMY=read-only|plan` 一行未动**。⚠️ 两者叠加时**本门先说话**:托管形 + `AUTONOMY=read-only`
|
|
799
801
|
+ `bypassPermissions` 的请求现在是 422,而 7.94 及以前它会跑起来(写被 autonomy 禁掉)。
|
|
800
802
|
要保留旧结局就 `TASK_WRITE_FACE_ESCALATION=allow`(写面仍被 autonomy 压回 roots)。
|
package/deploy/sema-up/README.md
CHANGED
|
@@ -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` |
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
#
|
|
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
|
|
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
|
|
105
|
+
add OK kvm "available" "kube 路径可用 kata runtimeClass(VM 级沙箱隔离)" ""
|
|
103
106
|
else
|
|
104
|
-
add WARN kvm "unavailable" "
|
|
105
|
-
"①云上开嵌套虚拟化/换 bare metal
|
|
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 决定检查面) ──────────────────────────────────────────────────────
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
# 执行车道表的唯一读者(S-599)。定义在**最前面**:`--help` 的词表也从表渲(手抄的词表会在加一条
|
|
19
|
+
# 车道时静默分叉),而 `-h` 在参数循环里就被处理,那时后面的函数还没定义。
|
|
20
|
+
lanes() { python3 "$HERE/lib/lanes.py" "$@"; }
|
|
18
21
|
MODE_INTERACTIVE="tty" # tty | json | none(--yes)
|
|
19
22
|
PROFILE="" USERS="" REGION="auto" CONF="" PROJECT="sema" TEARDOWN=false SKIP_PREFLIGHT=false
|
|
20
23
|
DATA_ROOT="" SERVER_PORT=8090 PG_PORT=5432 MINIO_PORT=9000 MINIO_CONSOLE_PORT=9001
|
|
@@ -37,7 +40,11 @@ usage() {
|
|
|
37
40
|
--dry-run 只打印将执行计划(不写 .env、不起任何容器)
|
|
38
41
|
--port=N 服务端口(默认 8090);--pg-port/--minio-port/--minio-console-port 同理
|
|
39
42
|
--gateway=URL --model=ID --model-key=K 模型面(带 agent 的档必需)
|
|
40
|
-
|
|
43
|
+
EOF
|
|
44
|
+
# 词表从 lib/lanes.json 渲(单一属主):加/删一条车道,这行自己跟着变
|
|
45
|
+
printf ' --sandbox=LANE %s(full-single / local-plus;kube 路径不选词,runtimeClass 自动)\n' \
|
|
46
|
+
"$(lanes choices | tr '\n' '|' | sed 's/|$//')"
|
|
47
|
+
cat <<'EOF'
|
|
41
48
|
--registry=BOOL 配置中心网站(默认 true;local-plus 默认 false)
|
|
42
49
|
--registry-image=REF sema-registry 镜像(registry=true 必需,可 -f 配置 SEMA_REGISTRY_IMAGE)
|
|
43
50
|
--git=MODE none(默认)|provision(一键自建 Gitea)|connect(接已有 git)
|
|
@@ -113,6 +120,79 @@ if [ -n "$CONF" ]; then
|
|
|
113
120
|
fi
|
|
114
121
|
|
|
115
122
|
WORK="$HOME/.sema-up/$PROJECT"; mkdir -p "$WORK"; chmod 700 "$HOME/.sema-up" "$WORK"
|
|
123
|
+
# 凭据/配置文件的坐标 —— 与 `has_env` 一起提到这里(S-599):车道决策要在模型面之前读既有 .env
|
|
124
|
+
# (冲突检测 + 「必需键已给过」判定),而生成段在几百行之后。纯搬家,零语义变。
|
|
125
|
+
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
|
+
# ── 部署事实(租户形与车道两个决策共用)────────────────────────────────────────────────
|
|
169
|
+
# 事实 = `.env`(严格读口)∪ shell / `-f` 环境。compose 插值里 shell **优先于** --env-file(本脚本 teardown 段同一
|
|
170
|
+
# 事实),所以两处都非空而不同 ⇒ 拒并点名两处 —— 不「取第一个非空」:放行的唯一形是两处至多一个值,无论谁赢,
|
|
171
|
+
# 赢的都是它。设 FACT_VAL(可空 = 没有事实)与 FACT_SRC(来源描述)。直接调用,不进命令替换(拒要退整个脚本)。
|
|
172
|
+
deploy_fact() {
|
|
173
|
+
local k="$1" fenv fsh shsrc
|
|
174
|
+
fenv="$(envf_get "$k")" || exit 1
|
|
175
|
+
eval "fsh=\${$k:-}"
|
|
176
|
+
shsrc="${CONF:+-f $CONF / }shell 环境"
|
|
177
|
+
if [ -n "$fenv" ] && [ -n "$fsh" ] && [ "$fenv" != "$fsh" ]; then
|
|
178
|
+
log "⛔ 部署事实冲突:$ENVF 里 $k=$fenv,$shsrc 里 $k=$fsh —— 两处不一致"
|
|
179
|
+
log " ↳ 下一步:两处只留一处(或改成同一个值)"
|
|
180
|
+
log " ↳ 为什么两边都要查:compose 插值里 shell 环境**优先于** --env-file,只对齐 .env 不足以决定容器真正拿到哪个值"
|
|
181
|
+
exit 1
|
|
182
|
+
fi
|
|
183
|
+
FACT_VAL="${fenv:-$fsh}"
|
|
184
|
+
if [ -n "$fenv" ]; then FACT_SRC="$ENVF"; elif [ -n "$fsh" ]; then FACT_SRC="$shsrc"; else FACT_SRC=""; fi
|
|
185
|
+
}
|
|
186
|
+
# 本次决策落盘的值与部署事实不一致 ⇒ 拒并点名两处(不静默覆盖:替运维挑一个是替他做决定)。
|
|
187
|
+
# 只有**显式旗**(或交互里改掉了缺省)走得到这里:不带旗时决策就取自事实,两者恒等。
|
|
188
|
+
# fact_conflict <键> <事实值> <事实来源> <本次要写的值> <本次的旗>
|
|
189
|
+
fact_conflict() {
|
|
190
|
+
[ -n "$2" ] || return 0
|
|
191
|
+
[ "$2" = "$4" ] && return 0
|
|
192
|
+
log "⛔ 部署事实冲突:$3 里已有 $1=$2,本次 $5 要写 $1=$4"
|
|
193
|
+
log " ↳ 下一步(二选一):① 去掉 ${5%%=*} 重跑(不带旗 = 沿用这台部署的事实);② 真要改:先清掉 $3 里的 $1,再带 ${5%%=*} 重跑"
|
|
194
|
+
exit 1
|
|
195
|
+
}
|
|
116
196
|
# 协议流(--interactive=json 的 question/summary 行)固定走 fd3=原始 stdout——
|
|
117
197
|
# $(ask …) 的命令替换只捕获答案,协议行不被吞(真机验出的坑)
|
|
118
198
|
exec 3>&1
|
|
@@ -208,7 +288,8 @@ if $TEARDOWN; then
|
|
|
208
288
|
exit 0
|
|
209
289
|
fi
|
|
210
290
|
|
|
211
|
-
|
|
291
|
+
# (KVM 探测已随 `kata` 词退役删除 —— compose 路径不再有需要 KVM 的车道;kube 路径的 kata/runc
|
|
292
|
+
# 由 kube-up.sh 按 runtimeClass 自动定,preflight 的 kvm 项仍如实报给要走 kube 的人看。)
|
|
212
293
|
|
|
213
294
|
# ── 决策点(三轴+registry+git,§3b;问句都带「为什么问」)─────────────────────────────────────
|
|
214
295
|
[ -z "$PROFILE" ] && PROFILE=$(ask profile "要装成哪种形态?" "full-single" "决定这台机器上起哪些服务" \
|
|
@@ -216,9 +297,39 @@ KVM_OK=$([ -e /dev/kvm ] && echo true || echo false)
|
|
|
216
297
|
"data-center:只装数据面(PG+MinIO+备份)——agent 在你自己机器上,数据放这里" \
|
|
217
298
|
"full-cluster:k8s 准生产——3 机 ssh 装配 / 已有集群 kubeconfig / 本机装 k3s" \
|
|
218
299
|
"local-plus:本机只跑 agent,连一个已经部署好的 data-center 数据面")
|
|
219
|
-
|
|
300
|
+
# ── 两个决策(租户形 / 车道)同一条规则:决策 = 显式旗 ∨ 部署事实 ∨ 表缺省 ─────────────────────
|
|
301
|
+
# 「部署事实」只对 compose 的 agent 档成立(`.env` 就是它们的部署状态;kube 路径的状态在 helm values 里,
|
|
302
|
+
# data-center 不起 agent)。事实 → 决策词只经属主翻译,本脚本不抄第二份:
|
|
303
|
+
# · REQUIRE_PRINCIPAL → --users:只认 sema-up 自己写出的两形 true / false(别的写法 ⇒ 拒;不复刻 server 的布尔词表);
|
|
304
|
+
# · REMOTE_EXEC → --sandbox:`lanes choice-of`(车道表是唯一属主;表里没有的落盘值 ⇒ 拒并列在役词),见车道块。
|
|
305
|
+
# 不带旗重跑 ⇒ 沿用事实(多租部署重跑不必再带 --users=multi);显式旗与事实不一致 ⇒ fact_conflict 拒并点名两处。
|
|
306
|
+
# 🔴 为什么缺省不能先于事实(合并复审两条真案):把表缺省当成意图去和事实比,合法的无旗重跑就被拒;
|
|
307
|
+
# 反过来若只比不拒,保留的多租 `.env` 在缺省 single 下会被车道表判成 host —— 容器拿到 REQUIRE_PRINCIPAL=true ∧ host。
|
|
308
|
+
# 租户形 ⇒ 容器上 REQUIRE_PRINCIPAL 的值 —— 本脚本内**唯一**一处这条翻译。正向(生成段 setk、事实核对)与反向
|
|
309
|
+
# (事实 → --users 缺省)都经它:反向是拿两个租户词逐个过这只函数去比,不另写一张反表(与车道的 `lanes choice-of`
|
|
310
|
+
# 同一姿势)。⚠️ kube 路径另有一处同义翻译(`kube-up.sh` 的 V_REQ_PRINCIPAL,v7.95.1 既有,登记 DEBTS)。
|
|
311
|
+
principal_of_users() { [ "$1" = "multi" ] && echo true || echo false; }
|
|
312
|
+
USERS_DEF="single" RP_FACT="" RP_FACT_SRC=""
|
|
313
|
+
case "$PROFILE" in
|
|
314
|
+
full-single|local-plus)
|
|
315
|
+
deploy_fact REQUIRE_PRINCIPAL; RP_FACT="$FACT_VAL" RP_FACT_SRC="$FACT_SRC"
|
|
316
|
+
if [ -n "$RP_FACT" ]; then
|
|
317
|
+
USERS_DEF=""
|
|
318
|
+
for _u in single multi; do [ "$(principal_of_users "$_u")" = "$RP_FACT" ] && USERS_DEF="$_u"; done
|
|
319
|
+
if [ -z "$USERS_DEF" ]; then
|
|
320
|
+
log "⛔ 部署事实 REQUIRE_PRINCIPAL=$RP_FACT(来自 $RP_FACT_SRC):本脚本只读自己写出的两形 $(principal_of_users multi) / $(principal_of_users single)"
|
|
321
|
+
log " ↳ 下一步:改成 REQUIRE_PRINCIPAL=$(principal_of_users multi)(多人,身份隔离)或 REQUIRE_PRINCIPAL=$(principal_of_users single)(单人),再重跑"
|
|
322
|
+
log " ↳ 为什么不认别的写法:它们在 server 那边也许同样是布尔,但在这里认它们就得抄一份 server 的布尔词表,两份词表迟早分叉"
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
fi
|
|
326
|
+
;;
|
|
327
|
+
esac
|
|
328
|
+
[ -z "$USERS" ] && USERS=$(ask users "几个人用?" "$USERS_DEF" "单人可跳过全部权限配置,多人要开身份隔离" \
|
|
220
329
|
"single:就我一个(超级管理员 turnkey,零权限配置)" \
|
|
221
330
|
"multi:一个团队(身份隔离 ON,能力默认全开,之后可用 policy 收紧)")
|
|
331
|
+
REQUIRE_PRINCIPAL_WANT="$(principal_of_users "$USERS")"
|
|
332
|
+
fact_conflict REQUIRE_PRINCIPAL "$RP_FACT" "$RP_FACT_SRC" "$REQUIRE_PRINCIPAL_WANT" "--users=$USERS"
|
|
222
333
|
|
|
223
334
|
# registry:各档默认带上(clay 2026-07-12 拍;显式 --registry=false 可关)。
|
|
224
335
|
# local-plus 例外默认 false——它的数据面在远端,配置中心应随数据面部署,本机再起一个=两个配置中心。
|
|
@@ -237,16 +348,63 @@ if [ "$GIT_MODE" = "connect" ]; then
|
|
|
237
348
|
[ -z "$GIT_TOKEN" ] && { [ "$MODE_INTERACTIVE" = "none" ] && { log "⛔ --git=connect 需要 --git-token(该服务的 access token)"; exit 1; } || GIT_TOKEN=$(ask git_token "该服务的 access token?" "" "验证连通用;Gitea 还会用它自动建 OAuth 登录应用"); }
|
|
238
349
|
fi
|
|
239
350
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
351
|
+
# ── 执行车道(S-599;单一属主 = lib/lanes.json,本脚本里零车道词字面量)──────────────────────
|
|
352
|
+
# 病:compose 路径的 `kata` / `runc` 对**执行运行时零接线**(有 KVM 时 kata 还是默认值)⇒ .env 里
|
|
353
|
+
# 压根不出现 `REMOTE_EXEC` ⇒ core 落 StubExecutionEnv ⇒ 起来的 worker 一只手都没有,而三条部署
|
|
354
|
+
# 路径没有任何一处会红。现在:词表 / 默认 / 退役指路 / 落 .env 的键值**全部**从表渲,拒一律非零退出。
|
|
355
|
+
# 位置刻意在模型面(gateway/model)**之前**:选错车道时失败原因要来自车道门,不是「你还没填网关」。
|
|
356
|
+
LANE_ENV="" LANE_REQUIRED="" LANE_REMOTE_EXEC=""
|
|
357
|
+
case "$PROFILE" in
|
|
358
|
+
full-single|local-plus)
|
|
359
|
+
# 同一条规则的车道半场(见租户形块):决策 = --sandbox ∨ 部署事实(REMOTE_EXEC 反查成车道词)∨ 表缺省。
|
|
360
|
+
# 租户形已在上面定死(事实或显式旗),所以这里 `lanes default "$USERS"` 读到的是容器真正会拿到的租户形 ——
|
|
361
|
+
# 表里「multi 不许 host」那条规则的输入是真的。
|
|
362
|
+
deploy_fact REMOTE_EXEC; RE_FACT="$FACT_VAL" RE_FACT_SRC="$FACT_SRC" RE_FACT_CHOICE=""
|
|
363
|
+
if [ -n "$RE_FACT" ]; then RE_FACT_CHOICE="$(lanes choice-of "$RE_FACT")" || exit 1; fi
|
|
364
|
+
if [ -z "$SANDBOX_LANE" ]; then
|
|
365
|
+
if [ -n "$RE_FACT_CHOICE" ]; then LANE_DEF="$RE_FACT_CHOICE"; else LANE_DEF="$(lanes default "$USERS")" || exit 1; fi
|
|
366
|
+
LANE_OPTS=()
|
|
367
|
+
while IFS= read -r _o; do [ -n "$_o" ] && LANE_OPTS+=("$_o"); done < <(lanes options "$USERS")
|
|
368
|
+
SANDBOX_LANE=$(ask sandbox "agent 跑代码用哪种沙箱?" "$LANE_DEF" \
|
|
369
|
+
"决定 agent 的手落在哪里——不点名车道就是**没有手**(工具面整排关)" \
|
|
370
|
+
${LANE_OPTS[@]+"${LANE_OPTS[@]}"})
|
|
371
|
+
fi
|
|
372
|
+
LANE_ENV="$(lanes resolve "$USERS" "$SANDBOX_LANE")" || exit 1
|
|
373
|
+
LANE_REQUIRED="$(printf '%s\n' "$LANE_ENV" | sed -n 's/^#REQUIRED //p')"
|
|
374
|
+
LANE_ENV="$(printf '%s\n' "$LANE_ENV" | { grep -v '^#REQUIRED ' || true; })"
|
|
375
|
+
LANE_REMOTE_EXEC="$(printf '%s\n' "$LANE_ENV" | sed -n 's/^REMOTE_EXEC=//p')"
|
|
376
|
+
fact_conflict REMOTE_EXEC "$RE_FACT" "$RE_FACT_SRC" "$LANE_REMOTE_EXEC" "--sandbox=$SANDBOX_LANE"
|
|
377
|
+
# 选了车道却没给它的必需键 ⇒ 当场拒。否则 .env 照写、容器起来才在 server 的执行车道拒启门上被拒,
|
|
378
|
+
# 运维要翻容器日志才看得到「缺 X」,而那个键在 sema-up 的对话里从没被问过。
|
|
379
|
+
for _k in $LANE_REQUIRED; do
|
|
380
|
+
eval "_v=\${$_k:-}"
|
|
381
|
+
_ev="$(envf_get "$_k")" || exit 1
|
|
382
|
+
if [ -z "$_v" ] && [ -z "$_ev" ]; then
|
|
383
|
+
log "⛔ --sandbox=$SANDBOX_LANE(车道 $LANE_REMOTE_EXEC)需要 $_k,但 -f 配置 / 环境 / $ENVF 里都没有"
|
|
384
|
+
log " ↳ 下一步:-f 配置里加 $_k=…(或 export $_k=…)后重跑;换车道见 --help 的 --sandbox 词表"
|
|
385
|
+
exit 1
|
|
386
|
+
fi
|
|
387
|
+
done
|
|
388
|
+
;;
|
|
389
|
+
full-cluster)
|
|
390
|
+
# kube 路径**不选词**:runtime 由 kube-up.sh 按 runtimeClass `kata-qemu` 在不在自动定。收到
|
|
391
|
+
# `--sandbox`(或 -f 里的 SANDBOX_LANE)⇒ 响亮拒,不静默忽略 —— 一个「设了却没有任何效果」的
|
|
392
|
+
# 旋钮正是本批要消灭的那类病。
|
|
393
|
+
if [ -n "$SANDBOX_LANE" ]; then
|
|
394
|
+
log "⛔ --profile=full-cluster(kube 路径)不选沙箱词,收到 --sandbox=$SANDBOX_LANE"
|
|
395
|
+
log " ↳ 下一步:删掉 --sandbox / -f 里的 SANDBOX_LANE。kube 路径的 runtime 按 runtimeClass kata-qemu 在不在自动定(在 ⇒ kata VM,不在 ⇒ runc),车道恒 REMOTE_EXEC=k8s"
|
|
396
|
+
exit 1
|
|
397
|
+
fi
|
|
398
|
+
;;
|
|
399
|
+
*)
|
|
400
|
+
# data-center:不启 agent ⇒ 没有执行车道可言;收到 --sandbox 同样响亮拒(同上一条理由)。
|
|
401
|
+
if [ -n "$SANDBOX_LANE" ]; then
|
|
402
|
+
log "⛔ --profile=$PROFILE 不启 agent,没有执行车道;收到 --sandbox=$SANDBOX_LANE"
|
|
403
|
+
log " ↳ 下一步:删掉 --sandbox;要跑 agent 用 --profile=full-single 或 local-plus"
|
|
404
|
+
exit 1
|
|
405
|
+
fi
|
|
406
|
+
;;
|
|
407
|
+
esac
|
|
250
408
|
if [ "$PROFILE" = "full-single" ] || [ "$PROFILE" = "local-plus" ]; then
|
|
251
409
|
[ -z "$GATEWAY" ] && { [ "$MODE_INTERACTIVE" = "none" ] && { log "⛔ $PROFILE 档要跑 agent,需要 --gateway/--model(模型面)。例:--gateway=https://api.deepseek.com/v1 --model=deepseek-v4-flash --model-key=sk-…"; exit 1; } || GATEWAY=$(ask gateway "模型网关 baseURL?(OpenAI 兼容,如 https://api.deepseek.com/v1)" "" "引擎的大脑入口,没有它 agent 无法思考"); }
|
|
252
410
|
[ -z "$MODEL" ] && MODEL=$(ask model "模型 id?" "deepseek-v4-flash" "按成本推荐 flash;要更强推理换 pro")
|
|
@@ -349,7 +507,7 @@ if [ "$REGION" = "auto" ]; then
|
|
|
349
507
|
REGION=$(awk -v a="$t_cn" -v b="$t_gl" 'BEGIN{print (a<b)?"cn":"global"}')
|
|
350
508
|
fi
|
|
351
509
|
HOSTIP=$(host_ip)
|
|
352
|
-
log "region=$REGION profile=$PROFILE users=$USERS sandbox=${SANDBOX_LANE:-n/a} registry=$REGISTRY_ENABLED git=$GIT_MODE"
|
|
510
|
+
log "region=$REGION profile=$PROFILE users=$USERS sandbox=${SANDBOX_LANE:-n/a} lane=${LANE_REMOTE_EXEC:-n/a} registry=$REGISTRY_ENABLED git=$GIT_MODE"
|
|
353
511
|
|
|
354
512
|
# ── 数据盘选择(多盘机自动选大容量盘;--data-root 显式优先;macOS 不自动——bind 要经 VM 挂载,
|
|
355
513
|
# 显式给了才用)。首装落 .env 后不漂移:换盘=先 --teardown-volumes 迁数据再重装。──────────────
|
|
@@ -380,8 +538,6 @@ case "$PROFILE" in full-single|local-plus) COMPOSE_PROFILES="$COMPOSE_PROFILES,a
|
|
|
380
538
|
[ "$REGISTRY_ENABLED" = "true" ] && COMPOSE_PROFILES="$COMPOSE_PROFILES,registry"
|
|
381
539
|
[ "$GIT_MODE" = "provision" ] && COMPOSE_PROFILES="$COMPOSE_PROFILES,git"
|
|
382
540
|
COMPOSE_PROFILES="${COMPOSE_PROFILES#,}"; COMPOSE_PROFILES="${COMPOSE_PROFILES%,}"
|
|
383
|
-
ENVF="$WORK/.env"
|
|
384
|
-
has_env() { [ -f "$ENVF" ] && grep -q "^$1=..*" "$ENVF"; }
|
|
385
541
|
case ",$COMPOSE_PROFILES," in *,agent,*)
|
|
386
542
|
if [ -z "${SEMA_SERVER_IMAGE:-}" ] && ! has_env SEMA_SERVER_IMAGE; then
|
|
387
543
|
log "⛔ $PROFILE 档需要 sema-server 镜像。下一步:-f 配置里加 SEMA_SERVER_IMAGE=<registry>/sema-server:<tag>(默认=ghcr.io/sema-agent/sema-server:<tag> 公开仓匿名可拉;CN 兜底=内网 Gitea 仓;SWR 已弃用);镜像分发=D4"
|
|
@@ -436,11 +592,25 @@ PG_TUNE_FLAGS="-c max_connections=100 -c shared_buffers=${SB}MB -c effective_cac
|
|
|
436
592
|
# ── .env 生成(幂等:已存在则只补缺,凭据绝不轮换;0600) ───────────────────────────────────
|
|
437
593
|
genpw() { openssl rand -hex 24; }
|
|
438
594
|
touch "$ENVF"; chmod 600 "$ENVF"
|
|
439
|
-
|
|
595
|
+
# ── .env 的**唯一**写口:一次写入恰好一个物理行 ────────────────────────────────────────────────
|
|
596
|
+
# 病(7.96.0 修复验证轮 [high]):`--model-key=$'k\nREQUIRE_PRINCIPAL=true'` 经旧 setk 原样写成两行 —— 第二行是
|
|
597
|
+
# 决策检查**之后**才出现在 .env 里的一条 REQUIRE_PRINCIPAL,紧接着的 `setk REQUIRE_PRINCIPAL` 见键在就不写 ⇒
|
|
598
|
+
# 容器拿到多租 + host。严格读口只管「读」;写口与它是同一条约定的两半:.env 一行一条赋值。值里带换行 / 回车
|
|
599
|
+
# ⇒ **写之前**拒(先判后动:resetk 的删旧行也排在判定之后),拒因不回显值(可能是凭据)。
|
|
600
|
+
envf_check() {
|
|
601
|
+
case "$2" in
|
|
602
|
+
*$'\n'*|*$'\r'*)
|
|
603
|
+
log "⛔ 要写进 $ENVF 的 $1 的值里有换行 / 回车 —— .env 一行一条赋值,换行后的内容会被 compose 当成另一条赋值"
|
|
604
|
+
log " ↳ 下一步:去掉值里的换行(查 $1 对应的命令行参数或 -f 配置项)后重跑"
|
|
605
|
+
exit 1 ;;
|
|
606
|
+
esac
|
|
607
|
+
}
|
|
608
|
+
envf_put() { envf_check "$1" "$2"; printf '%s=%s\n' "$1" "$2" >> "$ENVF"; }
|
|
609
|
+
setk() { grep -q "^$1=" "$ENVF" || envf_put "$1" "$2"; }
|
|
440
610
|
# setk_new:同 setk,但新写入时返回 0(summary 用来判断「密码只显示一次」)
|
|
441
|
-
setk_new() { if grep -q "^$1=" "$ENVF"; then return 1; else
|
|
442
|
-
# resetk:非凭据键每次重写(翻转要落 .env,不然 teardown/汇总口径漂)
|
|
443
|
-
resetk() { grep -v "^$1=" "$ENVF" > "$ENVF.tmp" || true; mv "$ENVF.tmp" "$ENVF"; chmod 600 "$ENVF";
|
|
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"; }
|
|
444
614
|
setk SEMA_PROJECT "$PROJECT"
|
|
445
615
|
setk PG_PASSWORD "$(genpw)"
|
|
446
616
|
setk MINIO_PASSWORD "$(genpw)"
|
|
@@ -452,16 +622,23 @@ setk SERVER_PORT "$SERVER_PORT"
|
|
|
452
622
|
# data-center 数据面对外服务(agent 在别处连);full-single 数据面仅回环、只有 server 对外
|
|
453
623
|
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
|
|
454
624
|
# PG_TUNE_FLAGS 每次重算(规格可能变;非凭据,轮换无害);引号:source 与 compose env-file 双方兼容
|
|
455
|
-
|
|
456
|
-
printf 'PG_TUNE_FLAGS="%s"\n' "$PG_TUNE_FLAGS" >> "$ENVF"
|
|
625
|
+
resetk PG_TUNE_FLAGS "\"$PG_TUNE_FLAGS\""
|
|
457
626
|
if [ "$PROFILE" = "full-single" ] || [ "$PROFILE" = "local-plus" ]; then
|
|
458
627
|
setk MODEL_GATEWAY_BASEURL "$GATEWAY"
|
|
459
628
|
setk MODEL_ID "$MODEL"
|
|
460
629
|
[ -n "$MODEL_KEY" ] && setk MODEL_API_KEY "$MODEL_KEY"
|
|
461
|
-
setk REQUIRE_PRINCIPAL "$
|
|
630
|
+
setk REQUIRE_PRINCIPAL "$REQUIRE_PRINCIPAL_WANT"
|
|
462
631
|
setk SANDBOX_PKG_SOURCE "$([ "$REGION" = "global" ] && echo global || echo '')"
|
|
463
632
|
[ -n "${SEMA_SERVER_IMAGE:-}" ] && setk SEMA_SERVER_IMAGE "$SEMA_SERVER_IMAGE"
|
|
464
|
-
|
|
633
|
+
# 执行车道落 .env(S-599:从上面 `lanes resolve` 的结果渲,脚本里零车道词)。
|
|
634
|
+
# **resetk 不是 setk**:`setk` 见键在就不改,而升级路径上最常见的既有值恰是空串 `REMOTE_EXEC=`
|
|
635
|
+
# (旧 compose 的 `${REMOTE_EXEC:-}` 渲出来的)——用 setk 就永远修不好那批无手部署。冲突检测在
|
|
636
|
+
# 上面的车道块里已做过(非空且不同 ⇒ 早已拒),所以这里的重写只会落在「空串 / 同值」两种情形上。
|
|
637
|
+
while IFS='=' read -r _k _v; do
|
|
638
|
+
[ -n "$_k" ] && resetk "$_k" "$_v"
|
|
639
|
+
done <<EOF_LANE
|
|
640
|
+
$LANE_ENV
|
|
641
|
+
EOF_LANE
|
|
465
642
|
fi
|
|
466
643
|
if [ "$PROFILE" = "local-plus" ]; then
|
|
467
644
|
setk AGENT_PG_HOST "$AGENT_PG_HOST"
|
|
@@ -94,7 +94,11 @@ services:
|
|
|
94
94
|
MODEL_API_KEY: ${MODEL_API_KEY:-}
|
|
95
95
|
REQUIRE_PRINCIPAL: ${REQUIRE_PRINCIPAL:-false}
|
|
96
96
|
SANDBOX_PKG_SOURCE: ${SANDBOX_PKG_SOURCE:-}
|
|
97
|
+
# 执行车道:值由 sema-up.sh 从 lib/lanes.json 渲进 .env(resetk,每次重写)。
|
|
98
|
+
# `REQUIRE_EXECUTION_LANE` 缺省 true —— 手工起这份 compose 而没点名车道时,server 拒启
|
|
99
|
+
# 并说「没有手」,而不是安静地起成一个什么都干不了的 worker(S-599)。
|
|
97
100
|
REMOTE_EXEC: ${REMOTE_EXEC:-}
|
|
101
|
+
REQUIRE_EXECUTION_LANE: ${REQUIRE_EXECUTION_LANE:-true}
|
|
98
102
|
E2B_API_KEY: ${E2B_API_KEY:-}
|
|
99
103
|
ports:
|
|
100
104
|
- "${SERVER_BIND:-0.0.0.0}:${SERVER_PORT:-8090}:8090"
|
|
@@ -171,6 +171,8 @@ spec:
|
|
|
171
171
|
- { name: MODEL_VISION, value: "false" }
|
|
172
172
|
- { name: SANDBOX_PKG_SOURCE, value: "__PKG_SOURCE__" }
|
|
173
173
|
- { name: REMOTE_EXEC, value: k8s }
|
|
174
|
+
# S-599:显式表态「这台有手」(车道被改没了 ⇒ 拒启,不是安静的无手 worker)
|
|
175
|
+
- { name: REQUIRE_EXECUTION_LANE, value: "true" }
|
|
174
176
|
- { name: K8S_API_URL, value: "https://kubernetes.default.svc" }
|
|
175
177
|
- { name: K8S_INSECURE_TLS, value: "true" }
|
|
176
178
|
- { name: K8S_NAMESPACE, value: sema-sandbox }
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
|
|
2
|
+
import { type RemoteExecProvider } from "./execution-lane-caps.js";
|
|
2
3
|
import { type Autonomy } from "./runtime-governance.js";
|
|
3
4
|
import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
|
|
4
5
|
/** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
|
|
@@ -215,6 +216,21 @@ export declare const configLkgEnabled: () => boolean;
|
|
|
215
216
|
export declare const hostBackgroundShellEnabled: () => boolean;
|
|
216
217
|
/** The `host` exec lane's spool-file stdio form; false ⇒ the pre-1.226 pipe fallback (read per exec). */
|
|
217
218
|
export declare const hostExecSpoolEnabled: () => boolean;
|
|
219
|
+
/**
|
|
220
|
+
* 每条执行车道**必需的 env 键名**([ref] 的 fail-closed 拒启门按它点名缺件;见 `parseOrchestrationDomain`
|
|
221
|
+
* 里那处 `REMOTE_EXEC` 校验)。
|
|
222
|
+
*
|
|
223
|
+
* 键集 = `RemoteExecProvider`(判别联合派生,`execution-lane-caps.ts`):配置面加一条 provider 臂而
|
|
224
|
+
* 这里不加行 = **编译红**(codex R1-F2)。此前是 `Record<string, …>`,漏一行只表现为该词在 boot 期被判
|
|
225
|
+
* 「不是执行车道」—— fail-closed 但把「新车道」讲成「拼错了」,运维照着报文永远查不到成因。
|
|
226
|
+
*
|
|
227
|
+
* 🔴 **S-599:提到模块顶层导出**(此前是 `parseOrchestrationDomain` 的函数内局部)。零语义变,动机是
|
|
228
|
+
* 部署面需要**同一张表**:`deploy/sema-up/lib/lanes.json` 的每行必须登记它那条车道的必需键名,根 /
|
|
229
|
+
* review compose 的 environment 白名单必须把**全部**车道的必需键透传出去 —— 那两处若各手抄一份名单,
|
|
230
|
+
* 加一条车道就会静默分叉(选得到、起不来,而 config 的拒启报文里那个键在 compose 里根本不存在)。
|
|
231
|
+
* 门 = `test/deploy-lanes-contract.test.ts`,它读的是本表而不是任何副本。
|
|
232
|
+
*/
|
|
233
|
+
export declare const REQUIRED_ENVS_BY_LANE: Record<RemoteExecProvider, readonly string[]>;
|
|
218
234
|
/** [ref]① 1M dual-window (core 1.289 `Model.autoCompactTokens`): an EXPLICIT positive value wins (env
|
|
219
235
|
* `MODEL_AUTO_COMPACT_TOKENS` / center roster `autoCompactTokens`); else the **model-id table** applies —
|
|
220
236
|
* sonnet-5-family id ∧ contextWindow>=1e6 ⇒ 967000 (the faithful shape of CC's single-entry per-model
|
package/dist/config.js
CHANGED
|
@@ -450,6 +450,15 @@ export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
|
|
|
450
450
|
export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
|
|
451
451
|
export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
|
|
452
452
|
export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
|
|
453
|
+
export const REQUIRED_ENVS_BY_LANE = {
|
|
454
|
+
e2b: ["E2B_API_KEY"],
|
|
455
|
+
k8s: ["K8S_SANDBOX_IMAGE"],
|
|
456
|
+
ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
|
|
457
|
+
adb: ["ADB_SERIAL"],
|
|
458
|
+
host: [],
|
|
459
|
+
"local-docker": ["DOCKER_IMAGE"],
|
|
460
|
+
device: [],
|
|
461
|
+
};
|
|
453
462
|
function parseReadDenyPatternsEnv(raw) {
|
|
454
463
|
let arr;
|
|
455
464
|
try {
|
|
@@ -1618,15 +1627,6 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1618
1627
|
{
|
|
1619
1628
|
const lane = process.env.REMOTE_EXEC;
|
|
1620
1629
|
if (lane !== undefined && lane !== "") {
|
|
1621
|
-
const REQUIRED_ENVS_BY_LANE = {
|
|
1622
|
-
e2b: ["E2B_API_KEY"],
|
|
1623
|
-
k8s: ["K8S_SANDBOX_IMAGE"],
|
|
1624
|
-
ssh: ["SSH_HOST", "SSH_USER", "SSH_PRIVATE_KEY"],
|
|
1625
|
-
adb: ["ADB_SERIAL"],
|
|
1626
|
-
host: [],
|
|
1627
|
-
"local-docker": ["DOCKER_IMAGE"],
|
|
1628
|
-
device: [],
|
|
1629
|
-
};
|
|
1630
1630
|
const required = Object.entries(REQUIRED_ENVS_BY_LANE).find(([word]) => word === lane)?.[1];
|
|
1631
1631
|
if (required === undefined) {
|
|
1632
1632
|
throw new Error(`REMOTE_EXEC="${lane}" is not an execution lane — valid: ${Object.keys(REQUIRED_ENVS_BY_LANE).join(" | ")}. ` +
|
|
@@ -4,6 +4,7 @@ import { resolveModelApiKey } from "../../key-resolver.js";
|
|
|
4
4
|
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
6
|
import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
7
|
+
import { autonomyOverrides } from "../../runtime-governance.js";
|
|
7
8
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
8
9
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
9
10
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
@@ -136,7 +137,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
136
137
|
interactiveTools: true,
|
|
137
138
|
projectContext: cwdHonored(deps.config),
|
|
138
139
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
139
|
-
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined,
|
|
140
|
+
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
|
|
140
141
|
taskApproverPosture: taskApproverPostureHonored(deps.config),
|
|
141
142
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
142
143
|
mcpInjection: mcpInjectionHonored(deps.config),
|
|
@@ -13,7 +13,7 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
|
|
|
13
13
|
import { mintCancelledResult } from "../../run-cancel-context.js";
|
|
14
14
|
import { recordResourceWindow } from "../../resource-window.js";
|
|
15
15
|
import { redactSecrets } from "../../trace/redact.js";
|
|
16
|
-
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
|
|
16
|
+
import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
|
|
17
17
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
18
18
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
19
19
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
@@ -527,7 +527,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
527
527
|
?
|
|
528
528
|
textEnd === null || textEnd === undefined ? null : { type: "text_end", ...textEnd }
|
|
529
529
|
: ev.type === "turn_end"
|
|
530
|
-
? { type: "turn_end", ...(
|
|
530
|
+
? { type: "turn_end", ...turnEndEventData(ev), ...ident }
|
|
531
531
|
: ev.type === "message_committed"
|
|
532
532
|
? { type: "message_committed", entryId: e.entryId, role: e.role, ...(e.toolCallId !== undefined ? { toolCallId: e.toolCallId } : {}), ...ident }
|
|
533
533
|
: { type: "context_usage", ...contextUsageEventData(ev), ...ident };
|
|
@@ -138,7 +138,7 @@ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requi
|
|
|
138
138
|
type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
|
|
139
139
|
type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
|
|
140
140
|
type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
|
|
141
|
-
type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles";
|
|
141
|
+
type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles" | "responseModel";
|
|
142
142
|
type TaskResultStripped = never;
|
|
143
143
|
type _GuardTaskResult = AssertAllKeysHandled<Exclude<keyof TaskResult, TaskResultForwarded | TaskResultStripped>>;
|
|
144
144
|
type _GuardTaskResultReverse = AssertAllKeysHandled<Exclude<TaskResultForwarded | TaskResultStripped, keyof TaskResult>>;
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -904,11 +904,22 @@ export declare function contextUsageEventData(ev: {
|
|
|
904
904
|
* 而不是"未知";这正是那种"回放后看起来一切正常"的丢失。
|
|
905
905
|
* 两者**在 live 腿看得见、durable 腿回放后永久消失** —— 与 [ref] 的 `label`、[ref] 的 workflowRunId 同一族
|
|
906
906
|
* (白名单没跟上 core 新增字段)。补 builder 的同时也就把它纳入了 `wire-whitelist-exhaustiveness` 的编译期差集门。
|
|
907
|
+
*
|
|
908
|
+
* 🔴 **本构造器是 `turn_end` 帧载荷的唯一铸点**(core 7.29.0 提货批,车LY):三条腿同调 —— live SSE
|
|
909
|
+
* (`http/routes/tasks.ts` 的 `/v1/tasks/stream` 臂,身份键另由 `liveIdentityFields` 补四键)、durable 账本
|
|
910
|
+
* (`ledger-sink.ts`)、resume 腿(`resume-legs.ts`)。live 臂此前是一份**手抄**的键集,于是 core 7.29.0 加的
|
|
911
|
+
* `responseModel` 在 durable 面由这里补上以后,live 面仍会被那份手抄静默剥掉 —— 同一帧两条腿两套键集。
|
|
912
|
+
*
|
|
913
|
+
* `responseModel`(core 7.29.0 #1029):**供应商自己**报的、回答这一 turn 的模型名,**只在**它 ≠ 请求所带 id
|
|
914
|
+
* 时在场。缺席 = 「报了同名」或「没报」,**一个读法**(请求 id 读 `TaskResult.model`)。值是供应商铸的名字
|
|
915
|
+
* (与 `apiFailure.requestId` 同类:不是模型写的自由文本)⇒ 逐字;非串 / 空串 ⇒ 缺席(core 的读律
|
|
916
|
+
* `providerReportedModel` 对空串答「没报」,本仓不铸一个看起来合法的空名)。
|
|
907
917
|
*/
|
|
908
918
|
export declare function turnEndEventData(ev: {
|
|
909
919
|
usage?: unknown;
|
|
910
920
|
usageMissing?: true;
|
|
911
921
|
stopReason?: string;
|
|
922
|
+
responseModel?: unknown;
|
|
912
923
|
eventId?: unknown;
|
|
913
924
|
parentToolCallId?: unknown;
|
|
914
925
|
}): Record<string, unknown>;
|
package/dist/trace/project.js
CHANGED
|
@@ -717,6 +717,7 @@ export function turnEndEventData(ev) {
|
|
|
717
717
|
...(ev.usage ? { usage: ev.usage } : {}),
|
|
718
718
|
...(ev.usageMissing === true ? { usageMissing: true } : {}),
|
|
719
719
|
...(typeof ev.stopReason === "string" ? { stopReason: ev.stopReason } : {}),
|
|
720
|
+
...(typeof ev.responseModel === "string" && ev.responseModel !== "" ? { responseModel: ev.responseModel } : {}),
|
|
720
721
|
...identityFields(ev),
|
|
721
722
|
};
|
|
722
723
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.96.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"types": "./dist/device-protocol.d.ts",
|
|
19
19
|
"default": "./dist/device-protocol.js"
|
|
20
20
|
},
|
|
21
|
-
"./package.json": "./package.json"
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./deploy/sema-up/lanes.json": "./deploy/sema-up/lib/lanes.json"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"dist",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
|
-
"@sema-agent/core": "7.
|
|
63
|
+
"@sema-agent/core": "7.29.0",
|
|
63
64
|
"@sema-agent/settings-schema": "3.0.0",
|
|
64
65
|
"e2b": "^2.28.0",
|
|
65
66
|
"libsodium-wrappers": "^0.8.4",
|