@sema-agent/server 7.129.0 → 7.130.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 +37 -0
- package/USAGE.md +38 -2
- package/dist/boot/execution-env.js +1 -0
- package/dist/boot/resolve-spec.js +8 -4
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +2 -0
- package/dist/capabilities/mcp-probe.js +16 -3
- package/dist/capabilities/scenarios.d.ts +33 -0
- package/dist/capabilities/scenarios.js +15 -0
- package/dist/capabilities/skills.js +1 -1
- package/dist/config-catalog.js +5 -1
- package/dist/config-types.d.ts +3 -0
- package/dist/config.js +7 -1
- package/dist/exec-bounds.d.ts +69 -0
- package/dist/exec-bounds.js +37 -0
- package/dist/hooks/hook-llm.d.ts +11 -1
- package/dist/hooks/hook-llm.js +10 -13
- package/dist/hooks/hook-runner.d.ts +1 -1
- package/dist/http/routes/capabilities.js +20 -2
- package/dist/http/routes/runs.js +12 -4
- package/dist/http/server.d.ts +8 -0
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/plugins/one-time-migrations.d.ts +12 -1
- package/dist/plugins/one-time-migrations.js +17 -0
- package/dist/plugins/remote-env-adb.js +5 -4
- package/dist/plugins/remote-env-e2b.d.ts +31 -9
- package/dist/plugins/remote-env-e2b.js +77 -26
- package/dist/plugins/remote-env-host.js +7 -5
- package/dist/plugins/remote-env-k8s.d.ts +12 -4
- package/dist/plugins/remote-env-k8s.js +22 -31
- package/dist/plugins/remote-env-local-docker.js +5 -4
- package/dist/plugins/remote-env-ssh.js +5 -4
- package/dist/runtime-caps-resolver.d.ts +7 -0
- package/dist/runtime-caps-resolver.js +1 -0
- package/dist/session-titler.d.ts +4 -1
- package/dist/session-titler.js +13 -3
- package/dist/stored-request-tool-axes-m19.d.ts +29 -0
- package/dist/stored-request-tool-axes-m19.js +27 -0
- package/dist/task-mcp.d.ts +32 -1
- package/dist/task-mcp.js +66 -33
- package/dist/tool-axis-words.d.ts +14 -17
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,43 @@
|
|
|
7
7
|
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
8
|
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
9
|
|
|
10
|
+
## 7.130.0 —— 请求体 `mcpServers[].toolAxes` 坏形 400(含显式 `null`)· 升级前停驻任务的一次性迁移 M19 · 会话标题 / hooks 选线随主请求 · 技能 frontmatter 收尾只认整行 `---`
|
|
11
|
+
|
|
12
|
+
1. **请求体 `mcpServers[].toolAxes` 与部署声明读同一只判官**(零 DDL):新提交(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)里坏形的 `toolAxes`
|
|
13
|
+
—— 效应词出表、未知成员、某只工具的值不是对象、整份不是对象(**含显式 `"toolAxes": null`**)、工具名为空 —— 此前剔掉坏成员、其余照挂、请求照常 200,
|
|
14
|
+
现在 `400 request.field_invalid`,拒句点名 `mcpServers[i].toolAxes` 与原因(整份声明不收)。**出路**:不想声明就不写这个键;写了就按拒句改对。
|
|
15
|
+
`POST /v1/capabilities/mcp/probe` 同构造同拒。部署侧(`config.d/mcp.json` / 配置中心)读法不变。
|
|
16
|
+
2. **升级前停驻的任务(一次性迁移 M19,窗 = 本版发布日起 30 天)**:升级前存下的请求体里 `toolAxes` 判不过的,续跑时窗内按升级前的读法重读一次
|
|
17
|
+
(不是对象读成没声明;对象里过不了形的成员剔掉、好的留下),这台 server 照挂,任务批准后照常续跑;每次重读记一行运维 warn
|
|
18
|
+
`mcp_tool_axes_read_as_before_upgrade`。窗关之后(或重读仍判不过)该 server 整条不挂,运维 warn `mcp_injection_dropped` + 用户面通告 `mcp.injection_dropped`。
|
|
19
|
+
升级零动作;想提前收口就在窗内把这些停驻任务决掉。
|
|
20
|
+
3. **会话自动标题与 settings hooks 的 prompt / agent 条目**:`MODEL_PROVIDER=anthropic` 而只配了网关(未配 anthropic 路由)的部署上,这两条腿此前整条拒发
|
|
21
|
+
(会话列表无自动标题、hook 的 LLM 判定不生效),现在与主请求同向走网关 —— 升级后每个新会话会向网关多发一次标题请求(开着自动标题时;会话已有标题就不再发)。零动作。
|
|
22
|
+
4. **技能 frontmatter**:收尾只认整行 `---`(行尾只许空白 / CR)。此前正文里以 `---` 开头的行(如 `---x`)会提前收尾,其后的 `name` / `description` / `scenarios`
|
|
23
|
+
被丢 —— 这类技能升级后读到完整元数据,其中 `scenarios` 限制从此生效(此前因丢失而对所有场景可见)。部署了这类技能的,升级后核一次它出现在哪些场景。
|
|
24
|
+
|
|
25
|
+
## 7.129.1 —— k8s / e2b 车道:带超时的命令不再被静默切断(行为修复)· 新旋钮 `K8S_LIVENESS_MS` · 两只活性旋钮非正值拒启
|
|
26
|
+
|
|
27
|
+
1. **一条命令的界只来自调用方说出口的那些**(零 DDL):墙钟 `timeout`、流式面自己的 `readTimeoutMs`;两样都没说时才由车道活性界(无输出满 N 毫秒即切,
|
|
28
|
+
缺省 120 s)兜底。界从调用开始算、覆盖全程(含 e2b 建命令那一段;那一段另受 e2b SDK 自己的建命令握手约束,缺省 60 s,比命令的界早时先由它切)。
|
|
29
|
+
引擎的 Bash 工具每一发都带超时,所以 Bash 调用从此不再被活性界切断(安静的长构建跑到它的超时)。`timedOut` / `timeoutSec` 仍不全是跑满:e2b 上建命令
|
|
30
|
+
卡住被那只 60 s 握手切掉、或沙箱中途死掉时,这一发仍以 `timeout` 码回来,Bash 照旧报「Command timed out after <N>s」(独立的码属于引擎的码表)。
|
|
31
|
+
活性切断的句子三车道一句,点名旋钮;错误码仍是引擎的 `timeout`。
|
|
32
|
+
- **新旋钮** `K8S_LIVENESS_MS`(ms,`E2B_LIVENESS_MS` 的孪生:同一只解析器;缺席 ⇒ 120 s;只在 `REMOTE_EXEC=k8s` 时求值)。两只旋钮都**只管没有显式超时的
|
|
33
|
+
命令** —— 此前为了让带超时的长构建跑完而调大 `E2B_LIVENESS_MS` 的部署,可以调回缺省(调大只会放宽引擎探针与文件读写的挂死护栏)。
|
|
34
|
+
- 🔴 **两只活性旋钮的值必须是正数**:非数、`0`、负数 ⇒ **拒启**,拒句点名旋钮(`E2B_LIVENESS_MS=0` 此前照收,结果是每条 e2b 命令都建不起来)。
|
|
35
|
+
升级前删掉这类设置或改成正数。嵌入方直接构造 k8s / e2b 执行环境时传非正的 `livenessMs` ⇒ 构造即抛。
|
|
36
|
+
- **受伤方**:依赖「120 s 无输出即杀」兜底带超时调用的部署 —— 带超时的挂死命令要等到它自己的超时(Bash 不传参数时 120 s,与改前同;显式最长到引擎上限)。
|
|
37
|
+
出路:给命令更短的超时;不带超时的调用照旧受活性界,可调小两只旋钮。
|
|
38
|
+
- **受伤方(调大过引擎的 `BASH_DEFAULT_TIMEOUT_MS` 的部署)**:每一发不传超时参数的 Bash 都带着这个调大了的超时,从此失去 120 s 的挂死护栏 —— 挂死的命令
|
|
39
|
+
要等满它,且没有本服务的旋钮能补回。出路:把 `BASH_DEFAULT_TIMEOUT_MS` 调回,或给需要护栏的命令显式的短超时。
|
|
40
|
+
- **读面**:e2b 车道的墙钟切断、活性切断与建命令期限到点此前报 `unknown`,现在报 `timeout`(e2b 上把这类切断当 `unknown` 读的消费方改读 `timeout`);
|
|
41
|
+
k8s 流式 exec 此前忽略 `timeout`,现在照它切。
|
|
42
|
+
- **e2b 建命令被切之后才到的句柄**:一到即被杀、输出丢弃;在**看见**它不在跑之前(供应商答了杀,或 `wait()` 以成功 / 非零退出结算 —— 流先断了、连接期限到点不算看见,进程哪怕已死也继续持有)该环境的挂起(`suspendVM`)照旧以
|
|
43
|
+
`command_in_flight` 拒。杀被供应商拒时记一行 warn `e2b_abandoned_start_kill_failed`(沙箱 id / 进程号 / 拒因类名),继续等看见退出;杀被拒且退出看不见的环境从此不能挂起、VM 计费到任务结束(fail-closed),出路 = 让命令退出或结束任务。建命令一直悬着(供应商既不交回
|
|
44
|
+
句柄、也不报失败)、或杀被拒而进程不退出的那一种,挂起会一直拒(说不出那条命令没在跑,不快照在跑的命令);任务结束销毁环境不受影响。建命令**以失败
|
|
45
|
+
告终**(含 SDK 自己的 60 s 握手到点)时在途保护即撤,与 7.129.0 同 —— 供应商若其实已起了那个进程、只是回执丢了,它不受这道保护。
|
|
46
|
+
|
|
10
47
|
## 7.129.0 —— 引擎 7.51.2 提货 · 会话规则一种文法与列改名(🔴 先停旧副本再起新)· 卡词换与存量一次性改写 · 读拒三席热生效 · 直连不跟重定向 · 两只告警标签改名
|
|
11
48
|
|
|
12
49
|
1. **引擎 7.51.2(精确钉;7.51.0 的硬破坏 + 7.51.1 / 7.51.2 两补丁)**:本服务随引擎的硬破坏在下列第 2–13 条;引擎自己的收窄直接落到部署上的见第 14 条。DDL 只有第 2 条的一处列改名(SQL 后端,由起服一次性迁移做,不用手跑);第 3 条的存量改写同样由起服一次性迁移做。
|
package/USAGE.md
CHANGED
|
@@ -127,6 +127,9 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
|
|
|
127
127
|
而档位表会铸出这个词时,这份候选的模型面整组被拒(码 `config.model_refused_name_minted`,warn `web_admin_model_plane_refused` 点名
|
|
128
128
|
「被拒名 → 目标条目」)—— 否则显式点它、或绑它的角色,会静默跑在另一条条目的端点与账号上;出路:改名 / 修好条目,或改绑那条档位绑定。
|
|
129
129
|
- **failover ≠ Anthropic↔vLLM**:failover 给所有 brain 发**同一个 model**(同协议同 id 的冗余);云↔本地是**按 `model.provider` 路由**的选择,不是故障转移(两者 model id/参数不同,不能透明互切)。设 `MODEL_PROVIDER=anthropic` + `MODEL_ID=claude-…` 让整个服务走 Anthropic。
|
|
130
|
+
- **会话自动标题与 hooks 的 `prompt` / `agent` 条目走哪条线路,和主请求读同一只判据**:`provider="anthropic"` 的模型只在 anthropic 路由装上(配了 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`)时走云 `/v1/messages`;
|
|
131
|
+
没装(如 `MODEL_PROVIDER=anthropic` 却只配了 `MODEL_GATEWAY_BASEURL` + `MODEL_API_KEY`)时主请求落网关,这两个面的廉价直连调用同样落网关的 openai 兼容线路(`/chat/completions`),
|
|
132
|
+
端点与凭据照网关的来 —— 此前它们按 `provider` 字面去找一条没装的 anthropic 路由而整条拒发,会话列表没有自动标题。网关端点也解析不到才拒(`config.model_route_endpoint_missing`)。
|
|
130
133
|
- **缺省推断**:`MODEL_PROVIDER` **未设**、但显式配了 Anthropic 协议 base URL(`ANTHROPIC_BASE_URL`)**和**对应凭证(`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`)时,缺省自动判 `anthropic` 并打一行启动 warn(`model_provider_inferred`)——纯净机直连 Anthropic 兼容上游不再需要显式第七个键。显式 `MODEL_PROVIDER` 恒赢;只给 base URL 或只给凭证不推断;两者皆无 = `gateway`,与以前逐字节一致。
|
|
131
134
|
- 启动日志的 `brain` 字段会回显当前组合(`failover` / `anthropicRoute` / `anthropicCacheBreakpoints`)。
|
|
132
135
|
|
|
@@ -1230,7 +1233,12 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
|
|
|
1230
1233
|
起服日志一行 `web_admin_mcp_tool_axes_invalid { server, problem, note }`(`problem` 指到工具名与成员名,不带值);它的名字仍被部署占着,请求体里的同名条目补不了位。
|
|
1231
1234
|
一只工具坏,整条的声明都不收。缺席、`{}`、全空条目 = 没声明。
|
|
1232
1235
|
- **重启生效**(与 MCP 条目其余部分同:改了之后 `/health` 的 `restart.reasons` 含 `mcp`)。
|
|
1233
|
-
- **请求体自带的 `mcpServers[].toolAxes`**(单用户闸:调用方即信任根)
|
|
1236
|
+
- **请求体自带的 `mcpServers[].toolAxes`**(单用户闸:调用方即信任根)与部署声明读**同一只判官**(引擎根导出的 `readMcpToolAxes`;形同上一条):坏形 ⇒ 新提交 `400 request.field_invalid`,
|
|
1237
|
+
点名 `mcpServers[i].toolAxes`、server 名与引擎的原因句,**整份声明都不收**(不剔坏成员、好的那几只工具也不留);续跑重放存量请求时不拒:升级前存下的请求体在一次性迁移 M19
|
|
1238
|
+
的窗内(窗口见 `src/plugins/one-time-migrations.ts` 登记表)按升级前的读法重读一次(非对象含 `null` ⇒ 没声明;对象里过不了形的成员剔、好的留),产物仍过同一只判官 ⇒ server 照挂、
|
|
1239
|
+
一行运维 warn `mcp_tool_axes_read_as_before_upgrade`(`server` / `migration` / `problem` / `note`);重读的产物仍判不过、或窗已关 ⇒ 这一条 server 整条不进,
|
|
1240
|
+
运维 warn `mcp_injection_dropped`(`malformed_entry` / `field: toolAxes`)+ 用户面通告 `mcp.injection_dropped`(续跑家族同投);`POST /v1/capabilities/mcp/probe` 同拒同句,
|
|
1241
|
+
判官受理的声明与提交腿同读(只有空条目的工具 = 没声明,受理)。`a2aPeers[].toolAxes` 不变。
|
|
1234
1242
|
|
|
1235
1243
|
**可选 — MCP 入站表单(elicitation,E23):默认关**
|
|
1236
1244
|
|
|
@@ -1712,6 +1720,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
1712
1720
|
| `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
|
|
1713
1721
|
| `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方锁定自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
|
|
1714
1722
|
| `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
|
|
1723
|
+
| `GET /v1/capabilities/mcp[?scenario=<场景名>]` | **无需先跑 run 的逐台 MCP 状态**(现连、列工具、关掉;按调用方限速 + 30 秒复用)。答的是**某个场景**会挂哪些中心声明的 MCP 服务器:不带参数 = 部署缺省场景;`?scenario=<名>`(**7.130.0+**)= 点名场景,可见集与同场景任务 run 时是同一只解析、过同一只场景门 —— 不在册 `400 scenario_unknown`(附在册词表;空串 `?scenario=` 同此,与提交 `scenario: ""` 同答)、这位 principal 的场景白名单集外 `400 scenario_not_allowed`(附 allowlist),两者的体与提交任务时的拒逐字同形;重复给出参数 `400 request.field_invalid`(提交腿没有这一形)。嵌入方自建 `ServiceDeps` 而没递场景门接缝 `scenarioGate` 时点名场景 `404 scenario_not_found`(与场景详情读面同一个答复)。老 server 忽略这个参数、回缺省场景那一份,按 `GET /v1/capabilities` 的 `version` 判。`REQUIRE_PRINCIPAL` 下无身份 `401`(先于场景门)。自带 `.mcp.json` 的探测口是 `POST /v1/capabilities/mcp/probe` |
|
|
1715
1724
|
| `POST /v1/admin/drain` `{"actionId":"…","worker":"…","deadlineAt":…}` | **排空口**(7.121.0 起体 = settings-schema `DrainRequest`;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 ⇒ 501)。体 `{ actionId, worker, deadlineAt, scope?, expectedGeneration?, instanceId?, term? }`:`actionId` 幂等键(人手形自取,如 `manual:<uuid>`)、`worker` 本引擎的 worker 名(`SEMA_WEB_ADMIN_WORKER`)、`deadlineAt` 到期自停的 epoch ms、`scope` 本引擎的租户 scope(`FLEET_SCOPE`;不带 = 缺省 scope `global`,与没设 `FLEET_SCOPE` 的引擎相符 —— 多租引擎要带它自己的)、`expectedGeneration` fleet 成员的代际(`FLEET_GENERATION`;非成员不带)、`instanceId` 本进程身份(`GET /health` 读;不带则不比)。身份逐字段比(`worker` / `expectedGeneration` 缺席对缺席),不符 ⇒ `409 conflict.fleet_identity`(什么都不变);首次 ⇒ `200 {actionId, since, deadlineAt, state:"draining"}` 并**进入排空**(拒新提交、本副本可抢占的在飞腿当拍让出、活的审批卡改路耐久停驻、在飞归零或到期限即停);再来 ⇒ `200 … state:"already_draining"` 回显第一次的 `since` / `deadlineAt`,不重置。体坏 ⇒ `400 request.field_invalid`;`deadlineAt` 离本引擎此刻超过 `FLEET_LEASE_OUTAGE_MAX_MS`(缺省 24 h,一台实例活着而不服务的最长时长;7.122.0 起)⇒ 同码 400(什么都不变;拒句点名字段与上界的值;只在这一发会进入排空时判 —— 已在排空或硬停已在进行时任何重发都回 `already_draining`),已过的期限照收(进入即到期);嵌入宿主(无收尾链)⇒ `501 capability.drain_state_required`。没有 worker 身份的引擎(没配 `SEMA_WEB_ADMIN_WORKER`)恒 409 —— 用 SIGTERM。经本口进的排空之后,平台随后发的第一发 SIGTERM 被吸收、不缩短期限(第二发判据见「排空期的第二发信号」)。7.120.x 的 `{"reason":"…"}` 声明形与 `/health.drainReason` 已退役。 |
|
|
1716
1725
|
| `GET /v1/config/catalog` | **配置目录自描述**(operator-only —— `explicitOperatorOk`,空名单=谁都不是)。逐 env 键一行:`{name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, staticDefault?/derivedDefaultNote?, effectiveValue, valueClass, danger, center?{domain,path,precedence,timing,restartSlice?,managedNow,effectiveNow}}`(🔴 7.106.0 起 `center.domainExists` 删除:座位在加载期对契约 `DOMAIN_SCHEMAS` 核过域 + 路径,核不过即拒启 ⇒ 有 `center` 块 ⇔ 中心可写;readFace 四键 / `PERMISSIONS_DISABLE_AUTO_MODE` / `MODEL_DEGRADE_ON` / `_REACTIVE` 七行的座位随之删除);顶层带 `restartSlices`(重启片闭集)与 `centerDomains`(settings-schema 现行域表)。**两轴不混**:`writeLanes`=谁能写,`effectiveLane`=现在谁在生效(env-wins 族 env 占位 ⇒ center 腿 `effectiveNow:false`)。secret 类值**永不回传**(只回在场位;opaque 回在场位+长度;URL 剥 userinfo)。完备性由仓内 AST 双向对账门执法(分母=全 `src/` 生产 env 读取点)。旋钮 `CONFIG_CATALOG_ENABLED=false`(合规部署)⇒ 路径 404 + 能力位 `configCatalog:false`。响应 `Cache-Control: no-store`。**7.57.0 起**:`enum` 行的 `effectiveValue` **恒落在该行自己的 `enumValues` 闭集内**(仓内机器门执法)——此前 `SESSION_BACKEND` 回的是**内部标签** `tidb`(自 5.0.0 起是**退役公名**,写回去 boot 拒),现归一成公名 `mysql`;`MODEL_DEFAULT_THINKING` 补上一直被解析器接受、却漏在目录闭集外的 `xhigh`,并改为回**实算值**(`off`/未设 ⇒ `null`)而不是 env 原文。 |
|
|
1717
1726
|
| `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。候选被判官整拒(坏域 / 坏 prompts / 带事实的退役域 —— 后者 7.126.0 起)是这一拍的结论,不是失败:照样 200,`targetVersion ≠ appliedVersion`,拒因在 `config_candidate_rejected` 日志行;拍真的失败(配置源不可达 / 读不了 / 应用抛错)⇒ `502 internal.config_refresh_failed`。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
|
|
@@ -2315,6 +2324,33 @@ k8s 车道每条 run 一只新 pod(pod 寿命 = 泄漏护栏,不跨 run 复用)
|
|
|
2315
2324
|
|
|
2316
2325
|
自检是**读数,不是边界**:起服之后加入的节点在下一次起服读到;射程里的三面(所在节点、宿主 DNAT、CNI 不执行)读不出,不在闭集里。
|
|
2317
2326
|
|
|
2327
|
+
### 9.3f 沙箱车道的命令静默切断:显式超时优先(`K8S_LIVENESS_MS`,7.129.1 起)
|
|
2328
|
+
|
|
2329
|
+
k8s 与 e2b 两条沙箱车道各有一只**活性界**:一条命令无输出满这么久(缺省 120 s)即判挂死、切断。一条命令由它的调用方**说出口**的界约束,活性界只在调用方什么都没说时兜底:
|
|
2330
|
+
|
|
2331
|
+
- **带显式超时 ⇒ 只受这个超时约束**,无输出多久都不切。引擎的 Bash 工具每一发都带超时(`timeout` 参数;不传时引擎给缺省值 120 s,上限 600 s)—— 所以 Bash 调用一律走这一条。
|
|
2332
|
+
引擎的缺省值可由进程 env `BASH_DEFAULT_TIMEOUT_MS` 调大:调大它就是调大每一发不传参数的 Bash 的挂死护栏(挂死的命令要等满它),本服务没有旋钮能再收紧 —— 要护栏就调回它,或给命令显式的短超时。
|
|
2333
|
+
安静的长构建(Gradle / R8 一段几分钟不出声)给命令一个够长的 `timeout` 即可;超出引擎上限的工作用 `run_in_background`。
|
|
2334
|
+
- 流式调用自己说了静默界(`readTimeoutMs`)⇒ 照它。
|
|
2335
|
+
- 两样都没说(引擎自己的 git 探针、文件读写这类)⇒ 车道活性界兜底,它是这条命令唯一的挂死护栏。
|
|
2336
|
+
|
|
2337
|
+
界从调用开始算,覆盖命令全程 —— 含 e2b 供应商建命令那一段(命令还没起跑,自然没有输出):建命令卡住按这条命令最早的那一界切(说了超时按超时,没说界按活性界)。
|
|
2338
|
+
e2b SDK 自己的建命令握手另有一界(缺省 60 s),比命令的界早时先由它切:超时或活性界长于 60 s 的命令(含缺省 120 s 的活性界)建命令卡住在约 60 s 被切 —— 缓冲 exec(Bash 走的这一面)报 `timeout`,流式 exec 报 `connect_failed`。
|
|
2339
|
+
|
|
2340
|
+
| 旋钮 | 车道 | 缺省 | 说明 |
|
|
2341
|
+
|---|---|---|---|
|
|
2342
|
+
| `K8S_LIVENESS_MS` | `REMOTE_EXEC=k8s` | 缺席 ⇒ 120000(ms) | 7.129.1 起;与 `E2B_LIVENESS_MS` 同一只解析器:必须是正数,非数 / `0` / 负数拒启;只在本车道被选中时求值 |
|
|
2343
|
+
| `E2B_LIVENESS_MS` | `REMOTE_EXEC=e2b` | 缺席 ⇒ 120000(ms) | 同上(7.129.1 起 `0` / 负数拒启;此前照收,结果是每条命令建不起来) |
|
|
2344
|
+
|
|
2345
|
+
**有显式超时时这两只旋钮不参与**:调大不会让一条带超时的 Bash 跑得更久,调小也不会更早切它 —— 要更早切就给命令更短的超时。
|
|
2346
|
+
|
|
2347
|
+
切断时报的是真正触发的那一界:活性切断的句子是 `no output for <N> s — cut off by the exec liveness bound (set K8S_LIVENESS_MS or give the command an explicit timeout)`
|
|
2348
|
+
(e2b 点名 `E2B_LIVENESS_MS`);错误码沿用引擎的 `timeout`(引擎码表把墙钟与活性两种超界归同一词,靠句子区分)。Bash 工具结果里的 `timedOut: true` / `timeoutSec: N`
|
|
2349
|
+
不再是活性切断的投影,但仍不全是「跑满了 N 秒」:e2b 上建命令卡住被那只 60 s 握手切掉、或沙箱中途死掉时,这一发仍以 `timeout` 码回来,Bash 照旧报
|
|
2350
|
+
「Command timed out after <N>s」(把它们与超时分开的独立码属于引擎的码表)。host 车道没有活性界(命令只受调用方的超时,缓冲 exec 不带超时时受 `HOST_COMMAND_TIMEOUT_MS`)。
|
|
2351
|
+
ssh / adb / local-docker 三条车道的流式调用同读这一条律:没有车道活性界,只受自己说的静默界 `readTimeoutMs` 约束,到点的切断句与 host 同一句
|
|
2352
|
+
(`no output for <N> s — cut off by the command's own idle bound (readTimeoutMs)`);`readTimeoutMs` 为 `0` / 负数 / 非数不算说了界。
|
|
2353
|
+
|
|
2318
2354
|
### 9.4 auto 模式(`permissionMode:"auto"`):武装三态、本地 org deny、自查(与 core 的意图武装式合修)
|
|
2319
2355
|
|
|
2320
2356
|
**背景**:此前引擎只在「组织授予」(web-admin 解出的 per-principal `runtimeCaps.autoMode === true`)时武装
|
|
@@ -2496,7 +2532,7 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
|
|
|
2496
2532
|
`http` 条目 header **值**里的 `$NAME` / `${NAME}` 按条目的 `allowedEnvVars` 展开,读法是契约包 settings-schema 的 `interpolateHookEnvVars`(壳与本服务同调,同一份钩子定义到端点的字节相同):
|
|
2497
2533
|
- 名字只认大写 `[A-Z_][A-Z0-9_]*`;小写名(`$my_token`)是普通文字,原样发出。
|
|
2498
2534
|
- 列在 `allowedEnvVars` 里 ⇒ 展开成钩子环境里的值(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`;服务进程的密钥不在其中);钩子环境里没有这个名字 ⇒ 空串。
|
|
2499
|
-
- 名单外的引用 ⇒ 空串;凭据名表里的名字(`
|
|
2535
|
+
- 名单外的引用 ⇒ 空串;凭据名表里的名字(契约包 `HOOK_ENV_CREDENTIAL_NAMES`:`CLAUDE_CODE_OAUTH_TOKEN` 一类 27 个名字 × 裸名与 `INPUT_` 前缀;**不含** `ANTHROPIC_API_KEY` / `MODEL_API_KEY` 这类模型凭据名)一律空串,点进名单也一样。本服务自己的模型凭据不靠这张表护:它们从不进钩子环境(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`,服务进程的密钥名在扣留名单里),所以 `${ANTHROPIC_API_KEY}` 在本服务上展开成空串是因为钩子环境里没有它,不是因为它在凭据名表里;调用方自己在 `settings.env` 里给的同名变量会照常展开。
|
|
2500
2536
|
- 每个 header 值剥 CR / LF / NUL;header 名不展开。
|
|
2501
2537
|
- 置空(名单外 / 凭据名)时日志一行 `hook_http_header_env_blanked`(名字与原因,不带值)。
|
|
2502
2538
|
例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`。7.125.x 及更早的读法不同(升级见 MIGRATION)。
|
|
@@ -222,6 +222,7 @@ export function createExecutionEnv(ctx) {
|
|
|
222
222
|
...(config.remoteExec.runtimeClass != null ? { runtimeClass: config.remoteExec.runtimeClass } : {}),
|
|
223
223
|
...(config.remoteExec.mountPath ? { mountPath: config.remoteExec.mountPath } : {}),
|
|
224
224
|
...(config.remoteExec.timeoutMs != null ? { timeoutMs: config.remoteExec.timeoutMs } : {}),
|
|
225
|
+
...(config.remoteExec.livenessMs != null ? { livenessMs: config.remoteExec.livenessMs } : {}),
|
|
225
226
|
dataTimeoutMs: workspaceSnapshotTimeoutOf(config.remoteExec),
|
|
226
227
|
...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
|
|
227
228
|
...(config.remoteExec.cpu ? { cpu: config.remoteExec.cpu } : {}),
|
|
@@ -5,7 +5,7 @@ import { cappedCeiling } from "../budget.js";
|
|
|
5
5
|
import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
|
|
6
6
|
import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
|
|
7
7
|
import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createScenarioGate, mergeUserSkills, selectScenario } from "../capabilities/scenarios.js";
|
|
9
9
|
import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.js";
|
|
10
10
|
import { buildNoticeLaneFacts, noPersistentMemoryNotice, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
|
|
11
11
|
import { applyLongtailDefer } from "../capabilities/tool-defer.js";
|
|
@@ -35,7 +35,8 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
|
|
|
35
35
|
import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
|
|
36
36
|
import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
|
|
37
37
|
import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
|
|
38
|
-
import { assertRequestMcpContentOrigin, assertRequestMcpIdentityHeaders, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
|
|
38
|
+
import { assertRequestMcpContentOrigin, assertRequestMcpIdentityHeaders, assertRequestMcpToolAxes, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
|
|
39
|
+
import { storedToolAxesRereadM19 } from "../stored-request-tool-axes-m19.js";
|
|
39
40
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, taskHooksHonored, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
|
|
40
41
|
import { skillDenyInertEntryOf } from "../rule-entry-list.js";
|
|
41
42
|
import { TENANCY_WALL_CLOCK_BASE_SEC, enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
|
|
@@ -47,6 +48,7 @@ import { withUnattendedLeg } from "../unattended-permission-mode.js";
|
|
|
47
48
|
import { memoryFaceMounted } from "../memory-scope.js";
|
|
48
49
|
export function createResolveSpec(ctx) {
|
|
49
50
|
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
|
|
51
|
+
const scenarioGate = createScenarioGate({ config, scenarios, principalCaps, logger }, "enforce");
|
|
50
52
|
assertGuardPatternsUsable(config);
|
|
51
53
|
const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, {
|
|
52
54
|
approvalSeat: durableEnabled ? "durable" : "absent",
|
|
@@ -72,6 +74,8 @@ export function createResolveSpec(ctx) {
|
|
|
72
74
|
assertRequestMcpContentOrigin(body.mcpServers);
|
|
73
75
|
if (opts?.leg === "fresh")
|
|
74
76
|
assertRequestMcpIdentityHeaders(body.mcpServers);
|
|
77
|
+
if (opts?.leg === "fresh")
|
|
78
|
+
assertRequestMcpToolAxes(body.mcpServers);
|
|
75
79
|
if (opts?.leg === "fresh") {
|
|
76
80
|
const conflict = scratchpadCoveringTaskRoot({
|
|
77
81
|
cwd: body.cwd,
|
|
@@ -136,7 +140,7 @@ export function createResolveSpec(ctx) {
|
|
|
136
140
|
return stamped;
|
|
137
141
|
};
|
|
138
142
|
const gateScenarioAndAppend = async (body, auth, opts) => {
|
|
139
|
-
const scenarioName =
|
|
143
|
+
const scenarioName = await scenarioGate(auth?.principal, body.scenario);
|
|
140
144
|
gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), executionLaneOf(config.remoteExec?.provider), logger);
|
|
141
145
|
const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal, auth?.sessionId !== undefined ? { sessionId: auth.sessionId } : {});
|
|
142
146
|
const centerDecls = getCenterPrompts()?.declarations;
|
|
@@ -456,7 +460,7 @@ export function createResolveSpec(ctx) {
|
|
|
456
460
|
...(() => { const v = toolNameListFromBody(body.userInvokedSkills); return v ? { userInvokedSkills: v } : {}; })(),
|
|
457
461
|
...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
|
|
458
462
|
...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
|
|
459
|
-
mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
|
|
463
|
+
mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined, opts?.leg === "fresh" ? undefined : storedToolAxesRereadM19(new Date().toISOString().slice(0, 10))),
|
|
460
464
|
a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
|
|
461
465
|
promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
|
|
462
466
|
toolPolicy: toolPolicyForHands(hands, durableEnabled
|
|
@@ -56,7 +56,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
|
|
|
56
56
|
resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
|
|
57
57
|
runStore: import("../plugins/store-backend.js").RunStore | undefined;
|
|
58
58
|
scenarioDetails: Record<string, import("../capabilities/scenarios.js").ScenarioDetail>;
|
|
59
|
-
scenarios: Record<string, import("../
|
|
59
|
+
scenarios: Record<string, import("../capabilities/scenarios.js").Scenario>;
|
|
60
60
|
selectEnvTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
|
|
61
61
|
taskLimitCaps: {
|
|
62
62
|
timeoutSec: number | undefined;
|
|
@@ -19,6 +19,7 @@ import { mintEngineSignal } from "../fleet/engine-signal.js";
|
|
|
19
19
|
import { createServingLeaseClient } from "../fleet/serving-lease.js";
|
|
20
20
|
import { fleetFenceLaneOf } from "../config.js";
|
|
21
21
|
import { createResolveSpec } from "./resolve-spec.js";
|
|
22
|
+
import { createScenarioGate } from "../capabilities/scenarios.js";
|
|
22
23
|
import { executionLaneOf } from "../execution-lane-caps.js";
|
|
23
24
|
import { startFleetReconciler } from "../fleet/fleet-reconciler.js";
|
|
24
25
|
import { createSideQueryLane } from "./side-query-lane.js";
|
|
@@ -102,6 +103,7 @@ export async function runStage10(ctx) {
|
|
|
102
103
|
sessionMirrorRuling: principalCaps
|
|
103
104
|
? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
|
|
104
105
|
: undefined,
|
|
106
|
+
scenarioGate: createScenarioGate({ config, scenarios, principalCaps, logger }, "peek"),
|
|
105
107
|
instrumentDegenerate,
|
|
106
108
|
sideQueryAccounting,
|
|
107
109
|
parkedReviveSurface: parkedReviveSurface ? parkedReviveSurface : undefined,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { probeMcpServers } from "@sema-agent/core";
|
|
1
|
+
import { probeMcpServers, readMcpToolAxes } from "@sema-agent/core";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { createFixedWindowRateLimiter } from "../device-enrollment.js";
|
|
4
4
|
import { canonicalJson } from "../canonical-json.js";
|
|
5
5
|
import { echoKeyName, keyClosureClause, scanKeyClosure } from "../request-key-closure.js";
|
|
6
6
|
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
|
|
7
|
-
import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, requestEntryIdentityRefusal, validateRequestMcp, } from "../task-mcp.js";
|
|
7
|
+
import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, requestEntryIdentityRefusal, requestEntryToolAxesRefusal, validateRequestMcp, } from "../task-mcp.js";
|
|
8
8
|
import { toCoreMcpServerSpec } from "../mcp-assertion-header.js";
|
|
9
9
|
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
10
10
|
import { projectWiringManifestMcpRows } from "../trace/project.js";
|
|
@@ -64,6 +64,9 @@ function entryKeyIssue(entry, at) {
|
|
|
64
64
|
`Accepted per entry: ${[...MCP_SERVER_ENTRY_KEYS].sort().join(",")} (transport.kind "stdio": ${[...STDIO_MCP_TRANSPORT_KEYS].sort().join(",")}; "http": ${[...HTTP_MCP_TRANSPORT_KEYS].sort().join(",")})`, buckets.unknown);
|
|
65
65
|
}
|
|
66
66
|
function entryValueIssue(entry, at) {
|
|
67
|
+
const axesRefusal = requestEntryToolAxesRefusal(entry, at);
|
|
68
|
+
if (axesRefusal !== undefined)
|
|
69
|
+
return fieldRefusal(axesRefusal.message);
|
|
67
70
|
const read = validateRequestMcp([entry]);
|
|
68
71
|
if (read === null)
|
|
69
72
|
return fieldRefusal(`${at} is not a readable MCP declaration`);
|
|
@@ -82,13 +85,23 @@ function entryValueIssue(entry, at) {
|
|
|
82
85
|
function declarationDowngradeIssue(entry, spec, at) {
|
|
83
86
|
if (spec === undefined)
|
|
84
87
|
return fieldRefusal(`${at} is not a readable MCP declaration`);
|
|
85
|
-
const diff = jsonPathDiff(stripEquivalentEmpties(entry), stripEquivalentEmpties(spec), at);
|
|
88
|
+
const diff = jsonPathDiff(stripEquivalentEmpties(declaredAsJudged(entry)), stripEquivalentEmpties(spec), at);
|
|
86
89
|
if (diff.length === 0)
|
|
87
90
|
return null;
|
|
88
91
|
diff.sort();
|
|
89
92
|
return fieldRefusal(`${keyClosureClause(diff, "unusable value(s)")} — this deployment's declaration reader could not use these exactly as written, and a probe must not dial a configuration that differs from the one you declared` +
|
|
90
93
|
" (a task submission would instead keep the server and silently drop just these, which is why the two faces disagree here on purpose; this endpoint refuses, because the reading would then describe a server you did not ask about)");
|
|
91
94
|
}
|
|
95
|
+
function declaredAsJudged(entry) {
|
|
96
|
+
const declared = entry;
|
|
97
|
+
if (!("toolAxes" in declared))
|
|
98
|
+
return declared;
|
|
99
|
+
const read = readMcpToolAxes(declared.toolAxes);
|
|
100
|
+
if (!read.ok)
|
|
101
|
+
return declared;
|
|
102
|
+
const { toolAxes: _declaredAxes, ...rest } = declared;
|
|
103
|
+
return read.toolAxes === undefined ? rest : { ...rest, toolAxes: read.toolAxes };
|
|
104
|
+
}
|
|
92
105
|
function stripEquivalentEmpties(entry) {
|
|
93
106
|
const emptyRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
|
|
94
107
|
const emptyArray = (v) => Array.isArray(v) && v.length === 0;
|
|
@@ -295,4 +295,37 @@ export declare function danglingScenarioRefusal(name: string, source: ScenarioNa
|
|
|
295
295
|
* ({@link danglingScenarioRefusal})。`principal` 只进那一行运维日志,不参与判定。
|
|
296
296
|
*/
|
|
297
297
|
export declare function gateScenarioRequest(ruling: ScenarioRuling | undefined, bodyScenario: unknown, defaultScenario: string, known: readonly string[], logger?: Logger, principal?: string): string;
|
|
298
|
+
/**
|
|
299
|
+
* 一条请求点名的场景名过门 —— {@link gateScenarioRequest} 的**唯一一处实参装配**。
|
|
300
|
+
*
|
|
301
|
+
* 门的六个实参各有来源:中心裁定(`principalCaps.scenarioRuling`,按 principal)、请求点名的名字、部署缺省场景、**在册词表**
|
|
302
|
+
* (`Object.keys(scenarios)` —— 场景表活引用,不抄快照)、日志与 principal。「这个场景能看见什么」这类问题有不止一个读面
|
|
303
|
+
* (任务提交腿 `resolveSpec`、`GET /v1/capabilities/mcp?scenario=` 的状态面),每个读面自己拼一遍这六个实参 = 第二只门:
|
|
304
|
+
* 两面对「在册」「放行」各判各的,读面就开始对不上 run。所以装配只在这里,读面持有的是**绑好的门**。
|
|
305
|
+
*
|
|
306
|
+
* 契约:`(principal, requested) => 在册的场景名`(返回值一定在册;被拒 ⇒ 抛 {@link gateScenarioRequest} 的那几只 typed 错,
|
|
307
|
+
* 读面不改写)。`requested` 非串 ⇒ 门走指派链(ruling 胜者,否则部署缺省)—— 状态面只在**请求点名了**时才问门。
|
|
308
|
+
* `principalCaps` 缺席(无中心 / 干跑 / 嵌入方)⇒ 无 ruling,门退回「请求名或部署缺省」那条现状链。
|
|
309
|
+
*/
|
|
310
|
+
export interface ScenarioGateSources {
|
|
311
|
+
readonly config: {
|
|
312
|
+
readonly defaultScenario: string;
|
|
313
|
+
};
|
|
314
|
+
/** 场景表(活引用):词表按调用时的自有键取,与表同生同灭。 */
|
|
315
|
+
readonly scenarios: Readonly<Record<string, unknown>>;
|
|
316
|
+
readonly principalCaps: {
|
|
317
|
+
scenarioRuling(principal: string | undefined): Promise<ScenarioRuling | undefined>;
|
|
318
|
+
peekScenarioRuling(principal: string | undefined): Promise<ScenarioRuling | undefined>;
|
|
319
|
+
} | undefined;
|
|
320
|
+
readonly logger?: Logger;
|
|
321
|
+
}
|
|
322
|
+
export type ScenarioGate = (principal: string | undefined, requested: unknown) => Promise<string>;
|
|
323
|
+
/**
|
|
324
|
+
* 门问中心裁定走哪条腿(闭集;新增一词 ⇒ 下面的穷尽 `switch` 编译红)—— 与 `resolveRuntimeCaps` / `peekRuntimeCaps` 是同一对:
|
|
325
|
+
* · `enforce` —— 任务提交腿:中心裁定走执法腿,center 硬失败铸下的 fail-closed 降级项两腿都认;
|
|
326
|
+
* · `peek` —— 读面(`GET /v1/capabilities/mcp?scenario=`):只读腿,同一只缓存 / 同一条抓取 / 同一份 fail-open 判决,但硬失败铸的降级项只对读面自己算数,
|
|
327
|
+
* 不替这个主体的真提交留下 `{allowWorkflows:false, allowFork:false, autoMode:false}` 的短窗 deny(读面不许成为别人被拒的原因)。
|
|
328
|
+
*/
|
|
329
|
+
export type ScenarioGateLeg = "enforce" | "peek";
|
|
330
|
+
export declare function createScenarioGate(sources: ScenarioGateSources, leg: ScenarioGateLeg): ScenarioGate;
|
|
298
331
|
//# sourceMappingURL=scenarios.d.ts.map
|
|
@@ -297,4 +297,19 @@ export function gateScenarioRequest(ruling, bodyScenario, defaultScenario, known
|
|
|
297
297
|
}
|
|
298
298
|
return name;
|
|
299
299
|
}
|
|
300
|
+
export function createScenarioGate(sources, leg) {
|
|
301
|
+
const rulingOf = (principal) => {
|
|
302
|
+
switch (leg) {
|
|
303
|
+
case "enforce":
|
|
304
|
+
return sources.principalCaps?.scenarioRuling(principal);
|
|
305
|
+
case "peek":
|
|
306
|
+
return sources.principalCaps?.peekScenarioRuling(principal);
|
|
307
|
+
default: {
|
|
308
|
+
const unhandled = leg;
|
|
309
|
+
return unhandled;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
return async (principal, requested) => gateScenarioRequest(await rulingOf(principal), requested, sources.config.defaultScenario, Object.keys(sources.scenarios), sources.logger, principal);
|
|
314
|
+
}
|
|
300
315
|
//# sourceMappingURL=scenarios.js.map
|
|
@@ -107,7 +107,7 @@ export function skillsForScenario(loaded, scenario) {
|
|
|
107
107
|
return loaded.filter((k) => k.scenarios.length === 0 || k.scenarios.includes(scenario)).map((k) => k.spec);
|
|
108
108
|
}
|
|
109
109
|
export function parseFrontmatter(raw) {
|
|
110
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n
|
|
110
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?(?:\n|$)([\s\S]*)$/.exec(raw);
|
|
111
111
|
if (!m)
|
|
112
112
|
return { meta: {}, body: raw };
|
|
113
113
|
const meta = {};
|
package/dist/config-catalog.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_M
|
|
|
5
5
|
import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
|
|
6
6
|
import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS } from "./workspace-cache.js";
|
|
7
7
|
import { WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE } from "./workspace-space.js";
|
|
8
|
+
import { LIVENESS_MS_DEFAULT } from "./exec-bounds.js";
|
|
8
9
|
import { THINKING_FORMATS } from "./model-compat.js";
|
|
9
10
|
import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CENTER_BASE_URL_RULE, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
10
11
|
import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
|
|
@@ -30,6 +31,8 @@ const RESTART_SLICE_SET = {
|
|
|
30
31
|
};
|
|
31
32
|
export const CATALOG_RESTART_SLICES = Object.keys(RESTART_SLICE_SET);
|
|
32
33
|
const r = (name, domain, type, semantics, opts = {}) => ({ name, domain, type, semantics, ...opts });
|
|
34
|
+
const LIVENESS_KNOB_SEMANTICS = (lane) => `${lane} 命令静默活性上限(ms):无输出满这么久即判挂死切断,只管没有显式超时的命令 —— 带显式超时的命令只受那个超时约束(安静的长构建给命令一个显式超时即可,不必调大本值)`;
|
|
35
|
+
const LIVENESS_KNOB_DEFAULT_NOTE = `缺席 ⇒ adapter 默认 ${LIVENESS_MS_DEFAULT / 1000}s`;
|
|
33
36
|
const cLimits = (path, over = {}) => ({
|
|
34
37
|
domain: "limits",
|
|
35
38
|
path,
|
|
@@ -138,7 +141,7 @@ export const CONFIG_CATALOG = [
|
|
|
138
141
|
r("E2B_BG_DEFAULT_TIMEOUT_SEC", "orchestration", "number", "e2b 后台 shell 缺省超时(秒)", { staticDefault: "300" }),
|
|
139
142
|
r("E2B_BG_MAX_CONCURRENT", "orchestration", "number", "e2b 后台 shell 并发帽", { staticDefault: "8" }),
|
|
140
143
|
r("E2B_BG_MAX_TIMEOUT_SEC", "orchestration", "number", "e2b 后台 shell 超时上限(秒)", { staticDefault: "1800" }),
|
|
141
|
-
r("E2B_LIVENESS_MS", "orchestration", "number", "e2b
|
|
144
|
+
r("E2B_LIVENESS_MS", "orchestration", "number", LIVENESS_KNOB_SEMANTICS("e2b"), { derivedDefaultNote: LIVENESS_KNOB_DEFAULT_NOTE }),
|
|
142
145
|
r("E2B_MOUNT_PATH", "orchestration", "path", "e2b 工作区根(resume 用当前值,改前先 drain 挂起任务)"),
|
|
143
146
|
r("E2B_ORPHAN_RECLAIM", "orchestration", "boolean", "崩溃遗留沙盒回收腿(boot 一次+周期;判据保守,拿不准恒不杀)", { staticDefault: "true", danger: BILL }),
|
|
144
147
|
r("E2B_SANDBOX_ENV", "orchestration", "secret", "转发进 e2b 沙箱的本进程 env 点名表(NAME,NAME;点到的键从 worker env 原样取,值不进 spec/center/事件日志;与 LSP_ENV_ALLOW / DOCKER_SANDBOX_ENV 同一只解析器与「点名放行」谓词:形错 / 点名名单型旋钮拒启且不回显值;本进程环境里没有的名按序号 warn、不转发;只在本车道被选中时求值;原文按凭据类对待:只回在场位、不进任何子进程)", { danger: SEC }),
|
|
@@ -191,6 +194,7 @@ export const CONFIG_CATALOG = [
|
|
|
191
194
|
r("K8S_CA_CERT_B64", "orchestration", "opaque", "k8s API CA 证书(base64;registry secrets 无换行约束)", { danger: SEC }),
|
|
192
195
|
r("K8S_CPU", "orchestration", "string", "沙箱 pod CPU 限额(profile 数据)"),
|
|
193
196
|
r("K8S_INSECURE_TLS", "orchestration", "boolean", "跳过 k8s API TLS 校验", { staticDefault: "false", danger: SEC }),
|
|
197
|
+
r("K8S_LIVENESS_MS", "orchestration", "number", LIVENESS_KNOB_SEMANTICS("k8s"), { derivedDefaultNote: LIVENESS_KNOB_DEFAULT_NOTE }),
|
|
194
198
|
r("K8S_MEMORY", "orchestration", "string", "沙箱 pod 内存限额(2Gi 默认曾被真 OOM)"),
|
|
195
199
|
r("K8S_MOUNT_PATH", "orchestration", "path", "k8s 工作区挂载路径"),
|
|
196
200
|
r("K8S_NAMESPACE", "orchestration", "string", "沙箱 pod namespace"),
|
package/dist/config-types.d.ts
CHANGED
|
@@ -898,6 +898,9 @@ export interface ServiceConfigFlat {
|
|
|
898
898
|
runtimeClass?: string;
|
|
899
899
|
mountPath?: string;
|
|
900
900
|
timeoutMs?: number;
|
|
901
|
+
/** `K8S_LIVENESS_MS` — the lane's liveness bound (ms; the `E2B_LIVENESS_MS` twin, same parser; absent ⇒ the adapter's
|
|
902
|
+
* default 120 s): cuts a command silent for this long ONLY when its caller stated no timeout (`exec-bounds.ts`). */
|
|
903
|
+
livenessMs?: number;
|
|
901
904
|
/** Resource-profile knobs: sandbox pod limits (adapter defaults: 2Gi / 1 cpu). */
|
|
902
905
|
memory?: string;
|
|
903
906
|
cpu?: string;
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,7 @@ import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdent
|
|
|
27
27
|
import { declaredSemaConfigDir } from "./sema-config-dir.js";
|
|
28
28
|
import { lkgPathOverrideOf } from "./config-lkg.js";
|
|
29
29
|
import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude } from "./workspace-space.js";
|
|
30
|
+
import { laneLivenessOf } from "./exec-bounds.js";
|
|
30
31
|
import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
|
|
31
32
|
import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap } from "./workspace-cache.js";
|
|
32
33
|
import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
|
|
@@ -333,6 +334,10 @@ function optNumEnvStrict(name) {
|
|
|
333
334
|
return undefined;
|
|
334
335
|
return parseNumOrFail(name, raw);
|
|
335
336
|
}
|
|
337
|
+
function livenessMsEnv(name) {
|
|
338
|
+
const n = optNumEnvStrict(name);
|
|
339
|
+
return n === undefined ? undefined : laneLivenessOf(n, name).ms;
|
|
340
|
+
}
|
|
336
341
|
function parseDeviceEnrollTokenTtlEnv() {
|
|
337
342
|
const raw = process.env.DEVICE_ENROLL_TOKEN_TTL_SEC;
|
|
338
343
|
if (raw === undefined || raw === "")
|
|
@@ -1836,7 +1841,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1836
1841
|
apiKey: process.env.E2B_API_KEY,
|
|
1837
1842
|
...(process.env.E2B_TEMPLATE ? { template: process.env.E2B_TEMPLATE } : {}),
|
|
1838
1843
|
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("E2B_TIMEOUT_MS")),
|
|
1839
|
-
...((n) => (n !== undefined ? { livenessMs: n } : {}))(
|
|
1844
|
+
...((n) => (n !== undefined ? { livenessMs: n } : {}))(livenessMsEnv("E2B_LIVENESS_MS")),
|
|
1840
1845
|
...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
|
|
1841
1846
|
...((names) => (names !== undefined ? { sandboxEnv: allowlistedEnv(names, process.env) } : {}))(parseEnvNameAllowlist("E2B_SANDBOX_ENV", singleUserTurnkey)),
|
|
1842
1847
|
...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
|
|
@@ -1855,6 +1860,7 @@ function parseOrchestrationDomain(ctx) {
|
|
|
1855
1860
|
...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
|
|
1856
1861
|
...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
|
|
1857
1862
|
...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
|
|
1863
|
+
...((n) => (n !== undefined ? { livenessMs: n } : {}))(livenessMsEnv("K8S_LIVENESS_MS")),
|
|
1858
1864
|
...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
|
|
1859
1865
|
...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
|
|
1860
1866
|
sandboxIpv6: enumEnv("K8S_SANDBOX_IPV6", SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS),
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行车道上「什么约束一条命令」的**唯一一条律**(host / e2b / k8s / ssh / adb / local-docker 六条车道的 adapter 同读这一只;纯叶,零依赖):
|
|
3
|
+
*
|
|
4
|
+
* 命令由调用方**说出口**的界约束 —— 墙钟 `timeout`(buffered 与流式两面)与 / 或它自己的静默界 `readTimeoutMs`
|
|
5
|
+
* (流式面)。调用方**两样都没说**时,才由车道自己的活性界兜底:无输出满这么久即判挂死(k8s `K8S_LIVENESS_MS` /
|
|
6
|
+
* e2b `E2B_LIVENESS_MS`,缺省 {@link LIVENESS_MS_DEFAULT};host / ssh / adb / local-docker 没有这一界)—— 那时它是这条命令唯一的挂死护栏。
|
|
7
|
+
* 切断时报的是**真正触发的那一界**(活性切断的句子只在 {@link livenessCutMessage} 铸)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么「说了墙钟就不再活性切断」:墙钟本身就是挂死护栏(它到点必切),而调用方说出一个墙钟,就是在说「这条命令可以跑
|
|
10
|
+
* 这么久」—— 安静的长构建(Gradle / R8 一段几分钟不出声)正是这种命令。两界并存时,活性界会先于墙钟切掉一条按约正在跑的
|
|
11
|
+
* 命令,而调用方(core 的 Bash 工具)只知道自己说过的墙钟,便把这次切断报成「跑满了 N 秒」。两界互斥之后,一次切断只可能
|
|
12
|
+
* 是它报的那一界。
|
|
13
|
+
*
|
|
14
|
+
* 与 core 远端契约(`ExecStreamOptions.readTimeoutMs`:「缺省也 SHOULD 有活性界,一条命令不得等到环境死」)的对齐:
|
|
15
|
+
* 说了墙钟的命令被墙钟兜住,不会无限等;两界都没说的命令,在有活性界的车道(k8s / e2b)上仍由它兜底。切断码沿用 core 的 `timeout`(core 远端码表
|
|
16
|
+
* 把墙钟与活性两种超界都归这一词);两者靠句子区分 —— 独立的活性码属于 core 的码表。
|
|
17
|
+
*
|
|
18
|
+
* 不在本律里的界(adapter 自己的上限,不是调用方说的话,不关活性界):k8s 文件读写的数据上限 `dataTimeoutMs`、e2b 交给
|
|
19
|
+
* 供应商的沙箱寿命上限。adapter 自己的整段数据搬运(k8s 快照 tar 上 / 下)天生静默,它们**自己说**一个墙钟(数据上限),
|
|
20
|
+
* 落在本律的第一支。
|
|
21
|
+
*/
|
|
22
|
+
/** 车道活性界的缺省(ms)—— k8s / e2b 两只 adapter 与配置目录的「缺席 ⇒ 默认」说法同读这一只。 */
|
|
23
|
+
export declare const LIVENESS_MS_DEFAULT = 120000;
|
|
24
|
+
/** 有活性界的车道(k8s / e2b)各自的旋钮名(闭集;切断句点名它,运维据此知道调哪一只)。 */
|
|
25
|
+
export type LivenessKnob = "K8S_LIVENESS_MS" | "E2B_LIVENESS_MS";
|
|
26
|
+
/** 一条车道自己的活性界:k8s / e2b 有,host / ssh / adb / local-docker 没有(传 `undefined`)。只经 {@link laneLivenessOf} 铸(值已判过)。 */
|
|
27
|
+
export interface LaneLiveness {
|
|
28
|
+
readonly ms: number;
|
|
29
|
+
readonly knob: LivenessKnob;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 车道活性界的唯一铸点与唯一判据:值必须是有限正数(ms),否则**拒**(抛,句子点名旋钮)。活性界是不说界的命令唯一的挂死护栏 ——
|
|
33
|
+
* `0` / 负数会把它整个撤掉(那条命令从此无界),非数无从计时。boot 的旋钮解析(`E2B_LIVENESS_MS` / `K8S_LIVENESS_MS`)与 adapter
|
|
34
|
+
* 构造(嵌入方直接传 `livenessMs`)都经这一只判,一个判据、一句拒词。
|
|
35
|
+
*/
|
|
36
|
+
export declare function laneLivenessOf(ms: number, knob: LivenessKnob): LaneLiveness;
|
|
37
|
+
/** 本条命令的静默界:调用方说的(`readTimeoutMs`),或车道兜底的。 */
|
|
38
|
+
export type LivenessBound = {
|
|
39
|
+
readonly ms: number;
|
|
40
|
+
readonly setBy: "caller";
|
|
41
|
+
} | {
|
|
42
|
+
readonly ms: number;
|
|
43
|
+
readonly setBy: "lane";
|
|
44
|
+
readonly knob: LivenessKnob;
|
|
45
|
+
};
|
|
46
|
+
/** 一条命令最终受的界:墙钟(调用方说的)与静默界,各自可缺。 */
|
|
47
|
+
export interface CommandBounds {
|
|
48
|
+
readonly wallMs: number | undefined;
|
|
49
|
+
readonly liveness: LivenessBound | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** 调用方说出口的界(ms)。`timeoutMs` = 墙钟;`readTimeoutMs` = 它自己的静默界。 */
|
|
52
|
+
export interface StatedBounds {
|
|
53
|
+
readonly timeoutMs?: number | undefined;
|
|
54
|
+
readonly readTimeoutMs?: number | undefined;
|
|
55
|
+
}
|
|
56
|
+
/** 本律的唯一实现(见文件头注)。 */
|
|
57
|
+
export declare function commandBoundsOf(stated: StatedBounds, lane: LaneLiveness | undefined): CommandBounds;
|
|
58
|
+
/**
|
|
59
|
+
* 一条**还没出过一个字节**的命令最早被哪一界切:墙钟与静默界里较早的那只(都从调用开始算)。供应商建命令那一段(命令还没起跑,
|
|
60
|
+
* 自然也没有输出)就受它约束 —— 说了墙钟的命令不会被车道活性界在建命令时切掉;没说界的命令建命令卡住按活性界切 —— 但供应商自己的建命令期限
|
|
61
|
+
* 先到就先切(e2b SDK 的 60 s 握手:到点报的是供应商那一界,不是本模块的任何一只)。两只都缺 ⇒ 无界(只剩供应商自己的界)。
|
|
62
|
+
*/
|
|
63
|
+
export declare function firstBoundOf(bounds: CommandBounds): {
|
|
64
|
+
readonly ms: number;
|
|
65
|
+
readonly cut: "wall" | LivenessBound;
|
|
66
|
+
} | undefined;
|
|
67
|
+
/** 活性切断的句子 —— 六条车道(host / e2b / k8s / ssh / adb / local-docker)唯一铸点(车道兜底的界点名旋钮与出路;调用方自己说的界点名那一参数)。 */
|
|
68
|
+
export declare function livenessCutMessage(bound: LivenessBound): string;
|
|
69
|
+
//# sourceMappingURL=exec-bounds.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const LIVENESS_MS_DEFAULT = 120_000;
|
|
2
|
+
export function laneLivenessOf(ms, knob) {
|
|
3
|
+
if (!(Number.isFinite(ms) && ms > 0)) {
|
|
4
|
+
throw new Error(`${knob}=${ms} must be a positive number of milliseconds — it is the only hang guard of a command whose caller states no timeout, and a zero, negative or non-numeric value would remove it`);
|
|
5
|
+
}
|
|
6
|
+
return { ms, knob };
|
|
7
|
+
}
|
|
8
|
+
const said = (ms) => (ms !== undefined && Number.isFinite(ms) && ms > 0 ? ms : undefined);
|
|
9
|
+
export function commandBoundsOf(stated, lane) {
|
|
10
|
+
const wallMs = said(stated.timeoutMs);
|
|
11
|
+
const idleMs = said(stated.readTimeoutMs);
|
|
12
|
+
if (idleMs !== undefined)
|
|
13
|
+
return { wallMs, liveness: { ms: idleMs, setBy: "caller" } };
|
|
14
|
+
if (wallMs !== undefined || lane === undefined)
|
|
15
|
+
return { wallMs, liveness: undefined };
|
|
16
|
+
return { wallMs, liveness: { ms: lane.ms, setBy: "lane", knob: lane.knob } };
|
|
17
|
+
}
|
|
18
|
+
export function firstBoundOf(bounds) {
|
|
19
|
+
const { wallMs, liveness } = bounds;
|
|
20
|
+
if (liveness !== undefined && (wallMs === undefined || liveness.ms <= wallMs))
|
|
21
|
+
return { ms: liveness.ms, cut: liveness };
|
|
22
|
+
return wallMs === undefined ? undefined : { ms: wallMs, cut: "wall" };
|
|
23
|
+
}
|
|
24
|
+
function spoken(ms) {
|
|
25
|
+
if (ms < 1000)
|
|
26
|
+
return `${ms} ms`;
|
|
27
|
+
return ms % 1000 === 0 ? `${ms / 1000} s` : `${(ms / 1000).toFixed(1)} s`;
|
|
28
|
+
}
|
|
29
|
+
export function livenessCutMessage(bound) {
|
|
30
|
+
switch (bound.setBy) {
|
|
31
|
+
case "lane":
|
|
32
|
+
return `no output for ${spoken(bound.ms)} — cut off by the exec liveness bound (set ${bound.knob} or give the command an explicit timeout)`;
|
|
33
|
+
case "caller":
|
|
34
|
+
return `no output for ${spoken(bound.ms)} — cut off by the command's own idle bound (readTimeoutMs)`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=exec-bounds.js.map
|
package/dist/hooks/hook-llm.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { HookLlmCall } from "./hook-runner.js";
|
|
|
5
5
|
* hookLlm(prompt 条目/session titler 的模型载体)= 直连一次非流式 completion,从 main.ts 抽出成可注入
|
|
6
6
|
* 单元(fetchImpl/keyResolver 可假),因为这条 wire 在每个默认部署上都活(titler),必须可单测。
|
|
7
7
|
*
|
|
8
|
-
* api 形分派(2026-07-15,黑板 [ref]③b
|
|
8
|
+
* api 形分派(2026-07-15,黑板 [ref]③b 真缺陷修;**选线依据 S-1630 起改为引擎主路由同一只判词**,见下):被选模型按所落的 brain 腿分家 ——
|
|
9
9
|
* - `openai-completions` ⇒ POST {base}/chat/completions(现状,字节不变);
|
|
10
10
|
* - `anthropic-messages` ⇒ POST {base}/v1/messages(x-api-key / Authorization: Bearer + anthropic-version
|
|
11
11
|
* 头,messages 数组,回包取 content[].text)。此前 hookLlm 只会说 openai 形且缺省 baseUrl 恒落
|
|
@@ -13,6 +13,16 @@ import type { HookLlmCall } from "./hook-runner.js";
|
|
|
13
13
|
* provider=anthropic 走 ANTHROPIC_BASE_URL,没配 MODEL_GATEWAY_BASEURL)下 titler 每会话打死
|
|
14
14
|
* localhost:8000 ⇒ `session_title_failed: TypeError: fetch failed`;即使端点活着,anthropic-messages
|
|
15
15
|
* 模型也不能用 /chat/completions 形调。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **选线 = 引擎主路由同一只判词**(S-1630):「这只模型走 openai-completions 还是 anthropic-messages」不是本面的事,
|
|
18
|
+
* 是引擎主路由的事 —— `brain.ts` 按 `model.provider` 在「anthropic 路由(仅当 ANTHROPIC 凭据把它装上)/ 网关栈」之间选,
|
|
19
|
+
* 路由没装时整台机器只有网关这一只 brain,**连 `provider:"anthropic"` 的模型也落网关**(`dispatchBrainLane` 的合取,
|
|
20
|
+
* 端点判官 / 降级腿 / 启动摘要读的是同一只)。本面此前自己按 `provider === "anthropic"` / `api === "anthropic-messages"`
|
|
21
|
+
* 字面判线 ⇒ 在 `MODEL_PROVIDER=anthropic` 而只配了网关的部署上,主请求去网关、标题与 hooks 的 llm 面却要找一条没装的
|
|
22
|
+
* anthropic 路由,整条拒发(零请求,只落 fail-open 标签)。现在本面只问那一只判词,不留第二个读法 —— 含 `api` 词:
|
|
23
|
+
* 引擎不按 `api` 选线(core 的 `createRoutingBrain` 缺省按 `provider` 键),settings-schema 的 `ModelEntry` 又强制两个词同真同假。
|
|
24
|
+
* (core 目前只导出 `createRoutingBrain` 这个组合子,没有「给定模型与已装路由 → 哪条腿 / 哪个根」的判官;它导出之日,
|
|
25
|
+
* `dispatchBrainLane` 连同本调用点一并改为问它。)
|
|
16
26
|
*/
|
|
17
27
|
export type HookModelPick = {
|
|
18
28
|
ok: true;
|