@sema-agent/server 7.129.1 → 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 +15 -0
- package/USAGE.md +13 -2
- 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/exec-bounds.d.ts +6 -6
- 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-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,21 @@
|
|
|
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
|
+
|
|
10
25
|
## 7.129.1 —— k8s / e2b 车道:带超时的命令不再被静默切断(行为修复)· 新旋钮 `K8S_LIVENESS_MS` · 两只活性旋钮非正值拒启
|
|
11
26
|
|
|
12
27
|
1. **一条命令的界只来自调用方说出口的那些**(零 DDL):墙钟 `timeout`、流式面自己的 `readTimeoutMs`;两样都没说时才由车道活性界(无输出满 N 毫秒即切,
|
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。 |
|
|
@@ -2339,6 +2348,8 @@ e2b SDK 自己的建命令握手另有一界(缺省 60 s),比命令的界早时
|
|
|
2339
2348
|
(e2b 点名 `E2B_LIVENESS_MS`);错误码沿用引擎的 `timeout`(引擎码表把墙钟与活性两种超界归同一词,靠句子区分)。Bash 工具结果里的 `timedOut: true` / `timeoutSec: N`
|
|
2340
2349
|
不再是活性切断的投影,但仍不全是「跑满了 N 秒」:e2b 上建命令卡住被那只 60 s 握手切掉、或沙箱中途死掉时,这一发仍以 `timeout` 码回来,Bash 照旧报
|
|
2341
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` / 负数 / 非数不算说了界。
|
|
2342
2353
|
|
|
2343
2354
|
### 9.4 auto 模式(`permissionMode:"auto"`):武装三态、本地 org deny、自查(与 core 的意图武装式合修)
|
|
2344
2355
|
|
|
@@ -2521,7 +2532,7 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
|
|
|
2521
2532
|
`http` 条目 header **值**里的 `$NAME` / `${NAME}` 按条目的 `allowedEnvVars` 展开,读法是契约包 settings-schema 的 `interpolateHookEnvVars`(壳与本服务同调,同一份钩子定义到端点的字节相同):
|
|
2522
2533
|
- 名字只认大写 `[A-Z_][A-Z0-9_]*`;小写名(`$my_token`)是普通文字,原样发出。
|
|
2523
2534
|
- 列在 `allowedEnvVars` 里 ⇒ 展开成钩子环境里的值(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`;服务进程的密钥不在其中);钩子环境里没有这个名字 ⇒ 空串。
|
|
2524
|
-
- 名单外的引用 ⇒ 空串;凭据名表里的名字(`
|
|
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` 里给的同名变量会照常展开。
|
|
2525
2536
|
- 每个 header 值剥 CR / LF / NUL;header 名不展开。
|
|
2526
2537
|
- 置空(名单外 / 凭据名)时日志一行 `hook_http_header_env_blanked`(名字与原因,不带值)。
|
|
2527
2538
|
例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`。7.125.x 及更早的读法不同(升级见 MIGRATION)。
|
|
@@ -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/exec-bounds.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 执行车道上「什么约束一条命令」的**唯一一条律**(host / e2b / k8s
|
|
2
|
+
* 执行车道上「什么约束一条命令」的**唯一一条律**(host / e2b / k8s / ssh / adb / local-docker 六条车道的 adapter 同读这一只;纯叶,零依赖):
|
|
3
3
|
*
|
|
4
4
|
* 命令由调用方**说出口**的界约束 —— 墙钟 `timeout`(buffered 与流式两面)与 / 或它自己的静默界 `readTimeoutMs`
|
|
5
5
|
* (流式面)。调用方**两样都没说**时,才由车道自己的活性界兜底:无输出满这么久即判挂死(k8s `K8S_LIVENESS_MS` /
|
|
6
|
-
* e2b `E2B_LIVENESS_MS`,缺省 {@link LIVENESS_MS_DEFAULT};host 没有这一界)—— 那时它是这条命令唯一的挂死护栏。
|
|
6
|
+
* e2b `E2B_LIVENESS_MS`,缺省 {@link LIVENESS_MS_DEFAULT};host / ssh / adb / local-docker 没有这一界)—— 那时它是这条命令唯一的挂死护栏。
|
|
7
7
|
* 切断时报的是**真正触发的那一界**(活性切断的句子只在 {@link livenessCutMessage} 铸)。
|
|
8
8
|
*
|
|
9
9
|
* 为什么「说了墙钟就不再活性切断」:墙钟本身就是挂死护栏(它到点必切),而调用方说出一个墙钟,就是在说「这条命令可以跑
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 是它报的那一界。
|
|
13
13
|
*
|
|
14
14
|
* 与 core 远端契约(`ExecStreamOptions.readTimeoutMs`:「缺省也 SHOULD 有活性界,一条命令不得等到环境死」)的对齐:
|
|
15
|
-
*
|
|
15
|
+
* 说了墙钟的命令被墙钟兜住,不会无限等;两界都没说的命令,在有活性界的车道(k8s / e2b)上仍由它兜底。切断码沿用 core 的 `timeout`(core 远端码表
|
|
16
16
|
* 把墙钟与活性两种超界都归这一词);两者靠句子区分 —— 独立的活性码属于 core 的码表。
|
|
17
17
|
*
|
|
18
18
|
* 不在本律里的界(adapter 自己的上限,不是调用方说的话,不关活性界):k8s 文件读写的数据上限 `dataTimeoutMs`、e2b 交给
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
/** 车道活性界的缺省(ms)—— k8s / e2b 两只 adapter 与配置目录的「缺席 ⇒ 默认」说法同读这一只。 */
|
|
23
23
|
export declare const LIVENESS_MS_DEFAULT = 120000;
|
|
24
|
-
/**
|
|
24
|
+
/** 有活性界的车道(k8s / e2b)各自的旋钮名(闭集;切断句点名它,运维据此知道调哪一只)。 */
|
|
25
25
|
export type LivenessKnob = "K8S_LIVENESS_MS" | "E2B_LIVENESS_MS";
|
|
26
|
-
/** 一条车道自己的活性界:k8s / e2b 有,host 没有(传 `undefined`)。只经 {@link laneLivenessOf} 铸(值已判过)。 */
|
|
26
|
+
/** 一条车道自己的活性界:k8s / e2b 有,host / ssh / adb / local-docker 没有(传 `undefined`)。只经 {@link laneLivenessOf} 铸(值已判过)。 */
|
|
27
27
|
export interface LaneLiveness {
|
|
28
28
|
readonly ms: number;
|
|
29
29
|
readonly knob: LivenessKnob;
|
|
@@ -64,6 +64,6 @@ export declare function firstBoundOf(bounds: CommandBounds): {
|
|
|
64
64
|
readonly ms: number;
|
|
65
65
|
readonly cut: "wall" | LivenessBound;
|
|
66
66
|
} | undefined;
|
|
67
|
-
/** 活性切断的句子 ——
|
|
67
|
+
/** 活性切断的句子 —— 六条车道(host / e2b / k8s / ssh / adb / local-docker)唯一铸点(车道兜底的界点名旋钮与出路;调用方自己说的界点名那一参数)。 */
|
|
68
68
|
export declare function livenessCutMessage(bound: LivenessBound): string;
|
|
69
69
|
//# sourceMappingURL=exec-bounds.d.ts.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;
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -2,6 +2,7 @@ import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyO
|
|
|
2
2
|
import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
|
|
3
3
|
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
4
4
|
import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
|
|
5
|
+
import { dispatchBrainLane } from "../model-provider.js";
|
|
5
6
|
import { egressRedirectRefusal } from "../egress-redirect.js";
|
|
6
7
|
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
7
8
|
export function buildHookFailureText(raw) {
|
|
@@ -16,21 +17,17 @@ export function buildHookFailureText(raw) {
|
|
|
16
17
|
}
|
|
17
18
|
export function createHookLlm(deps) {
|
|
18
19
|
const { config, metrics } = deps;
|
|
19
|
-
const anthropicRouteBaseUrl = () => config.anthropic
|
|
20
|
+
const anthropicRouteBaseUrl = () => config.anthropic?.baseUrl || "https://api.anthropic.com";
|
|
20
21
|
const shapeFrom = (catalogRef, m, providerModelId) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
22
|
+
const lane = dispatchBrainLane(m?.provider, config.anthropic !== undefined);
|
|
23
|
+
switch (lane) {
|
|
24
|
+
case "anthropic":
|
|
25
|
+
return { ok: true, catalogRef, providerModelId, baseUrl: m?.baseUrl || anthropicRouteBaseUrl(), api: "anthropic-messages" };
|
|
26
|
+
case "gateway": {
|
|
27
|
+
const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
|
|
28
|
+
return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api: "openai-completions" } : { ok: false, error: endpoint.refusal };
|
|
29
29
|
}
|
|
30
|
-
return { ok: true, catalogRef, providerModelId, baseUrl, api };
|
|
31
30
|
}
|
|
32
|
-
const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
|
|
33
|
-
return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api } : { ok: false, error: endpoint.refusal };
|
|
34
31
|
};
|
|
35
32
|
const hookModelFor = (requested) => {
|
|
36
33
|
const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
|
|
@@ -135,7 +132,7 @@ export function createHookLlm(deps) {
|
|
|
135
132
|
const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal, redirect: "manual" });
|
|
136
133
|
const redirected = egressRedirectRefusal(r, "declare the final endpoint as this route's base URL");
|
|
137
134
|
if (redirected !== undefined)
|
|
138
|
-
return { ok: false, error: `hook llm ${redirected}
|
|
135
|
+
return { ok: false, error: `hook llm ${redirected}`, code: "redirect_refused" };
|
|
139
136
|
if (!r.ok)
|
|
140
137
|
return { ok: false, error: `hook llm HTTP ${r.status}` };
|
|
141
138
|
let text;
|
|
@@ -270,7 +270,7 @@ export type HookLlmCall = (opts: {
|
|
|
270
270
|
}>;
|
|
271
271
|
/** 失败判别码(B8:判别一律走码,禁按 error 文案分支——v3.1 批2,统检第二波 high)。error 仍是给人看的
|
|
272
272
|
* 自由文本;code 是给控制流的。缺席=未分类失败(不重试、不特判)。 */
|
|
273
|
-
export type HookLlmFailureCode = "no_content" | "hard_timeout";
|
|
273
|
+
export type HookLlmFailureCode = "no_content" | "hard_timeout" | "redirect_refused";
|
|
274
274
|
/**
|
|
275
275
|
* hook 上下文片段 → 喂给模型的单串。**所有** `additionalContext` 聚合点的唯一属主(此前是同一个
|
|
276
276
|
* `contexts.join("\n")` 表达式抄在二十余处,总量帽无处可挂 —— 那正是漂移成因)。
|
|
@@ -44,6 +44,9 @@ function retentionAdvertisement(config) {
|
|
|
44
44
|
export function createCapabilitiesLocal() {
|
|
45
45
|
return { mcpProbe: createMcpProbeLocal() };
|
|
46
46
|
}
|
|
47
|
+
function sendScenarioNotFound(res) {
|
|
48
|
+
sendError(res, 404, "scenario_not_found", "scenario not found");
|
|
49
|
+
}
|
|
47
50
|
export async function handleCapabilities(req, res, match, ctx) {
|
|
48
51
|
const { deps } = ctx;
|
|
49
52
|
switch (match.decl.id) {
|
|
@@ -51,7 +54,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
51
54
|
const name = match.params.name;
|
|
52
55
|
const detail = deps.scenarioDetails?.[name];
|
|
53
56
|
if (!detail) {
|
|
54
|
-
|
|
57
|
+
sendScenarioNotFound(res);
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
sendJson(res, 200, detail);
|
|
@@ -217,7 +220,22 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
217
220
|
sendPrincipalRefusal(res, req, deps.config);
|
|
218
221
|
return;
|
|
219
222
|
}
|
|
220
|
-
|
|
223
|
+
const scenarioParams = new URLSearchParams(match.search).getAll("scenario");
|
|
224
|
+
if (scenarioParams.length > 1) {
|
|
225
|
+
sendError(res, 400, "request.field_invalid", "scenario must be given at most once (omit it to read this deployment's default scenario)");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const requestedScenario = scenarioParams[0];
|
|
229
|
+
let scenarioName;
|
|
230
|
+
if (requestedScenario === undefined)
|
|
231
|
+
scenarioName = deps.config.defaultScenario;
|
|
232
|
+
else if (deps.scenarioGate === undefined) {
|
|
233
|
+
sendScenarioNotFound(res);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
else
|
|
237
|
+
scenarioName = await deps.scenarioGate(principal, requestedScenario);
|
|
238
|
+
await serveMcpProbeFace(res, ctx, principal, mcpForScenario(deps.config.mcpServers, scenarioName) ?? [], "deployment");
|
|
221
239
|
return;
|
|
222
240
|
}
|
|
223
241
|
case "capability-mcp-probe": {
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -599,6 +599,14 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
599
599
|
};
|
|
600
600
|
const job = prior.then(async () => {
|
|
601
601
|
const cs = deps.checkpointStore;
|
|
602
|
+
const withdrawWakeCard = async (token, scope, arm) => {
|
|
603
|
+
try {
|
|
604
|
+
await cs.expire(token, scope);
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
deps.logger?.warn?.("steer_wake_card_withdraw_failed", { taskId, sessionId: run.sessionId, arm, error: redactHead(e instanceof Error ? e.message : String(e), 2000) });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
602
610
|
const reprobe = await tryPark();
|
|
603
611
|
if (reprobe === "parked") {
|
|
604
612
|
mintResult.parked = true;
|
|
@@ -645,18 +653,18 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
645
653
|
});
|
|
646
654
|
const stillRead = await readResumeCtx(cs.getCtx(run.sessionId));
|
|
647
655
|
if (stillRead.kind === "corrupt") {
|
|
648
|
-
await
|
|
656
|
+
await withdrawWakeCard(wakeToken, cpScope, "resume_context_corrupt");
|
|
649
657
|
mintResult.corruptCtxWhere = stillRead.where;
|
|
650
658
|
return;
|
|
651
659
|
}
|
|
652
660
|
if (stillRead.kind !== "ok") {
|
|
653
|
-
await
|
|
661
|
+
await withdrawWakeCard(wakeToken, cpScope, "resume_context_absent");
|
|
654
662
|
mintResult.noResumeCtx = true;
|
|
655
663
|
return;
|
|
656
664
|
}
|
|
657
665
|
const winner = await cs.findPendingTokenBySession(run.sessionId, undefined, { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" }).catch(() => null);
|
|
658
666
|
if (winner !== null && winner !== wakeToken) {
|
|
659
|
-
await
|
|
667
|
+
await withdrawWakeCard(wakeToken, cpScope, "converged_on_winner");
|
|
660
668
|
await parkOnce(winner, cpScope);
|
|
661
669
|
return;
|
|
662
670
|
}
|
|
@@ -680,7 +688,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
680
688
|
return;
|
|
681
689
|
}
|
|
682
690
|
if (mintResult.corruptCtxWhere !== undefined) {
|
|
683
|
-
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read —
|
|
691
|
+
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
|
|
684
692
|
return;
|
|
685
693
|
}
|
|
686
694
|
if (mintResult.unservable !== undefined) {
|
package/dist/http/server.d.ts
CHANGED
|
@@ -563,6 +563,14 @@ export interface ServiceSeamDeps {
|
|
|
563
563
|
* per-principal 查询走 caps client 的 executionRuling 同车(零额外 RTT);absent = 无 center(单用户/
|
|
564
564
|
* dry-run)⇒ 观测面不接线,零行为差。fail-open:观测失败绝不影响 sync 面本身。 */
|
|
565
565
|
sessionMirrorRuling?: (principal: string | undefined) => Promise<SessionMirrorRuling | undefined>;
|
|
566
|
+
/** 请求点名的场景名过门 —— `capabilities/scenarios.ts` `createScenarioGate` 铸的**那一只**(与 `resolveSpec` 同一个工厂、同一张场景表、同一份
|
|
567
|
+
* `principalCaps`、同一份部署缺省):`GET /v1/capabilities/mcp?scenario=<名>` 的状态面靠它回答「这个场景名在不在册、这位 principal 放不放行」,
|
|
568
|
+
* 拒体(400 `scenario_unknown` 附在册词表 / 400 `scenario_not_allowed` 附 allowlist)因此与任务提交腿逐字同。
|
|
569
|
+
* 读面上这只门走**只读腿**(`createScenarioGate(…, "peek")`):问中心裁定时 center 抖动的那一拍,不替这个主体的真提交留下 fail-closed 降级项
|
|
570
|
+
* (提交腿自己是 `"enforce"`)。只在请求**点名了**场景时才问(缺席 ⇒ 部署缺省场景,不问门、不多一次中心往返;点名空串也交门答,与提交腿 `scenario: ""` 同码)。
|
|
571
|
+
* 本接缝缺席 = 嵌入方的 HTTP 面没有场景表可问:状态面点名场景答 404 `scenario_not_found`,与场景详情读面({@link scenarioDetails} 缺席)同一个答复,
|
|
572
|
+
* 不另造空表门、不静默回缺省集。 */
|
|
573
|
+
scenarioGate?: import("../capabilities/scenarios.js").ScenarioGate;
|
|
566
574
|
/** Degenerate-output a/b instrument: on errorCode=output.degenerate, inspect the session's turns to
|
|
567
575
|
* classify where the salvageable answer was (counter + log). Fire-and-forget; no-op otherwise. */
|
|
568
576
|
instrumentDegenerate?: (result: TaskResult) => void;
|
|
@@ -117,7 +117,11 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
117
117
|
};
|
|
118
118
|
readonly "server.session-titler.gateway-unreachable": {
|
|
119
119
|
readonly cls: "F";
|
|
120
|
-
readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
|
|
120
|
+
readonly note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时 / 非 2xx 应答;端点回 3xx 重定向被拒的那一族另记 `server.session-titler.redirect-refused`,不进本 tag)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。";
|
|
121
|
+
};
|
|
122
|
+
readonly "server.session-titler.redirect-refused": {
|
|
123
|
+
readonly cls: "F";
|
|
124
|
+
readonly note: "`session-titler.ts logFailure`:自动会话标题那一次廉价模型调用被拒 —— 配置的模型端点回了 3xx 重定向。本服务发往配置端点的直连请求不跟随重定向(跟随会把请求体里的用户内容与凭据头送到 `Location` 指向的另一个 origin),所以这是**端点配置问题,不是网络问题**:出路 = 把该模型的网关 URL(`MODEL_GATEWAY_BASEURL` / 目录条目自己的 baseUrl / `ANTHROPIC_BASE_URL`)改成最终地址,查网络没有用。与 `server.session-titler.gateway-unreachable` 的分界 = 失败码(llm 面交回的 `redirect_refused`),不看文案。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。放行的最坏后果 = 该部署的会话没有自动标题。逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的);`session_title_total{outcome=llm_failed}` 照常逐次记账。";
|
|
121
125
|
};
|
|
122
126
|
readonly "server.prompts.artifact-cache-write-failed": {
|
|
123
127
|
readonly cls: "F";
|
|
@@ -98,7 +98,11 @@ export const FAIL_OPEN_TAGS = {
|
|
|
98
98
|
},
|
|
99
99
|
"server.session-titler.gateway-unreachable": {
|
|
100
100
|
cls: "F",
|
|
101
|
-
note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
|
|
101
|
+
note: "S-178(cli [6662] ① clay 裁定转达,`session-titler.ts logFailure`):自动会话标题那一次廉价模型调用失败(gateway 不可达 / 凭据缺失 / 超时 / 非 2xx 应答;端点回 3xx 重定向被拒的那一族另记 `server.session-titler.redirect-refused`,不进本 tag)。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。此前每个新会话首次同 cause 打一条 `warn`,于是一台没配模型 gateway 的单机部署在日志里被这条噪音刷屏,壳因此去关 `SESSION_AUTO_TITLE`(替上游压噪),而那是把一个部署级旋钮当消音器用。现在:逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的),运维仍看得见「这台机器的 titler 一直在失败」,用户面零噪音。观测面另有 `session_title_total{outcome}` 逐次记账。",
|
|
102
|
+
},
|
|
103
|
+
"server.session-titler.redirect-refused": {
|
|
104
|
+
cls: "F",
|
|
105
|
+
note: "`session-titler.ts logFailure`:自动会话标题那一次廉价模型调用被拒 —— 配置的模型端点回了 3xx 重定向。本服务发往配置端点的直连请求不跟随重定向(跟随会把请求体里的用户内容与凭据头送到 `Location` 指向的另一个 origin),所以这是**端点配置问题,不是网络问题**:出路 = 把该模型的网关 URL(`MODEL_GATEWAY_BASEURL` / 目录条目自己的 baseUrl / `ANTHROPIC_BASE_URL`)改成最终地址,查网络没有用。与 `server.session-titler.gateway-unreachable` 的分界 = 失败码(llm 面交回的 `redirect_refused`),不看文案。标题是**纯装饰**——列表面回退 objectivePreview,零行为影响,门/裁决/账本一个字节不受它管。放行的最坏后果 = 该部署的会话没有自动标题。逐次 `debug` + 本计数 + 一次性 warn(recorder 自己的);`session_title_total{outcome=llm_failed}` 照常逐次记账。",
|
|
102
106
|
},
|
|
103
107
|
"server.prompts.artifact-cache-write-failed": {
|
|
104
108
|
cls: "F",
|
|
@@ -170,6 +170,17 @@
|
|
|
170
170
|
* **读侧半场**(同一行、同一窗,不另起登记行):由存卡唯一读者入口 `approval-card.ts` `readStoredCardEnvelope`(起服改写够不着的卡 —— 滚动升级窗里
|
|
171
171
|
* 7.128.x 副本新写的、local 车道的文件 ask 账)与停驻行入口 `currentParkArm`(**所有**停驻行 —— SQL 与 local 车道引擎 File 店的同一条路径)读时过同一只改写,
|
|
172
172
|
* **响亮**(`server.approval-card.retired-card-word-read`);读的是浅拷贝,被读的行一个字节不动。local 车道读数恒零(`NO_SQL_MIGRATIONS`)。
|
|
173
|
+
* · **M19 续跑重放腿上的存量 `toolAxes`**(7.130.0)—— 请求体 `mcpServers[].toolAxes` 的判官从本版起是引擎的 `readMcpToolAxes`(整份一坏全拒);
|
|
174
|
+
* 升级前的请求腿读者把非对象(含 `null`)整键忽略、对象逐成员剔坏留好,server 照挂。续跑重放腿重放的是升级前存下的请求体(`checkpoint_ctx`),
|
|
175
|
+
* 停驻在那台 server 某只工具审批上的任务,若这台 server 整条不进,批准即撞 `resume.tool_unavailable`、检查点重开、永远批不过去。与 M10 同形:
|
|
176
|
+
* 不改写存下的字节(体按原样留在 ctx 里),是一只**窗内才生效的行为臂** —— 窗开着时(运行期读本表 M19 行的 `removableAfter`,见 {@link migrationWindowOpen}),
|
|
177
|
+
* **只在续跑重放腿**、**只对判官判不过的那一格**按升级前的读法重读一次(`stored-request-tool-axes-m19.ts`),产物再交同一只判官(不过 ⇒ 整条不进的原臂),
|
|
178
|
+
* 真挂上的这类 server 各一行运维 warn `mcp_tool_axes_read_as_before_upgrade`;窗关之后照今天答(整条不进),删码因此是零行为变化。
|
|
179
|
+
* 窗 = 发布日 + 30 天 = 停驻行的终局兜底窗缺省(`APPROVAL_TERMINAL_BACKSTOP_MS` 缺省 30 天)—— 与 M7 / M12 同一条论证。没有起服读数(它不写库)。
|
|
180
|
+
* 窗按**发布日**算、停驻行的期限按**停驻时刻**算 ⇒ 三类部署会有升级前停驻的行活过窗、窗关后回到「整条不进」:发布后才升级的部署(发布日到它升级那天之间
|
|
181
|
+
* 停驻的行)、把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、滚动升级里仍跑 7.129.x 的副本在窗内新写的行。与 M7 / M18 一样如实登记,不为它们加长窗。
|
|
182
|
+
* **它是上游字段标准的限时例外**:引擎对 `toolAxes` 的标准是「坏键不丢、坏词不默认」(core `mcp-server-spec.d.ts`),窗内这一臂恰恰剔坏留好 ——
|
|
183
|
+
* 依据是持久数据律(存量数据不能「变红」,只许一次性迁移、窗关删码),只对升级前已经按这种读法跑过的存量体,且对运维响亮。新提交与部署腿不受它影响。
|
|
173
184
|
*
|
|
174
185
|
* ## 幂等
|
|
175
186
|
* 改写的几条都可重复跑(M4:`detail IS NOT NULL AND … = 'null'`;M5:列在场探针 + `WHERE incarnation = ''`;M6:`WHERE error_code IS NULL`;
|
|
@@ -295,7 +306,7 @@ export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
|
|
|
295
306
|
/**
|
|
296
307
|
* 一条登记行的窗**此刻**开着没有(`today` = UTC 日历日 YYYY-MM-DD):未发布形(`removableAfter: null`)⇒ 开;否则今天不晚于 `removableAfter` ⇒ 开
|
|
297
308
|
* (窗关当天仍开,与到期门「今天严格晚于才红」同一条边界)。
|
|
298
|
-
* 只给**窗内才生效的行为臂**用(M10
|
|
309
|
+
* 只给**窗内才生效的行为臂**用(M10 的认领、M19 的续跑重读):那种臂过了窗就停,不等删码车 —— 删码因此是零行为变化,而窗只写在本表这一处。
|
|
299
310
|
*/
|
|
300
311
|
export declare function migrationWindowOpen(window: Pick<OneTimeMigrationWindow, "removableAfter">, today: string): boolean;
|
|
301
312
|
/** local 车道上 SQL 独有的几条(M4 / M5 / M8 / M9 / M11 / M17 / M18)的读数(M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
|