@sema-agent/server 7.121.0 → 7.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/MIGRATION.md +98 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +35 -14
  5. package/dist/approval-reconciler.d.ts +8 -0
  6. package/dist/approval-reconciler.js +72 -52
  7. package/dist/auto-mode-face.d.ts +9 -2
  8. package/dist/auto-mode-face.js +5 -3
  9. package/dist/bench/s1/live-deps.js +1 -0
  10. package/dist/boot/leader.js +1 -0
  11. package/dist/boot/reapers.d.ts +3 -0
  12. package/dist/boot/reapers.js +2 -1
  13. package/dist/boot/resolve-spec.js +1 -4
  14. package/dist/boot/shutdown.js +6 -1
  15. package/dist/boot/stage-08-reapers.js +2 -2
  16. package/dist/capabilities/memory-notice.d.ts +1 -1
  17. package/dist/config-catalog.js +3 -3
  18. package/dist/config-center/center-request.d.ts +6 -9
  19. package/dist/config-center/center-request.js +11 -27
  20. package/dist/config-types.d.ts +11 -8
  21. package/dist/config-types.js +1 -1
  22. package/dist/config.d.ts +2 -2
  23. package/dist/config.js +23 -35
  24. package/dist/fleet/drain-entry.d.ts +28 -3
  25. package/dist/fleet/drain-entry.js +3 -0
  26. package/dist/fleet/serving-lease.js +13 -15
  27. package/dist/governance-overrides.d.ts +7 -2
  28. package/dist/http/admission.js +7 -5
  29. package/dist/http/approval-sweeps.js +2 -2
  30. package/dist/http/checkpoint-ctx.d.ts +60 -11
  31. package/dist/http/checkpoint-ctx.js +23 -9
  32. package/dist/http/dispatch.js +17 -5
  33. package/dist/http/resume-legs.js +8 -8
  34. package/dist/http/route-ctx.d.ts +19 -3
  35. package/dist/http/route-table.d.ts +3 -0
  36. package/dist/http/route-table.js +4 -0
  37. package/dist/http/routes/admin-drain.d.ts +5 -2
  38. package/dist/http/routes/admin-drain.js +14 -2
  39. package/dist/http/routes/approvals-assistant.d.ts +1 -0
  40. package/dist/http/routes/approvals-assistant.js +6 -6
  41. package/dist/http/routes/capabilities.js +5 -4
  42. package/dist/http/routes/fleet.d.ts +1 -0
  43. package/dist/http/routes/fleet.js +1 -1
  44. package/dist/http/routes/images.d.ts +1 -0
  45. package/dist/http/routes/images.js +1 -1
  46. package/dist/http/routes/leader.js +6 -0
  47. package/dist/http/routes/runs.d.ts +2 -0
  48. package/dist/http/routes/runs.js +5 -4
  49. package/dist/http/routes/sessions.d.ts +1 -0
  50. package/dist/http/routes/sessions.js +1 -1
  51. package/dist/http/routes/trace-usage.d.ts +1 -0
  52. package/dist/http/routes/trace-usage.js +1 -1
  53. package/dist/http/routes/workflows.d.ts +1 -0
  54. package/dist/http/routes/workflows.js +1 -1
  55. package/dist/http/send.d.ts +13 -0
  56. package/dist/http/send.js +9 -0
  57. package/dist/http/server.d.ts +3 -0
  58. package/dist/http/stored-body-replay.d.ts +27 -0
  59. package/dist/http/stored-body-replay.js +23 -0
  60. package/dist/leader/leader.d.ts +22 -1
  61. package/dist/leader/leader.js +4 -1
  62. package/dist/leader/repair-wire.d.ts +1 -1
  63. package/dist/leader/repair-wire.js +2 -2
  64. package/dist/leader/run-entry.d.ts +27 -0
  65. package/dist/leader/run-entry.js +29 -0
  66. package/dist/leader/wire.d.ts +10 -0
  67. package/dist/leader/wire.js +15 -10
  68. package/dist/model-route-url.d.ts +13 -20
  69. package/dist/model-route-url.js +28 -24
  70. package/dist/observability/fail-open.d.ts +23 -6
  71. package/dist/observability/fail-open.js +8 -4
  72. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  73. package/dist/plugins/approval-ask-store-file.js +3 -0
  74. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  75. package/dist/plugins/approval-ask-store-memory.js +6 -0
  76. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  77. package/dist/plugins/approval-ask-store-sql.js +4 -0
  78. package/dist/plugins/one-time-migrations.js +13 -0
  79. package/dist/plugins/park-commit-watch.d.ts +19 -5
  80. package/dist/plugins/park-commit-watch.js +24 -17
  81. package/dist/router/route-orchestration.d.ts +4 -2
  82. package/dist/router/route-orchestration.js +2 -1
  83. package/dist/run-local.js +3 -1
  84. package/dist/runtime-governance.d.ts +44 -32
  85. package/dist/runtime-governance.js +23 -9
  86. package/dist/sighup-idle.d.ts +7 -1
  87. package/dist/sighup-idle.js +4 -4
  88. package/dist/task-settings.d.ts +37 -14
  89. package/dist/task-settings.js +20 -20
  90. package/dist/tool-approval.d.ts +6 -0
  91. package/dist/tool-approval.js +13 -1
  92. package/dist/trace/redact.d.ts +3 -3
  93. package/dist/unattended-permission-mode.d.ts +17 -13
  94. package/dist/unattended-permission-mode.js +3 -1
  95. package/dist/url-shape.d.ts +31 -0
  96. package/dist/url-shape.js +41 -0
  97. package/package.json +2 -2
  98. package/scripts/drain-check-local.mjs +1 -1
package/MIGRATION.md CHANGED
@@ -7,6 +7,104 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.122.0 —— 配置型 URL 拒句零回显(两种模型路由形改拒启)· 排空口的期限有上界(`FLEET_LEASE_OUTAGE_MAX_MS`)· 服务租约能力位撤掉再宣告不重置失联钟 · 改路停驻的审批在停驻当拍收口(迟到作答的 202 写真)· 排空不被只读订阅流挂住 · plan 模式一只判官(运维 `AUTONOMY=plan` = 提交入口的模式词地板)
11
+
12
+ **升级前必读**:按拒句文字匹配配置拒启的脚本改按拒启码匹配(第 1 条);模型路由三键里没有 `ftp://` 一类非 http(s) 根(被消费的根修前已拒启,不被消费的那一根升级后也拒启)、没有以空 `#` 结尾的根(第 2 条,升级后拒启);
13
+ 调 `POST /v1/admin/drain` 的编排脚本 / 执行器发的 `deadlineAt` 不超过「此刻 + `FLEET_LEASE_OUTAGE_MAX_MS`」(缺省 24 h)(第 3 条);
14
+ 设了 `AUTONOMY=plan` 的部署核一遍它要的是「plan 模式」还是「整条 run 只读」,升级窗内有停驻的 run 时先读第 7 条;开了 leader 端点的这类部署上 `POST /v1/leader` 答 `422 request.autonomy_lane_refused`(第 7 条);
15
+ 请求 `settings.permissions.deny` / `.ask` 里没有 `Monitor(<命令>)`(改写成 `Bash(<命令>)`;停驻中带这一形的任务取消后按改正的规则重提)(第 8 条);
16
+ `Bash(...)` 命令规则现在也判 `Monitor`,allow 也清它的普通问 —— 核一遍自己的 allow 规则(第 9 条);
17
+ 跑在无人值守腿(leader、无终端的 `run-local`)上的脚本里没有带命令的 `sudo -s|-i`(升级后那是一张没人答的 `probe` 卡 ⇒ 拒,第 10 条)。
18
+
19
+ 1. **配置型 URL 的拒句只点名规则,不再回显值里解析出的片段**:中心基址三席、`FLEET_ADVERTISE_ADDRESS`、请求期中心基址检查、中心凭证续签的 issuer、
20
+ 模型路由三键(`MODEL_GATEWAY_BASEURL` / `MODEL_GATEWAY_FALLBACK_URLS` 逐项 / `ANTHROPIC_BASE_URL`)、`MEMORY_EMBEDDER_ENDPOINT`、`A2A_SERVE_URL` 同一只判官、同一种拒句:
21
+ `<键> <规则词>; got "<值的样子>"` —— 规则词不含 scheme,合格的 http(s) 形只展示到 origin(其后一律 `/…`),其余整只 `«redacted:url»`。
22
+ 旧拒句文字(`scheme must be http or https (got "…")`、`must be http(s) (got`、`is not a parseable absolute URL`、`must be a parseable http(s) URL`、
23
+ `must not carry credentials in the URL`,以及 userinfo / fragment 规则词的旧括号句 `(credentials never ride in a base URL: …)` / `(request paths are appended to this base)`)不再出现;
24
+ 带码的四席拒启码不变(`config.fleet_advertise_address_invalid` / `config.model_route_url_invalid` / `config.gateway_url_userinfo` / `config.model_route_url_fragment`)—— 按码匹配
25
+ (一处角形换了码:模型路由根是层级式非 http(s) **又**带 userinfo 或 fragment 时,见第 2 条);
26
+ 中心基址三席、`MEMORY_EMBEDDER_ENDPOINT`、`A2A_SERVE_URL` 的拒启句修前修后都不带机读码 —— 这五席按规则词前缀匹配
27
+ (`is not a parseable http(s) URL` / `must not carry userinfo` / `must not carry a query` / `must not carry a fragment`),不匹配括号内的解释句。拒句里不再有路径:要认是哪一条 URL,看自己的配置。
28
+ 2. **模型路由三键的两种形**:层级式非 http(s)(`ftp://…` 一类)⇒ 声明期拒启、`config.model_route_url_invalid`(被消费的根修前已由引擎的路由构造器拒启 —— 引擎的句、无本仓码、整串回显;本部署不消费的那一根修前照常起服,升级后同样拒启);
29
+ 这一形**同时**带 userinfo 或 fragment(`ftp://u:p@gw/v1`、`ftp://gw/v1#x`)⇒ 同样先判形、答 `config.model_route_url_invalid` —— 修前分别答 `config.gateway_url_userinfo` /
30
+ `config.model_route_url_fragment`,按这两个码认这类根的告警改认 `config.model_route_url_invalid`;
31
+ 根以一个空的 `#` 结尾(`https://gw/v1#`)⇒ `config.model_route_url_fragment`(修前放行)—— 删掉那个 `#`。query 照收。
32
+ 3. **排空口 `deadlineAt` 离引擎此刻超过 `FLEET_LEASE_OUTAGE_MAX_MS` ⇒ `400 request.field_invalid`**,什么都不变(没进排空、在飞不动);拒句点名 `deadlineAt` 与上界的值。
33
+ 修前任何期限都收(`此刻 + 365 天` ⇒ 200,实例不就绪直到在飞归零或第二发停止信号)。上界与服务租约的失联容忍是同一席(一台实例活着而不服务的最长时长);
34
+ 非 fleet 成员同样生效(没设就是缺省 24 h)。要发更远的期限 ⇒ 调大这台部署的 `FLEET_LEASE_OUTAGE_MAX_MS`(它同时放宽租约失联容忍)。期限按引擎的挂钟量:
35
+ 调用方与引擎时钟有偏差时,不要贴着上界发。已过的期限照旧收(进入即到期)。只在这一发会进入排空时判:已在排空(或硬停已在进行)时的重发(含换了期限的)照回 `already_draining`。
36
+ 4. **服务租约能力位撤掉再宣告,不再重置失联钟**(只影响 fleet 成员 ∧ 中心宣告过 `servingLease` 的部署;零动作):钟只由授予与本进程第一次见到能力位写。
37
+ 中心降级到不宣告能力位超过 `FLEET_LEASE_OUTAGE_MAX_MS` 再升回来时,一直没被确认的成员在位再出现后即拒新工作(`503 state.lease_expired`),中心授予即恢复 ——
38
+ 修前位再出现那一拍重新起钟。
39
+ 5. **改路停驻的审批在停驻当拍收口;迟到作答的 `202` 写真**(零动作;按 202 处置的壳核一遍):排空进入或卡窗到期把活卡改路耐久停驻之后,审批行在
40
+ 引擎把停车行写进店的那一拍就收口(修前要等对账周期扫,缺省 60 s 一拍;local 车道实测排空窗内 / 窗到期后整窗收不了)。于是对旧 approvalId 的
41
+ `POST /v1/tool-approvals/:id/respond` 修后通常直接 `200 accepted:"parked_redeemed"`。仍可能出现的 `202 reason:"parking_in_progress"`(停驻与写店之间那一段)
42
+ **不是结算 —— 作答没有生效**:体多两键 `message`(`parking in progress — not applied; retry after retryAfterMs or answer through decidePath`)与
43
+ `decidePath`(耐久回决口 `/v1/approvals/<sessionId>/decide` 的路径串)。壳按 `retryAfterMs` 重试一次,或改走 `decidePath`;把 202 当已结算的壳改掉这一处。
44
+ `decidePath` 只在停车行所在会话读得准时在场:委派子代腿与无会话的临时腿上它缺席,`message` 改为 `… or answer through the parent session's durable decide`。
45
+ 6. **排空不再被只读订阅流挂住**(零动作;读 `/health.inflight` 的监控核一遍):`/health.inflight`(排空等的同一只读数)只数在飞的工作 —— run 腿、计费提交、
46
+ HITL 应答与普通请求;只读订阅流(`/v1/approvals/stream`、`/v1/fleet/stream`、`/v1/sessions/:id/events`、`/v1/runs/:id/events`、`/v1/tasks/:id/stream`、
47
+ `/v1/runs/:id/subagents/:sub/stream`、`/v1/workflows/:id/stream`、`/v1/images/bakes/:id/events`)不再计入,读数变小。修前壳附着(常开审批订阅流)时 SIGTERM
48
+ 排空跑满整窗、再等强断才退出;修后订阅流在排空进入那一拍收到一帧 `event: error` + `{type:"error", errorCode:"draining", message}`(与流帽那一帧同一套重连协议)
49
+ 后被服务端关闭,排空只等真在飞的工作。消费端按「服务端收流 ⇒ 重连」处置(到替换实例)。排空期间新开的订阅照常服务,硬停起点同样收这一帧。
50
+ SIGHUP 空闲窗照旧把只挂着订阅流的壳当「有人在用」。
51
+ 7. **plan 模式一只判官**:用户选的 plan(顶层 `permissionMode:"plan"` / `settings.permissions.defaultMode:"plan"`)、运维 `AUTONOMY=plan`、模型自己调 `EnterPlanMode`
52
+ 都只落引擎的 `permissionMode:"plan"`。工具表与 `default` 逐名相同(委派工具 `Agent`、`Write` / `Edit` 都在),写与非只读调用在调用时由引擎的 plan 门问(有人可答)/
53
+ 拒(无人可答);委派子代以 plan 为天花板继承同一道门。带 `agents` 的 plan 任务不再以 `config.agents.no_delegation_tool` 整轮失败。
54
+ · **依赖「plan 下工具表里没有 `Agent` / `Write` / `Edit`」的消费方**:改按调用结果判(plan 门的问 / 拒),不按工具表判。
55
+ · **run 的模式词只有一个源 —— 存储的提交体顶层 `permissionMode`**:写者是每一个出生点(新的持久化写)与 `plan_review` 写回,治理层不再按段重折模式词。
56
+ · **`AUTONOMY=plan` = 提交入口的模式词地板**:不再是只读手。每个新提交的任务以 `plan` 起步(请求不带模式词 ⇒ `plan`;带任何词 ⇒ 钳成 `plan`),
57
+ 一次写在有人可答时出一张卡(人批了就写);批准(或 `edit`)一份计划之后,整条 run 按批准时选的档(`permissionModeAfter`,缺席 = `default`)跑完,
58
+ 此后每一次挂起→续跑都不回 plan;`permissionModeAfter` 对这类任务照收。热改 `AUTONOMY` 只影响之后提交的任务,在飞的 run 保持它的入口词。
59
+ 地板盖在每一个 spec 出生点:run-local 盖在它自建的 spec 上,leader run 盖在它出生时的入口体上。**跑 leader 的这类部署:`POST /v1/leader` 答
60
+ `422 request.autonomy_lane_refused`**,一条 run 都不建(上一版:受理,worker 首跑不经治理、写照落,只有 `/decide` 续跑按段折只读手)——
61
+ plan 靠人审通道(`plan_review`)出口,leader 的 worker 会话没有那条通道。出路:要 leader 照跑 ⇒ `AUTONOMY=read-only`(续跑只读手)或 `ask`;
62
+ 要计划审阅 ⇒ 改走 `POST /v1/runs`(计划经 `POST /v1/assistant/tasks/:id/plan_review` 批准)。能力位 `leader` 在这类部署上答 `false`(值变键不变),提交方按位预判;按码分支。
63
+ `ROUTER_ENABLED` 上非隔离车道按可写任务升级到监督姿态。
64
+ 请求 `dontAsk` 的无人调用方在这类部署上同样被钳成 `plan`(引擎的模式秩里 `dontAsk` 高于 `plan`):写与交计划撞 plan 门的卡(无人可答 ⇒ 拒;配耐久审批 ⇒ 停驻等人),
65
+ 批准计划时 `permissionModeAfter` 只有 `default` / `acceptEdits` 两档、回不到 `dontAsk`(上一版:只读手 + `dontAsk`,问一律由模式拒、不停驻)。
66
+ 能力面:`GET /v1/capabilities` 的 `permissionModeAuto.armed` 在这类部署上答 `false`(`reason:"mode_not_auto"`,在这里提交 auto 会被钳成 plan),`taskWriteFaceOpen` 照旧 `false`,
67
+ `leader` 由 `true` 变 `false`(车道挂着但不受理;既有 leader run 的 `GET` 照常)—— 三位读同一只地板钳。
68
+ 托管部署(`REQUIRE_PRINCIPAL=true`,未放开 `TASK_WRITE_FACE_ESCALATION`)上 `AUTONOMY=plan` × `bypassPermissions` 的请求由 `422` 变为受理并在 plan 里起步。
69
+ 要「整条 run 一个字节都不写」⇒ 改设 `AUTONOMY=read-only`。
70
+ · **升级窗内停驻着的 run × `AUTONOMY=plan`**:上一版写下的续跑上下文体里是请求原词,运维的 plan 那时由服务在每一次续跑重新施加(只读手)。
71
+ 本版不按原词重放它们:续跑(`/decide`、`plan_review`、`resume`、`wake`、终态 steer)⇒ `409 checkpoint.unsupported_version`,行字节不动、卡仍待决
72
+ (审批卡停驻的行在 `wake` / `resume` 上先被既有的人审门拒 —— `409 wake.gate_pending` / `gate_not_resumable` —— 本码在 `/decide` 与 `plan_review` 上读到),
73
+ SLA 到期拒扫跳过这类行。出路:升级前把它们决掉;或取消后作为新任务重提;或升级窗内把部署设成 `AUTONOMY=read-only`(按段的只读围栏,
74
+ 与上一版的结局相同,不触发这条拒),它们收尾之后再改回 `plan`。没设 `AUTONOMY=plan` 的部署不受影响;体词已是 `plan` 的 `plan_review` 审阅行照常批准,批准后按批准的档续跑。
75
+ 随包的排空检查(`scripts/drain-check-local.mjs` 与两份 `drain-check.*.sql`)本版**不数**这一类行(判据下一版补);升级前自查:`GET /v1/approvals` 列出的待决行,
76
+ 逐条看它所在任务提交时的模式词(顶层 `permissionMode` / `settings.permissions.defaultMode`;无词也算)—— 不是 `plan` 的那些,升级后在 `AUTONOMY=plan` 部署上续跑会被拒。
77
+ · **`AUTONOMY=read-only`**:仍是只读手 + 写面 `roots`,委派工具不再被剥 —— 子代同样只读(引擎把只读手夹进子代),带 `agents` 的任务照常起跑。
78
+ · **`plan_review` 的 `edit`**:按人改过的计划开干,与 `approve` 同:续跑解到 `permissionModeAfter`(缺席 = `default`),`edit` 现在也收这一位(修前 `400`)——
79
+ 修前 `edit` 之后仍是只读手;要「改了计划但继续只读研究」请用 `reject` + `reason`。
80
+ · **`plan_review` 的两句 `400 request.field_conflict` 改了文字**(码不变,按码匹配):错门句 `permissionModeAfter is only valid with a decision that carries the plan out ('approve' or 'edit'; got 'reject')`;
81
+ 存储模式词不是 plan 的任务送它 ⇒ `permissionModeAfter does not apply to this task: it was not started in plan mode (its stored permission mode is not "plan"), so approving the plan returns it to the mode it was in before planning`。
82
+ 8. **`Monitor(<命令>)` 不是规则拼法:请求 `settings.permissions.deny` / `.ask` 里写它 ⇒ 提交 `422`**(引擎 7.47.0 的规则编译器拒它)。修前 `Monitor(touch:*)` /
83
+ `Monitor(git push:*)` / `Monitor(timeout:5)` / `Monitor(command:touch*)` 照收、编译成一条什么都匹配不上的规则(以为拦住了 `Monitor` 的命令,其实没有);
84
+ `Monitor(touch k1)` 一类已是 422,句子换了。现在句子点名条目、引擎的码(`invalid.command_rule` / `invalid.primary_field`)与引擎的句(命令拼法给出该写的
85
+ `Bash(<命令>)`)。**出路**:改写成 `Bash(<命令>)` —— 它同时判 `Bash` 与 `Monitor`(第 9 条);`Monitor(description:…)` 与裸 `Monitor` 照收。
86
+ 停驻中的任务、请求体带这一形的:升级后续跑重解析同一份请求体,答 `409 resume_blocked_by_policy`(`blockedBy: "request.unprocessable"`)。出路 = 取消它、按改正的
87
+ `Bash(<命令>)` 重提。在 7.121.0 上把它决完也能走,但那一形在 7.121.0 上什么都不匹配 —— 决完 = 这条任务在没有那条 deny / ask 的情况下跑完
88
+ (`bypassPermissions` 下被它点名的命令照跑、不问),只在接受这一点时才选。
89
+ 同一只「编不成规则」的指引(请求 deny / ask 的 422 句、`APPROVAL_REQUIRE` / `APPROVAL_DENY` / `APPROVAL_NEVER_AUTO` 的拒启句、中心下发 `governance.approvalRequire`
90
+ 的 warn detail)不再缀「Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use mcp__<server>__* …」那一句,
91
+ 只带引擎的码与句 —— 按那句匹配的脚本改按码匹配。其余受理口不变:请求 allow 桶照收(引擎开跑时通告读成空)、`APPROVAL_GRANT` 拒启、
92
+ `POST /v1/rules` 400 `rule_rejected`、CC 导入不成候选。
93
+ 9. **`Bash(...)` 命令规则判每一只跑 shell 命令的工具,`Monitor` 在内**(引擎行为,零动作;核一遍自己的规则):deny / ask —— `deny Bash(touch:*)` 下 `Monitor`
94
+ 跑 `touch k1` 在每一种模式都被拒(修前 `default` 一张卡、`bypassPermissions` 下照跑),`ask Bash(git push:*)` 下是一张 `bypassPermissions` 不放的卡;
95
+ **allow(放宽)** —— 一条 `Bash(...)` allow 现在也清 `Monitor` 的普通问,`dontAsk` 下也是(`Bash(tail:*)` 允许 `Monitor` 跑 `tail -f app.log` 不再出卡)。
96
+ 来源不分:请求的三只桶、`APPROVAL_GRANT`、持久规则行(卡上「总是允许」、`POST /v1/rules`、两条导入车道)、技能授予。裸 `Bash` 规则仍只管名叫 `Bash`
97
+ 的工具 —— 不要 shell 就 `Bash` 与 `Monitor` 两个名字都拒。
98
+ 子代 `agents[].denyTools` / `allowTools` 是**工具名表**,不是规则文法(引擎的子代定义:`Tool(content)` 把整只 `Tool` 对该子代藏起):同一串 `Bash(touch:*)` 写在请求 deny / ask / allow 里按命令判 `Bash` 与 `Monitor`,写在子代 `denyTools` 里是对该子代藏起整只 `Bash`、够不到 `Monitor`(`Monitor` 等 hands 之外的引擎工具不受这两张表管;要按命令拦而保留工具,写调用方权限策略的 deny 规则)。
99
+ 10. **经 `sudo -s|-i` / `su` / `runuser` 的删除与读(引擎行为;无人值守腿上的脚本要核一遍)**:`sudo -s rm -rf '$HOME'`、`su root -c 'rm -rf ~'`、`runuser -u root -- rm -rf ~` 一类删受保护位置的命令,
100
+ 在 `bypassPermissions` 与 `Bash` allow 下也停在一张要人的 `dangerous_delete` 卡上(修前:`'$HOME'` 形是不点名家目录的 `unverifiable_delete`,`su` / `runuser` 形
101
+ allow 下一张 `probe` 卡、`bypassPermissions` 零卡照跑);每个带命令的 `sudo -s|-i` 都按 `bash -c` 读 —— 无害的 `sudo -s ls` 在 `default` 与 `Bash` allow 下
102
+ 多一张卡(对照 `sudo ls` 照放)。**无人值守腿上这张卡没人答 ⇒ 拒**:leader 的各条腿与无终端的 `run-local` 按类作答的答集只有 `ordinary`(`probe` 不在其中),
103
+ 所以 `Bash` allow 下的 `sudo -s ls` 被拒(修前这张卡不存在,放行);leader 配了耐久审批的部署上改为停驻、等人经 `/decide` 答;`run-local` 有终端时问人。
104
+ 要免问地以他人身份跑命令:字面写出目标。括号类里带引号 / 反斜杠的字符(`["a"]*`)现在按「任一字符」判,读拒与删除守卫会多拦几个 bash 本不碰的名字。
105
+ `tool_end.gate.disposition.released.reason` 多一个词 `write_protection_unresolved`(线名为 `Bash` / `Monitor`、声明写路径目标的调用方工具在 `bypassPermissions`
106
+ 下的释放;引擎自带的两只工具铸不出它)—— 按词分派的消费方认这个词。
107
+
10
108
  ## 7.121.0 —— 编排器片 2(引擎侧):`/readyz` 唯一探针 · 排空口契约体 · 服务租约客户端 · fleet 成员必须能被围栏
11
109
 
12
110
  **升级前核查(一句话)**:探针改指 `/readyz`(第 1 条);调 `POST /v1/admin/drain {reason}` 的改发契约体,多租引擎的排空请求带它的 `scope`(第 2 条);读 `/health.drainReason` / `modelPlaneDeferred` 的删读点(第 2 / 5 条,两键都已删);
package/README.md CHANGED
@@ -227,7 +227,7 @@ and the tool-level error forms) is documented in `USAGE.md` §9.5.
227
227
  ## Security & privacy note
228
228
 
229
229
  > 🔴 **Trace redaction ≠ egress control** (anchor: 脱敏 ≠ 出站控制). Trace redaction (the `«redacted»` markers you see in the
230
- > ledger / SSE / turns / approval previews) only rewrites the **persistence and display** surfaces —
230
+ > ledger / SSE / turns / approval previews) only rewrites the **read / display faces** (ledger, SSE, turns, approval previews, `GET /v1/sessions/:id`); it does **not** touch the engine's session archive (the audit source of truth — stored as written, redacted only on the HTTP read face, and replayed to the model route as written on every later turn) nor the session-sync faces `/v1/sessions/:id/sync/*` (the durable log goes in and out verbatim) — those two are **original text** —
231
231
  > **the request body carrying a tool result to the model's actual route is the original, unredacted
232
232
  > text.** That route is not just `MODEL_GATEWAY_BASEURL`: it can also be the fallback gateway list
233
233
  > (`MODEL_GATEWAY_FALLBACK_URLS`), an independent Anthropic route for `provider:"anthropic"` models
package/README.zh-CN.md CHANGED
@@ -193,7 +193,9 @@ Web search(部署 env `WEB_SEARCH_*` 七键、per-request `settings.webSearch`
193
193
  ## 安全与隐私提示
194
194
 
195
195
  > 🔴 **trace 脱敏 ≠ 出站控制。** trace 脱敏(你在账本 / SSE / turns / 审批预览里看到的 `«redacted»`
196
- > 标记)只作用于**持久化与展示面**——**发往「该 run 实际选中的模型路由」的请求体是工具结果原文**。
196
+ > 标记)只作用于**读面 / 展示面**(账本 / SSE / turns / 审批预览 / `GET /v1/sessions/:id`);**不碰**引擎会话存档
197
+ > (审计真源:store 里怎么写就怎么回、HTTP 读面才脱敏,之后每一次续轮按原文回送模型路由)与 session-sync 七口
198
+ > `/v1/sessions/:id/sync/*`(逐字 durable log 出入)——这两面是**原形**——**发往「该 run 实际选中的模型路由」的请求体是工具结果原文**。
197
199
  > 这条路由不止 `MODEL_GATEWAY_BASEURL` 一根:还可能是备用网关列表(`MODEL_GATEWAY_FALLBACK_URLS`)、
198
200
  > `provider:"anthropic"` 模型的独立直连路由(缺省根 `https://api.anthropic.com`,除非显式配
199
201
  > `ANTHROPIC_BASEURL`)、或配置中心可对单个目录模型下发的 per-model `baseUrl` 覆盖(见
package/USAGE.md CHANGED
@@ -676,6 +676,8 @@ SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配
676
676
  # publishNotReadyAddresses,Ingress 的活性规则指它)。缺席 = SEMA_REGISTRY_URL。用量、配额租约、
677
677
  # 拉配置始终打 SEMA_REGISTRY_URL。中心基址三席(SEMA_REGISTRY_URL / _LIVE_URL / _WEB_URL)同一条判据:
678
678
  # 可解析的 http(s) URL,不带凭据 / query / fragment(基址后面要拼路径),坏形拒启(没接中心也拒)。
679
+ # 拒句只点名规则,不回显 scheme:不是合格 http(s) 形的值(`host:8080`、`<令牌>:<口令>@host`、`ftp://…`)
680
+ # 整只成 `«redacted:url»`;合格 http(s) 形只到 `scheme://host[:port]`,后面的凭据 / 路径 / query / fragment 一律 `/…`。
679
681
  # SEMA_REGISTRY_WEB_URL=https://<中心 web 面> # 7.121.0:这个中心 **web 面**的固定基址,引擎原样转述到 `/health` 的 `center.webBaseUrl`
680
682
  # (壳按它拼会话深链,路径怎么拼归壳)。缺席即缺席,引擎不拼不猜。形判同上(三席一条判据);
681
683
  # 只在接了中心(SEMA_REGISTRY_URL)时挂。
@@ -698,7 +700,8 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启公
698
700
  # 符号 / 空白拒启);缺席 = 非 fleet 成员(单机形:不续服务租约、不受围栏)。回声到心跳与 /health 的
699
701
  # generation(连同 leaseState / leaseUntil);排空口按它比 expectedGeneration。
700
702
  # 服务租约只在中心于心跳答复里宣告 capabilities.servingLease 之后生效;没宣告 ⇒ leaseState none,
701
- # 零续租零 503。🔴 设了它就必须配齐中心三件(URL + 凭证 + SEMA_REGISTRY_WORKER)与 FLEET_ADVERTISE_ADDRESS
703
+ # 零续租零 503;位撤掉再宣告不重置 T_max 的钟(最后一次授予的到期 / 本进程第一次见到位的时刻照算)。
704
+ # 🔴 设了它就必须配齐中心三件(URL + 凭证 + SEMA_REGISTRY_WORKER)与 FLEET_ADVERTISE_ADDRESS
702
705
  # (不公告就听不到能力位),缺一 ⇒ 起服拒启 config.fleet_member_unfenced —— 成员不许悄悄脱离围栏。
703
706
  # 续租打 POST /api/fleet/live/serving-lease(契约 FLEET_PATHS.servingLease;活性基址见 SEMA_REGISTRY_LIVE_URL)。
704
707
  # 心跳答复体读不出 / 不是 JSON 对象 = 没有能力读数(位与租约钟原样,warn fleet_announce_reply_unreadable)。
@@ -706,9 +709,12 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启公
706
709
  # 缺省 scope global;在场必须是 scope id 形(小写字母 / 数字 / 内部连字符,1–64;空串拒启
707
710
  # config.fleet_scope_invalid)。与 FLEET_GENERATION 不联动。只进排空口的身份比对:请求的 scope 与它比,
708
711
  # 请求不带 scope 读成 global。续租体与心跳不带它(scope 由中心凭证绑定)。
709
- # FLEET_LEASE_OUTAGE_MAX_MS=86400000 # 7.121.0:服务租约**中心不可达的容忍上限**(缺省 24 h;< 60000 / 0 / 非整数拒启)。租约过期(中心
710
- # 不可达 / 答 unknown)后照常服务,过期满这么久 ⇒ 新工作 503 state.lease_expired(Retry-After 60;
711
- # 取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,/readyz 不翻);中心恢复授予即恢复。
712
+ # FLEET_LEASE_OUTAGE_MAX_MS=86400000 # 7.121.0:一台实例**活着而不服务的最长时长**(缺省 24 h;< 60000 / 0 / 非整数拒启)。两处读它:
713
+ # ① 服务租约中心不可达的容忍上限 —— 租约过期(中心不可达 / 答 unknown)后照常服务,过期满这么久 ⇒
714
+ # 新工作 503 state.lease_expired(Retry-After 60;取消 / HITL 应答 / 续跑 / 读面照放行,在飞不动,
715
+ # /readyz 不翻);中心恢复授予即恢复。从没被授予过的,从本进程第一次见到能力位起算。
716
+ # ② 7.122.0 起排空口 POST /v1/admin/drain 的期限上界:deadlineAt 离此刻超过它 ⇒ 400
717
+ # request.field_invalid(非 fleet 成员同样生效 —— 这一席无条件解析)。
712
718
  # 中心以正面证据拒(被取代 / 已退役)不受它管:当拍排空、到期自停,数据根记一只
713
719
  # fleet-serving-lease-denied.json,同 worker 同代际重启后仍记住、第一条心跳就是排空态(换代由中心做,
714
720
  # 不要手删)。那只文件写不进(数据根只读 / 盘满)⇒ 本进程照拒照停,但 error 行
@@ -945,6 +951,7 @@ APPROVAL_GRANT='mcp__figma__*,a2a__planner__*,Bash(git:*)' # 逗号分隔的 a
945
951
  leader 没有请求体 —— 运维为自己接的服务器、自家信得过的命令,在这里写一次。
946
952
  - **值形(与引擎的文法相同)**:工具名、`mcp__<server>__*` 或两段形 `mcp__<server>`、`a2a__<peer>__*`、`Bash(<命令>)` / `Bash(<前缀>:*)` 命令规则、
947
953
  `Read(//<dir>/**)` 读目录。`auto` 模式下交出任意代码的条目(整只 shell、解释器 / 启动器前缀)交分类器判(引擎的剔除表)。
954
+ 命令规则 `Bash(<命令>)` 按命令判每一只跑 shell 命令的工具(`Bash` 与 `Monitor`;7.122.0 起);`Monitor(<命令>)` 不是规则拼法(引擎读成空 ⇒ 启动拒,见下)。
948
955
  坏值 ⇒ **启动拒**(句子点名那一条、引擎给的原因词、怎么写):引擎读成空的条目(`*`、`mcp__*`、写路径形 `Edit(src/**)`、域名形、带空白的名字;
949
956
  7.118.0 起条目名也过引擎的判官 —— 退役名 / 无前缀短名 / 协议名服务器段铸不出读成 `tool_name`,句子带现名)、工具段为空的协议名(`mcp__srv__`)。
950
957
  内容形里带逗号的规则写不进(这张表的分隔符就是逗号)。
@@ -1070,7 +1077,7 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
1070
1077
  - **它管什么**:`permissionMode:"bypassPermissions"` 派生 `TaskSpec.writeFace:"open"` ——
1071
1078
  也就是说**一个调用方**可以掀掉写围栏(任务根之外也能写)。而引擎的解析序在普通部署上是
1072
1079
  **任务席赢部署席**,引擎里唯一压得住它的两形(org 治理 / 只读 verifier 挂载)在本仓一个结构上不可达、
1073
- 一个要靠 `AUTONOMY=read-only|plan`(那是**整条 run 禁写**,不是等比例旋钮)。⇒ 天花板由 **server 在
1080
+ 一个要靠 `AUTONOMY=read-only`(那是**整条 run 禁写**,不是等比例旋钮)。⇒ 天花板由 **server 在
1074
1081
  受理请求时**执行:不给的时候 **422 响亮拒**(`errorCode: request.host_posture_refused`),而**不是**
1075
1082
  被静默钳回 roots(静默钳是本仓明令要修掉的失败形:调用方明说「别拦我」,却撞上一道它掀不掉
1076
1083
  也看不见的墙)。
@@ -1105,10 +1112,13 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
1105
1112
  上层折成 `409 resume_blocked_by_policy`(`blockedBy: request.host_posture_refused`)。
1106
1113
  运维的解法是 `TASK_WRITE_FACE_ESCALATION=allow`。
1107
1114
  - **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422;或 —— 在
1108
- `AUTONOMY=read-only|plan` 的部署上(7.96.0 起)—— 受理但写面被钳回 roots)——壳不必靠踩一次才学会。
1109
- - **`AUTONOMY=read-only|plan` 一行未动**。⚠️ 两者叠加时**本门先说话**:托管形 + `AUTONOMY=read-only`
1115
+ `AUTONOMY=read-only` 的部署上(7.96.0 起)受理但写面被钳回 roots,在 `AUTONOMY=plan` 的部署上(7.122.0 起)受理但模式词
1116
+ 在提交入口被钳成 `plan`)——壳不必靠踩一次才学会。
1117
+ - **`AUTONOMY=read-only` 一行未动**。⚠️ 两者叠加时**本门先说话**:托管形 + `AUTONOMY=read-only`
1110
1118
  + `bypassPermissions` 的请求现在是 422,而 7.94 及以前它会跑起来(写被 autonomy 禁掉)。
1111
1119
  要保留旧结局就 `TASK_WRITE_FACE_ESCALATION=allow`(写面仍被 autonomy 压回 roots)。
1120
+ `AUTONOMY=plan`(7.122.0 起)反过来:它是提交入口的模式词地板,先于本门 —— 托管形 + `AUTONOMY=plan` + `bypassPermissions` 的请求
1121
+ 受理、在 plan 里起步(这一跑的模式词是 `plan`,写面不开,本门无话可说)。
1112
1122
 
1113
1123
  - **MCP 撤销腿(7.39 起)**:MCP 服务器 mount 仍是 restart-to-apply(新增/改址要
1114
1124
  restart 才生效),但**从 enabled 集删除一个服务器即时生效**——refresh 拍收货后,被删服务器的
@@ -1577,7 +1587,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1577
1587
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
1578
1588
  | `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) |
1579
1589
  | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
1580
- | `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_REGISTRY_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`;嵌入宿主(无收尾链)⇒ `501 capability.drain_state_required`。没有 worker 身份的引擎(没配 `SEMA_REGISTRY_WORKER`)恒 409 —— 用 SIGTERM。经本口进的排空之后,平台随后发的第一发 SIGTERM 被吸收、不缩短期限(第二发判据见「排空期的第二发信号」)。7.120.x 的 `{"reason":"…"}` 声明形与 `/health.drainReason` 已退役。 |
1590
+ | `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_REGISTRY_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_REGISTRY_WORKER`)恒 409 —— 用 SIGTERM。经本口进的排空之后,平台随后发的第一发 SIGTERM 被吸收、不缩短期限(第二发判据见「排空期的第二发信号」)。7.120.x 的 `{"reason":"…"}` 声明形与 `/health.drainReason` 已退役。 |
1581
1591
  | `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` 七行的座位随之删除,S-866 ⑤a);顶层带 `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 原文。 |
1582
1592
  | `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
1583
1593
  | `GET /health` | 健康(无需鉴权)。**7.67.0+** 恒带 `startedAt`(引擎进程起点 epoch ms,一次铸;与 `pid` 并列的换代锚——值变了就是换了一条命)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
@@ -2067,11 +2077,22 @@ GET /v1/capabilities?permissionMode=default
2067
2077
 
2068
2078
  ### 9.4b plan 模式(`permissionMode:"plan"`):批准一份计划时,顺带说「解到哪一档」(7.86.0 起)
2069
2079
 
2070
- 只读起步的任务把计划交给人之后停在 `plan_review` 门上。`POST /v1/assistant/tasks/:id/plan_review` 的
2071
- `decision:"approve"` 可携 **`permissionModeAfter`**(闭集 `"default" | "acceptEdits"`,**缺席 ⇒ `"default"`**):
2072
- `acceptEdits` = 继续跑时对自己工作目录内的写不再逐次征询,`default` = 有手但每次写照常征询;`edit` / `reject`
2073
- 的语义是重做计划,只读**原样保留**,在它们上送这一位是 `400`(词表外的值、以及本来就不是只读起步的任务送它,
2074
- 同样是 `400`)。解除会写回这条会话的续跑输入 —— 此后每一次挂起→续跑都沿用解除后的档,不回落只读。
2080
+ **plan 只有一个判官 —— 引擎的 plan 模式(7.122.0 起)**:请求顶层 `permissionMode:"plan"`、`settings.permissions.defaultMode:"plan"`、
2081
+ 运维 `AUTONOMY=plan` 与模型自己调 `EnterPlanMode`,落到的都是同一个模式词。提交之后,一条 run 的模式词只有一个源 —— 存储的提交体顶层
2082
+ `permissionMode`;写它的只有提交入口与下面的 `plan_review` 写回。运维 `AUTONOMY=plan` 是**提交入口的模式词地板**:受理时盖进提交体
2083
+ (请求不带模式词 ⇒ `plan`;带任何词 ⇒ 钳成 `plan`;bundle 的 `defaultMode` 不动,顶层赢),之后不再按段续跑重钳;热改 `AUTONOMY` 只影响之后提交的任务,
2084
+ 在飞的 run 保持它的入口词。工具表与 `default` 同面(写手、委派工具 `Agent` 都在),
2085
+ 写与非只读调用在**调用时**由引擎的 plan 门问(有人可答)/ 拒(无人可答);委派的子代以 plan 为天花板继承同一道门。所以请求体带
2086
+ `agents`(自定义子代定义)的任务进 plan 照常起跑。此前本服务把用户选的 plan 另翻成「只读手 + 剥委派工具」,带 `agents` 的任务因此整轮被引擎拒
2087
+ (`config.agents.no_delegation_tool`);只读手现在只是 `AUTONOMY=read-only` 一个词的事(子代同受只读,委派照挂)。
2088
+
2089
+ plan 起步的任务把计划交给人之后停在 `plan_review` 门上。`POST /v1/assistant/tasks/:id/plan_review` 里把计划**带出 plan** 的决定
2090
+ —— `decision:"approve"`(按原计划开干)与 `decision:"edit"`(按人改过的计划开干;7.122.0 起)—— 可携 **`permissionModeAfter`**
2091
+ (闭集 `"default" | "acceptEdits"`,**缺席 ⇒ `"default"`**):`acceptEdits` = 继续跑时对自己工作目录内的写不再逐次征询,`default` = 有手但每次写照常征询。
2092
+ `reject` 的语义是重做计划,plan **原样保留**,在它上面送这一位是 `400`(判据与引擎同句:只有 `reject` 不出 plan)。词表外的值、以及存储模式词不是 plan
2093
+ 的任务(模型自己调 `EnterPlanMode` 那一形 —— 批准把它送回进 plan 之前的档)送它,同样是 `400`;`AUTONOMY=plan` 下提交的任务存储词就是 plan,照收。
2094
+ 解除会写回这条会话的续跑输入 —— 此后每一次挂起→续跑都沿用解除后的档,不回落 plan(`AUTONOMY=plan` 也不把它拉回:地板只在提交入口盖一次)。
2095
+ 7.122.0 之前 `edit` 之后仍是只读手(模型被告知开干却没有写手),`edit` 上送这一位是 `400`。
2075
2096
  🔴 **这个体是闭集(7.86.0,S-438)**:受理集 = `decision` / `editedPlan` / `reason` / `permissionModeAfter` /
2076
2097
  `boundCallId` 五键,集外键(含定谳前的旧拼法 `nextPermissionMode`)**响亮拒** `400 request.body_shape` +
2077
2098
  机读两桶逐字点名,**不再静默丢** —— 与 `/v1/approvals/:id/decide` 同一只机器、同码同串。
@@ -2157,7 +2178,7 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2157
2178
  ### 9.6 trace 脱敏的射程:与出站请求体无关
2158
2179
 
2159
2180
  🔴 **脱敏 ≠ 出站控制**:trace 脱敏(账本 / SSE / turns / 审批预览里的 `«redacted»` 标记)只作用于
2160
- **持久化与展示面**——**工具结果发往「该 run 实际选中的模型路由」的请求体是原文**。⚠️ 「实际选中的路由」
2181
+ **读面 / 展示面**(账本 / SSE / turns / 审批预览 / `GET /v1/sessions/:id`);**不碰**引擎会话存档(审计真源:store 里怎么写就怎么回、HTTP 读面才脱敏,之后每一次续轮按原文回送模型路由)与 session-sync 七口 `/v1/sessions/:id/sync/*`(逐字 durable log 出入)——这两面是**原形**——**工具结果发往「该 run 实际选中的模型路由」的请求体是原文**。⚠️ 「实际选中的路由」
2161
2182
  不等于 `MODEL_GATEWAY_BASEURL` 一根:本部署的模型出口至少四条——默认网关(`MODEL_GATEWAY_BASEURL`)、
2162
2183
  备用网关(`MODEL_GATEWAY_FALLBACK_URLS`,failover 拓扑参与)、`provider:"anthropic"` 模型的**独立直连
2163
2184
  路由**(缺省根 `https://api.anthropic.com`,除非显式配 `ANTHROPIC_BASEURL`)、以及配置中心可对**单个
@@ -7,6 +7,7 @@ import type { Logger } from "./observability/logger.js";
7
7
  import type { Metrics } from "./observability/metrics.js";
8
8
  import { type DenyReason, type VoidReason } from "./approval-deny-reasons.js";
9
9
  import { type ApprovalRevokeFrame } from "./approval-card.js";
10
+ import type { ParkCommitWatch } from "./plugins/park-commit-watch.js";
10
11
  /** 判据表 v2 的一次判定结果(纯数据;执行器按 kind 分派到店面的一次 CAS)。 */
11
12
  export type ReconcileAction = {
12
13
  kind: "bind";
@@ -230,6 +231,13 @@ export interface ApprovalReconcilerDeps {
230
231
  */
231
232
  unattendedPolicy: UnattendedApprovalPolicy;
232
233
  emitRevoke?: ApprovalRevokeEmitter | undefined;
234
+ /**
235
+ * S-1280:park 提交的唯一观察点(`plugins/park-commit-watch.ts`;在场 ⇔ 本部署有 checkpoint 店)。在场 ⇒ 收敛器订阅它:一只审批停车行
236
+ * 提交的那一拍,对**这次调用**的 `PARKING` 行即时跑判据 1(与周期扫同一只执行体),`put` 等它收完才交还 core。缺席 ⇒ 只有周期扫(修前形)。
237
+ */
238
+ parkCommits?: Pick<ParkCommitWatch, "onParkCommitted" | "track"> | undefined;
239
+ /** {@link PARK_COMMIT_BIND_BUDGET_MS} 的注入口 —— 不接 env(不是部署旋钮,是这一拍拖住 core 停驻的上界),可注入只为让判据观测得到。 */
240
+ parkCommitBindBudgetMs?: number | undefined;
233
241
  }
234
242
  /** {@link createApprovalReconciler} 返回的活对象。 */
235
243
  export interface ApprovalReconciler {
@@ -6,6 +6,7 @@ import { buildRevokeFrame } from "./approval-card.js";
6
6
  import { encodeCheckpointScope } from "./security.js";
7
7
  const RECONCILE_STORE_TIMEOUT_MS = 10_000;
8
8
  const RECONCILE_SEGMENT_BUDGET_MS = 30_000;
9
+ const PARK_COMMIT_BIND_BUDGET_MS = 2_000;
9
10
  function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
10
11
  let timer;
11
12
  return Promise.race([
@@ -80,6 +81,13 @@ export function decideReconcileAction(input) {
80
81
  }
81
82
  export function createApprovalReconciler(deps) {
82
83
  const { askStore, checkpoints, runs, logger, metrics, batchLimit, pendingGraceMs, adhocGraceMs, orphanTtlMs, unattendedPolicy, emitRevoke } = deps;
84
+ const warn = (event, fields) => {
85
+ try {
86
+ logger.warn(event, fields);
87
+ }
88
+ catch {
89
+ }
90
+ };
83
91
  const revoke = (row, askIds, reason, nowMs) => {
84
92
  if (askIds.length === 0 || !emitRevoke)
85
93
  return;
@@ -87,39 +95,32 @@ export function createApprovalReconciler(deps) {
87
95
  emitRevoke(buildRevokeFrame(row.batchId, askIds, reason, nowMs, row.taskId), { owner: row.owner, sessionId: row.sessionId, taskId: row.taskId });
88
96
  }
89
97
  catch (err) {
90
- try {
91
- logger.warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
92
- }
93
- catch {
94
- }
98
+ warn("approval_revoke_emit_failed", { batchId: row.batchId, count: askIds.length, err: err instanceof Error ? err.message : String(err) });
95
99
  }
96
100
  };
97
- const reconcileOne = async (ask, nowMs) => {
101
+ const bindAtGate = async (ask, nowMs, deadlineMs) => {
102
+ const expired = () => deadlineMs !== undefined && Date.now() >= deadlineMs;
98
103
  const batchRow = await withDeadline(askStore.getBatch(ask.batchId), "getBatch");
99
104
  let batchState = batchRow?.state ?? "MISSING";
100
105
  let batchBoundAskId = batchRow?.boundAskId ?? null;
101
106
  const irreversibleBatch = batchState === "ABORTED" || (batchState === "ROUTING_BOUND" && batchBoundAskId !== null && batchBoundAskId !== ask.askId);
102
107
  let gateMatch;
103
- if (checkpoints && !irreversibleBatch) {
108
+ if (checkpoints && !irreversibleBatch && !expired()) {
104
109
  const candidates = await withDeadline(checkpoints.findCheckpointCandidatesForAsk(encodeCheckpointScope(ask.owner), ask.sessionId, ask.toolCallId, ask.createdAtMs), "findCheckpointCandidatesForAsk");
105
110
  gateMatch = classifyGateMatch(ask, candidates);
106
111
  if (gateMatch.kind === "hash_mismatch") {
107
- try {
108
- logger.warn("approval_reconcile_hash_mismatch", {
109
- askId: ask.askId,
110
- taskId: ask.taskId,
111
- toolCallId: ask.toolCallId,
112
- candidates: gateMatch.candidates,
113
- reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
114
- "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
115
- "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
116
- });
117
- }
118
- catch {
119
- }
112
+ warn("approval_reconcile_hash_mismatch", {
113
+ askId: ask.askId,
114
+ taskId: ask.taskId,
115
+ toolCallId: ask.toolCallId,
116
+ candidates: gateMatch.candidates,
117
+ reason: "ask and checkpoint minted the same bound input under the same (sourceTaskId, toolCallId) identity but the digests differ — " +
118
+ "the known causes are deployment-side (a hook/policy mutating an object it still holds a reference to between the two mints, " +
119
+ "or a stateful getter returning a fresh value per read), not out-of-band interference; this ask keeps its fail-safe route (no park, no denial)",
120
+ });
120
121
  }
121
122
  const match = gateMatch.kind === "match" ? gateMatch.candidate : undefined;
122
- if (match) {
123
+ if (match && !expired()) {
123
124
  const res = await withDeadline(askStore.bindBatch(ask.batchId, ask.askId, {
124
125
  gateToken: match.token,
125
126
  gateBoundCallId: match.boundCallId,
@@ -127,12 +128,20 @@ export function createApprovalReconciler(deps) {
127
128
  }), "bindBatch");
128
129
  if (res.ok) {
129
130
  revoke(ask, res.voidedSiblings, "superseded_by_park", nowMs);
130
- return { outcome: "parked", match: gateMatch };
131
+ return { bound: true, match: gateMatch };
131
132
  }
132
133
  batchState = res.batchState;
133
134
  batchBoundAskId = res.boundAskId ?? null;
134
135
  }
135
136
  }
137
+ return { bound: false, ...(gateMatch !== undefined ? { match: gateMatch } : {}), batchState, batchBoundAskId, irreversibleBatch };
138
+ };
139
+ const reconcileOne = async (ask, nowMs) => {
140
+ const attempt = await bindAtGate(ask, nowMs);
141
+ if (attempt.bound)
142
+ return { outcome: "parked", match: attempt.match };
143
+ const { batchState, batchBoundAskId, irreversibleBatch } = attempt;
144
+ const gateMatch = attempt.match;
136
145
  const run = runs && !irreversibleBatch ? ((await withDeadline(runs.getRun(ask.taskId), "getRun")) ?? null) : null;
137
146
  const action = decideReconcileAction({ ask, candidates: [], run, batchState, batchBoundAskId, nowMs, adhocGraceMs, orphanTtlMs, allowBind: false });
138
147
  switch (action.kind) {
@@ -149,7 +158,7 @@ export function createApprovalReconciler(deps) {
149
158
  if (!won)
150
159
  return { outcome: "held", match: gateMatch };
151
160
  if (action.reason === VOID_REASONS.ORPHAN_TTL_EXCEEDED) {
152
- logger.warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
161
+ warn("approval_ask_orphan_voided", { askId: ask.askId, taskId: ask.taskId, ageMs: nowMs - ask.createdAtMs, ttlMs: orphanTtlMs });
153
162
  }
154
163
  revoke(ask, [ask.askId], action.reason === VOID_REASONS.BATCH_BOUND_ELSEWHERE ? "superseded_by_park" : "aborted", nowMs);
155
164
  return { outcome: "voided", match: gateMatch };
@@ -160,6 +169,37 @@ export function createApprovalReconciler(deps) {
160
169
  }
161
170
  }
162
171
  };
172
+ const parkCommits = deps.parkCommits;
173
+ const budgetMs = deps.parkCommitBindBudgetMs ?? PARK_COMMIT_BIND_BUDGET_MS;
174
+ const bindCommittedPark = async (calls) => {
175
+ const deadlineMs = Date.now() + budgetMs;
176
+ const work = (async () => {
177
+ const rows = await askStore.listParkingBySession(calls.sessionId);
178
+ for (const ask of rows) {
179
+ if (!calls.toolCallIds.includes(ask.toolCallId))
180
+ continue;
181
+ if (Date.now() >= deadlineMs) {
182
+ warn("approval_park_commit_bind_failed", { askId: ask.askId, taskId: ask.taskId, err: `park-commit bind budget (${budgetMs}ms) spent before this row`, fallback: "periodic_sweep" });
183
+ return;
184
+ }
185
+ try {
186
+ if ((await bindAtGate(ask, Date.now(), deadlineMs)).bound)
187
+ metrics?.inc("approval_reconciled_total", { outcome: "parked" }, 1);
188
+ }
189
+ catch (err) {
190
+ warn("approval_park_commit_bind_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err), fallback: "periodic_sweep" });
191
+ }
192
+ }
193
+ })();
194
+ parkCommits?.track(work);
195
+ try {
196
+ await withDeadline(work, "bindCommittedPark", budgetMs);
197
+ }
198
+ catch (err) {
199
+ warn("approval_park_commit_bind_failed", { sessionId: calls.sessionId, err: err instanceof Error ? err.message : String(err), fallback: "periodic_sweep" });
200
+ }
201
+ };
202
+ parkCommits?.onParkCommitted(bindCommittedPark);
163
203
  return {
164
204
  async runOnce(nowMs) {
165
205
  const stats = {
@@ -184,11 +224,7 @@ export function createApprovalReconciler(deps) {
184
224
  for (const ask of parking) {
185
225
  if (Date.now() >= parkingDeadline) {
186
226
  stats.budgetExhausted += 1;
187
- try {
188
- logger.warn("approval_reconcile_budget_exhausted", { segment: "PARKING", processed: stats.parked + stats.denied + stats.voided + stats.held + stats.failed, scanned: stats.scanned, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
189
- }
190
- catch {
191
- }
227
+ warn("approval_reconcile_budget_exhausted", { segment: "PARKING", processed: stats.parked + stats.denied + stats.voided + stats.held + stats.failed, scanned: stats.scanned, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
192
228
  break;
193
229
  }
194
230
  if (ask.boundInputHash === null)
@@ -224,11 +260,7 @@ export function createApprovalReconciler(deps) {
224
260
  }
225
261
  catch (err) {
226
262
  stats.failed += 1;
227
- try {
228
- logger.warn("approval_reconcile_row_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err) });
229
- }
230
- catch {
231
- }
263
+ warn("approval_reconcile_row_failed", { askId: ask.askId, taskId: ask.taskId, err: err instanceof Error ? err.message : String(err) });
232
264
  }
233
265
  }
234
266
  const pending = await withDeadline(askStore.listByState("STREAM_PENDING", batchLimit), "listByState(STREAM_PENDING)");
@@ -236,11 +268,7 @@ export function createApprovalReconciler(deps) {
236
268
  for (const row of pending) {
237
269
  if (Date.now() >= orphanDeadline) {
238
270
  stats.budgetExhausted += 1;
239
- try {
240
- logger.warn("approval_reconcile_budget_exhausted", { segment: "STREAM_PENDING", expired: stats.orphansExpired, scanned: pending.length, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
241
- }
242
- catch {
243
- }
271
+ warn("approval_reconcile_budget_exhausted", { segment: "STREAM_PENDING", expired: stats.orphansExpired, scanned: pending.length, budgetMs: RECONCILE_SEGMENT_BUDGET_MS });
244
272
  break;
245
273
  }
246
274
  if (row.expiresAtMs + pendingGraceMs >= nowMs)
@@ -254,23 +282,15 @@ export function createApprovalReconciler(deps) {
254
282
  }
255
283
  catch (err) {
256
284
  stats.failed += 1;
257
- try {
258
- logger.warn("approval_orphan_expire_failed", { askId: row.askId, taskId: row.taskId, policy: unattendedPolicy, err: err instanceof Error ? err.message : String(err) });
259
- }
260
- catch {
261
- }
285
+ warn("approval_orphan_expire_failed", { askId: row.askId, taskId: row.taskId, policy: unattendedPolicy, err: err instanceof Error ? err.message : String(err) });
262
286
  }
263
287
  }
264
288
  if (stats.unmatchableNoHash > 0) {
265
- try {
266
- logger.warn("approval_reconcile_rows_unmatchable", {
267
- rows: stats.unmatchableNoHash,
268
- scanned: stats.scanned,
269
- reason: "bound_input_hash is NULL — criterion 1 (identity ∧ hash) cannot match; rows fall through to the run-terminal / adhoc / orphan arms",
270
- });
271
- }
272
- catch {
273
- }
289
+ warn("approval_reconcile_rows_unmatchable", {
290
+ rows: stats.unmatchableNoHash,
291
+ scanned: stats.scanned,
292
+ reason: "bound_input_hash is NULL — criterion 1 (identity ∧ hash) cannot match; rows fall through to the run-terminal / adhoc / orphan arms",
293
+ });
274
294
  }
275
295
  if (stats.parked + stats.denied + stats.voided + stats.orphansExpired > 0) {
276
296
  metrics?.inc("approval_reconciled_total", { outcome: "parked" }, stats.parked);
@@ -15,7 +15,8 @@
15
15
  *
16
16
  * 本模块按**server 侧可知的同三项**算一份「若此刻提交会不会武装」的裁决,供壳(cli `sema doctor permissions`)
17
17
  * 与人自查。三项分别对应三个未武装原因(判序 = 谓词的合取序,先判到的先报):
18
- * · `mode_not_auto` —— 本次意图不是 auto(`?permissionMode=` 查询串给的;缺席 = 按 auto 意图答);
18
+ * · `mode_not_auto` —— 本次意图不是 auto(`?permissionMode=` 查询串给的;缺席 = 按 auto 意图答),或意图经本部署的运维入口地板
19
+ * (`AUTONOMY=plan`)钳过之后不是 auto —— 与受理门同一只钳,提交时那个词到不了引擎;
19
20
  * · `deployment_incapable` —— 装配面没挂 `RunnerDeps.autoMode` 分类器席位(生产装配恒挂;桩/一次性 CLI 不挂);
20
21
  * · `org_denied` —— per-principal caps 的 `autoMode === false` 且本地 deny 未设(center 显式 deny、center
21
22
  * 硬失败的 fail-closed 拒项);
@@ -80,7 +81,7 @@ export declare const AUTO_MODE_UNARMED_REASONS: readonly ["mode_not_auto", "depl
80
81
  export type AutoModeUnarmedReason = (typeof AUTO_MODE_UNARMED_REASONS)[number];
81
82
  /** 锁步表:词表与语义一一对应(加员/删员任一侧不同步 = tsc 红)。语义只供文档/测试,不上 wire。 */
82
83
  export declare const AUTO_MODE_UNARMED_REASON_SEMANTICS: {
83
- readonly mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\")";
84
+ readonly mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\"),或意图经本部署的运维入口地板(AUTONOMY=plan)钳过之后不是 auto —— 与受理门同一只钳,提交时那个词到不了引擎";
84
85
  readonly deployment_incapable: "装配面没挂 RunnerDeps.autoMode 分类器席位(本部署/本进程形不具备分类器)";
85
86
  readonly org_denied: "per-principal caps.autoMode === false 且本地 deny 未设:center 显式 deny / center 硬失败 fail-closed 拒项";
86
87
  readonly local_denied: "本地 catalog 键 PERMISSIONS_DISABLE_AUTO_MODE=true(center/settings 键名 permissions.disableAutoMode)把 caps.autoMode 折成 false";
@@ -118,11 +119,17 @@ export type AutoModeVerdict = {
118
119
  * 三项合取的 server 侧裁决。`requestedMode` 缺席 = 按 auto 意图答(「若请求会不会武装」);`principal` =
119
120
  * 调用方经门验证的身份(匿名 ⇒ undefined ⇒ center 客户端答 undefined,本地 deny 层照样罩)。
120
121
  *
122
+ * 意图那一项判的是**提交之后到得了引擎的那个词**:本次意图先过本部署的运维入口地板(`floor`,调用方传 `runtime-governance.ts`
123
+ * `autonomyModeFloor(<活 config 的 AUTONOMY>)` —— 与受理门盖章读同一只属主),钳法 = core `clampPermissionMode`(受理门 `stampEntryPermissionMode`
124
+ * 用的同一只钳;地板缺席 ⇒ 原样)。`AUTONOMY=plan` 部署上任何意图都被钳成 `plan` ⇒ `mode_not_auto`(闭集词复用,不另铸)。
125
+ *
121
126
  * 🔴 resolver 抛错**不吞**:合成腿的每一层都自带 fail-closed/缺席臂(center 客户端 catch-all、grant 腿 onFault),
122
127
  * 走到这里还在抛 = 装配缺陷,读面 500 比一句编出来的 reason 诚实。
123
128
  */
124
129
  export declare function judgeAutoModeArming(face: AutoModeFaceDeps | undefined, input: {
125
130
  readonly requestedMode: PermissionMode | undefined;
131
+ /** 本部署此刻的运维入口地板(`autonomyModeFloor(config.autonomy)`;`undefined` = 没有地板)。必填:不传地板不是「没有地板」的合法拼法。 */
132
+ readonly floor: PermissionMode | undefined;
126
133
  readonly principal: string | undefined;
127
134
  /** 本地 `PERMISSIONS_DISABLE_AUTO_MODE`(= catalog/settings 键 `permissions.disableAutoMode`)是否置 true —— caps false 时分词
128
135
  * `local_denied` / `org_denied`(seam ⑤;两者对壳的指路不同:改本机 env vs 找组织)。 */
@@ -1,4 +1,4 @@
1
- import { classifierSeatModel } from "@sema-agent/core";
1
+ import { clampPermissionMode, classifierSeatModel } from "@sema-agent/core";
2
2
  import { resolveMainModel } from "./model-select.js";
3
3
  export function classifierRouteFor(deps) {
4
4
  try {
@@ -11,7 +11,7 @@ export function classifierRouteFor(deps) {
11
11
  }
12
12
  export const AUTO_MODE_UNARMED_REASONS = ["mode_not_auto", "deployment_incapable", "org_denied", "local_denied", "settings_denied", "resolver_fault"];
13
13
  export const AUTO_MODE_UNARMED_REASON_SEMANTICS = {
14
- mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\")",
14
+ mode_not_auto: "本次意图不是 auto(壳发的 permissionMode 不是 \"auto\"),或意图经本部署的运维入口地板(AUTONOMY=plan)钳过之后不是 auto —— 与受理门同一只钳,提交时那个词到不了引擎",
15
15
  deployment_incapable: "装配面没挂 RunnerDeps.autoMode 分类器席位(本部署/本进程形不具备分类器)",
16
16
  org_denied: "per-principal caps.autoMode === false 且本地 deny 未设:center 显式 deny / center 硬失败 fail-closed 拒项",
17
17
  local_denied: "本地 catalog 键 PERMISSIONS_DISABLE_AUTO_MODE=true(center/settings 键名 permissions.disableAutoMode)把 caps.autoMode 折成 false",
@@ -21,7 +21,9 @@ export const AUTO_MODE_UNARMED_REASON_SEMANTICS = {
21
21
  export async function judgeAutoModeArming(face, input) {
22
22
  const route = face?.seatMounted ? face.classifierModel?.() : undefined;
23
23
  const model = route !== undefined ? { model: route } : {};
24
- if (input.requestedMode !== undefined && input.requestedMode !== "auto")
24
+ const intent = input.requestedMode ?? "auto";
25
+ const entry = input.floor === undefined ? intent : clampPermissionMode(intent, input.floor).effective;
26
+ if (entry !== "auto")
25
27
  return { armed: false, reason: "mode_not_auto", ...model };
26
28
  if (face === undefined || !face.seatMounted)
27
29
  return { armed: false, reason: "deployment_incapable" };
@@ -370,6 +370,7 @@ async function runLeaderForTrap(rt, brain, models, roles, pricing, trap, cellId,
370
370
  process.env.LEADER_WORKER_BUDGET_USD = String((rt.maxCostUsd ?? 1.0) / 2);
371
371
  const run = createLeaderRunner({
372
372
  brain, models, roles, pricing, e2bApiKey: rt.e2bApiKey, workspace: W,
373
+ autonomy: () => undefined,
373
374
  ...(s1DebugGit() ? { logger: { warn: (m, x) => console.error(`[S1_DEBUG_LEADER warn] ${m}`, x ? JSON.stringify(x).slice(0, 300) : ""), info: (m, x) => console.error(`[S1_DEBUG_LEADER info] ${m}`, x ? JSON.stringify(x).slice(0, 200) : "") } } : {}),
374
375
  });
375
376
  const teamSeedWrite = (trap.seedFiles ?? [])
@@ -51,6 +51,7 @@ export function createLeaderFace(ctx) {
51
51
  retentionPolicy: governanceSeams.retentionPolicy,
52
52
  },
53
53
  deploymentPosture: () => buildDeploymentPostureSeats(config),
54
+ autonomy: () => config.autonomy,
54
55
  ...(ctx.tracer ? { tracer: ctx.tracer } : {}),
55
56
  ...(config.usageWindows && ctx.usageWindowStore
56
57
  ? { usageWindows: config.usageWindows, usageWindowStore: ctx.usageWindowStore }