@sema-agent/server 7.33.0 → 7.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +31 -5
- package/dist/boot/coordinators.js +7 -0
- package/dist/boot/runner-deps.js +5 -0
- package/dist/config-types.d.ts +42 -2
- package/dist/config.js +25 -1
- package/dist/http/routes/tasks.js +9 -2
- package/dist/tool-approval.d.ts +95 -5
- package/dist/tool-approval.js +262 -50
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -307,16 +307,19 @@ QUESTION_TTL_MS=300000
|
|
|
307
307
|
> [`docs/ASSISTANT-WIRE-CONTRACT.md` §4a-quint](docs/ASSISTANT-WIRE-CONTRACT.md)(错误码 `feature.approval_ask_disabled` 在附录 A)。
|
|
308
308
|
|
|
309
309
|
```bash
|
|
310
|
-
# 协议总开关。默认 true;显式 =false
|
|
311
|
-
#
|
|
310
|
+
# 协议总开关。默认 true;显式 =false 是**本协议面**的干净还原键(不发 approval_request、不落 ask 行、
|
|
311
|
+
# 不起收敛器腿,窗回到 DEFAULT_APPROVAL_TTL_MS)。⚠️ 它**不**回滚 7.34.0 的 R-13 窗到期语义——那一条的
|
|
312
|
+
# 回滚键是下面的 UNATTENDED_APPROVAL_POLICY=deny(两根旋钮正交,别当一根用)
|
|
312
313
|
STREAM_APPROVAL_ENABLED=true
|
|
313
|
-
# 活卡窗(毫秒,默认 300000=5min)。0 = 运维显式关窗 ⇒ 恒 park(不是"还原"
|
|
314
|
+
# 活卡窗(毫秒,默认 300000=5min)。0 = 运维显式关窗 ⇒ 恒 park(不是"还原",还原用上面那个键;
|
|
315
|
+
# 配了 UNATTENDED_APPROVAL_POLICY=deny 的部署这里是"恒当场拒",见下面那段)
|
|
314
316
|
STREAM_ASK_WINDOW_MS=300000
|
|
315
317
|
# 协调器窗长三元的安全余量(毫秒,默认 10000):有效窗 = min(ttl, 本 leg 余量 − 本值)
|
|
316
318
|
STREAM_ASK_WINDOW_MARGIN_MS=10000
|
|
317
319
|
# 开流重放:一次最多投几张未决卡(默认 50)。超出只投最新的并记一次 warn,开流不失败
|
|
318
320
|
STREAM_APPROVAL_REPLAY_MAX=50
|
|
319
|
-
# 写侧准入帽:单个 task / 单个 owner 的未决 ask 上限(默认 32 / 256)。超限走 park
|
|
321
|
+
# 写侧准入帽:单个 task / 单个 owner 的未决 ask 上限(默认 32 / 256)。超限走 park(**缺省 park 政策下
|
|
322
|
+
# 永不 deny**;配了 UNATTENDED_APPROVAL_POLICY=deny 的部署按它的自声明当场拒——见下面那段)
|
|
320
323
|
STREAM_APPROVAL_ADMIT_MAX_PER_TASK=32
|
|
321
324
|
STREAM_APPROVAL_ADMIT_MAX_PER_OWNER=256
|
|
322
325
|
# 对账收敛器每 tick 每段最多处理的行数(默认 200,有界 [1,10000],且必须是**整数**)
|
|
@@ -346,12 +349,35 @@ STREAM_APPROVAL_ORPHAN_TTL_MS=604800000
|
|
|
346
349
|
- **无 durable 前置的部署(五合取不满足:local backend / 无 checkpoint 能力)**:流内协议整个不上场
|
|
347
350
|
(启动一行 `stream_approval_disabled` 点名缺哪项),ask 走旧活卡腿——断连场景的最坏结局是活卡 TTL
|
|
348
351
|
窗满**自动 deny(fail-closed)后 run 继续**,不会死锁在等一张没人能回的卡上;代价是断连期间用户的
|
|
349
|
-
|
|
352
|
+
批准机会直接过期。(7.34.0 起这条 deny 由**引擎**在「没有可停靠的 durable 门」时给出——服务侧只报
|
|
353
|
+
「此刻无人可答」,见上面的 `UNATTENDED_APPROVAL_POLICY`;结局不变,拒绝理由的文案更准确。)要「断连也不丢决策」的语义,配齐 durable 前置(持久 store + `DURABLE_APPROVAL=true`)。
|
|
350
354
|
默认值(300s + 60s vs 7d)自然满足,只有显式改坏才会撞上。
|
|
351
355
|
- **调参方向**:想让人有更长时间点审批卡 → 调大 `STREAM_ASK_WINDOW_MS`;卡太多刷屏 → 调小两个 `ADMIT_MAX_*`
|
|
352
356
|
(代价是超限的 ask 走 park,要有人去审批队列捞);库压大 → 调小 `STREAM_APPROVAL_RECONCILE_BATCH`
|
|
353
357
|
(代价是收敛变慢,靠队列轮转保证下轮接着扫)。
|
|
354
358
|
|
|
359
|
+
**无人值守政策(`UNATTENDED_APPROVAL_POLICY`,7.34.0 起 / #280 R-13)**
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# 闭集 park|deny,缺省 park。拼错词**启动期响亮拒**(不静默回缺省)
|
|
363
|
+
UNATTENDED_APPROVAL_POLICY=park
|
|
364
|
+
```
|
|
365
|
+
- 管的是**同一个问题**:一只需要人批的 ask 到了,而此刻**没有任何人**能答(窗走完没人点、连接全断、
|
|
366
|
+
运维把窗关成 0、bg/headless 腿本来就没有活流、未决卡撞上准入帽)。
|
|
367
|
+
- `park`(缺省)⇒ 服务报「此刻无人可答」,**durable 部署**(`DURABLE_APPROVAL=true` + checkpoint 能力的
|
|
368
|
+
backend)把 run **挂起候人**(`done(suspended)` + `pendingGate`),人回来经审批面补批就接着跑;
|
|
369
|
+
没有 park 设施的部署由引擎 fail-closed 拒绝并告诉模型「没有可停靠的 durable 审批门」。
|
|
370
|
+
- `deny` ⇒ **真无人值守/headless 部署的显式声明**:这类 ask 当场拒绝,让模型自己改道,不积压一堆
|
|
371
|
+
等不到人的挂起 run。它是**收紧**方向(不放行任何东西)。
|
|
372
|
+
- **7.34.0 的行为变化(park 侧)**:此前「窗走完没人答」是**当场拒绝 + 一条 error 回模型**(模型往往就
|
|
373
|
+
绕开了那次治理);现在它与其余「无人可答」的情形一样走 park。要恢复旧结局请显式配 `deny`。
|
|
374
|
+
- 纯部署级:**不看**客户端的任何表态/权限模式。协调器整体关掉(`TOOL_APPROVAL_ENABLED` 关)时这个旋钮
|
|
375
|
+
没有施加对象;`APPROVAL_REQUIRE` 名单里的工具走的是 durable 审批门,不受它影响。
|
|
376
|
+
- 🔴 **与 `STREAM_APPROVAL_ENABLED` 正交**:把流内协议开关关掉**不会**回滚 R-13 —— 它只是不发
|
|
377
|
+
`approval_request`、不落 ask 行、不起收敛器腿,活卡窗到期照样走 park(park 的承载是引擎的 checkpoint,
|
|
378
|
+
不是 server 的 ask 行,「有 durable 设施但没开协议」正是 R-13 要救的那类部署)。要回到旧的「窗满即拒」
|
|
379
|
+
就配 `UNATTENDED_APPROVAL_POLICY=deny`。
|
|
380
|
+
|
|
355
381
|
**可选 — READ 面姿态(readFace,7.18.0 起;缺席=引擎当家)**
|
|
356
382
|
|
|
357
383
|
```bash
|
|
@@ -74,6 +74,13 @@ export function createLiveCoordinators(ctx) {
|
|
|
74
74
|
admitMaxPerOwner: config.streamApproval.admitMaxPerOwner,
|
|
75
75
|
// #154 车二:同意车道(缺席 = 规则店没装配 ⇒ 帧上零候选、回决的 persistRule 如实拒)。
|
|
76
76
|
...(ruleConsent !== undefined ? { ruleConsent } : {}),
|
|
77
|
+
// #280 R-13 C:无人值守政策(闭集 park|deny)。**恒传**(不挂任何在场性推断):它是纯部署级的一位,
|
|
78
|
+
// 语义/三问/射程边界一处成文见 `config-types.ts` 的 `unattendedApprovalPolicy` 域注。
|
|
79
|
+
// ⚠️ 恒传是**刻意**的,不是顺手(codex R2-F2 点过这一行):`streamApprovalGate.active` 为假时只
|
|
80
|
+
// 省略 askStore/窗,协调器照常在场,R-13 的 D1 窗到期臂照样按政策走 —— 那正是设计稿点名的主受益
|
|
81
|
+
// 部署(park 的承载是 core checkpoint,不是 ask 行)。把政策挂到协议开关上会把它整个切掉。
|
|
82
|
+
// 协调器本身缺席时(`TOOL_APPROVAL_ENABLED` 关)本旋钮无处施加 —— 成文事实,不是推断腿。
|
|
83
|
+
unattendedPolicy: config.unattendedApprovalPolicy,
|
|
77
84
|
})
|
|
78
85
|
: undefined;
|
|
79
86
|
// SendUserFile(真 CC 契约,clay 2026-07-14)两种 lane 形态,其余缺席=诚实(工具不进 roster):
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -63,6 +63,11 @@ export function createEngineNoticeSeat(logger) {
|
|
|
63
63
|
const warn = logger?.warn?.bind(logger);
|
|
64
64
|
return warn ? { onNotice: createEngineNoticeForwarder({ warn }) } : {};
|
|
65
65
|
}
|
|
66
|
+
// #289(core 5.43.0 #316 观测席,[4448] 表态② 的评估结论,如实登记):`onSecretEnvScrub` 席在 server
|
|
67
|
+
// 侧**无真挂点**——host lane 的执行 env(真跑用户 Bash 的那只)由 core 引擎自建,server 构造的三只
|
|
68
|
+
// NodeExecutionEnv(resolve-spec 裁决 env / parked-revive-gate 裁决 env / run-local 治理探测 env)都不
|
|
69
|
+
// 执行用户 Bash,挂席=假遥测面。终态=core 的 once-per-process console 摘要腿(5.43 起自带,响亮权在
|
|
70
|
+
// core);若未来 core 给 RunnerDeps 开执行 env 构造参透传席,再按表态制接(上游件,不在此仓旁路)。
|
|
66
71
|
export function createSharedRunnerDeps(ctx) {
|
|
67
72
|
// 具名带类型的字面量(非裸 return)——deps-literal-shape-gate 的 POINTS 按 `const X: T = {` 咬装配
|
|
68
73
|
// 字面量,裸 return 形在它的覆盖外(复审 2026-07-30 F5):conditional-spread 病在这里就会失检。
|
package/dist/config-types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { A2aServerSpec, CompliancePosture, LockedKey, McpServerSpec, Model,
|
|
|
10
10
|
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
11
11
|
import type { ElicitationThrottle } from "./elicitation.js";
|
|
12
12
|
import type { QuestionThrottle } from "./question.js";
|
|
13
|
+
import type { UnattendedApprovalPolicy } from "./tool-approval.js";
|
|
13
14
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
14
15
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
15
16
|
import type { GitApiKind } from "./git-api-kind.js";
|
|
@@ -126,7 +127,8 @@ export interface StreamApprovalConfig {
|
|
|
126
127
|
* `STREAM_APPROVAL_REPLAY_MAX`,默认 **50**。 */
|
|
127
128
|
replayMax: number;
|
|
128
129
|
/** 写侧准入门(§0 X-2)的 per-task 未决 ask 上限。超限 ⇒ 在落 pending/建 timer/发帧**之前**返回
|
|
129
|
-
* `"unavailable"`(park
|
|
130
|
+
* `"unavailable"`(park 路由;**缺省 park 政策下永不 deny**——过载 bypass 到 park 是 §3.3 的硬条款。
|
|
131
|
+
* #280 R-13 C:`UNATTENDED_APPROVAL_POLICY=deny` 的部署按自声明当场拒,理由见该键域注)。
|
|
130
132
|
* `STREAM_APPROVAL_ADMIT_MAX_PER_TASK`,默认 **32**。消费点在刀 3b。 */
|
|
131
133
|
admitMaxPerTask: number;
|
|
132
134
|
/** 同上,per-owner(租户)维。`STREAM_APPROVAL_ADMIT_MAX_PER_OWNER`,默认 **256**。 */
|
|
@@ -1157,6 +1159,44 @@ export interface ServiceConfigFlat {
|
|
|
1157
1159
|
* `STREAM_APPROVAL_RECONCILE_BATCH` / `STREAM_APPROVAL_PENDING_GRACE_MS` /
|
|
1158
1160
|
* `STREAM_APPROVAL_ADHOC_GRACE_MS` / `STREAM_APPROVAL_ORPHAN_TTL_MS`(后四键 = 车5 收敛器)。 */
|
|
1159
1161
|
streamApproval: StreamApprovalConfig;
|
|
1162
|
+
/**
|
|
1163
|
+
* #280 R-13 C(`UNATTENDED_APPROVAL_POLICY`;裁定 [4297]Q1「裁2:A+C」/[4338] 裁②)——一只治理 ask
|
|
1164
|
+
* 到了而**没有任何人**能答时,这条部署要什么终局。闭集 `park`(缺省)/ `deny`,**坏值 boot 拒启**
|
|
1165
|
+
* (#210 A 档:一个拼错的词若被当成「未设」静默回缺省,运维会以为自己关掉了 park 积压而实际没有)。
|
|
1166
|
+
*
|
|
1167
|
+
* **谁需要**:auto / headless / 无人值守部署(夜间批处理、CI 里的 agent、没有任何人盯屏的 worker)。
|
|
1168
|
+
* 它们的 park 是死信:run 挂起候一个永远不会来的人,占着 checkpoint 与配额直到 TTL。
|
|
1169
|
+
* **谁被伤**:配了 `deny` 却其实有人的部署 —— 那些本可以补批的操作变成当场拒绝。补偿 = 这是**显式**
|
|
1170
|
+
* 一句声明,缺省永远是 `park`;改回来只需删掉这一行 env。
|
|
1171
|
+
* **不看任何客户端表态**(纯部署级;memory: operator-knob-must-be-unconditional —— #153 shellGate 让
|
|
1172
|
+
* 一个部署级旋钮的生死由客户端 posture 决定,同病犯过两次)。
|
|
1173
|
+
*
|
|
1174
|
+
* 施加点 = `ToolApprovalCoordinator` 的构造参 `unattendedPolicy` **一处**,五条「无人可答」的臂读同一
|
|
1175
|
+
* 个值(设计稿 §1 的表:(a) 有店窗到期赢 CAS /(b) 无店窗到期 /(c) 店报错 catch /(d) windowZero 与
|
|
1176
|
+
* headless 腿 /(e) 断连强转 + emit 全灭 + 写侧准入超限)。`"timeout"` 宿主自报只留在 (b)(c) ——
|
|
1177
|
+
* 结束等待的确实是本仓的窗;其余三臂结束等待的是容量/连接/装配,自报恒缺席。
|
|
1178
|
+
*
|
|
1179
|
+
* 🔴 **射程边界(成文,不是遗漏)**:
|
|
1180
|
+
* · 唯一让本旋钮**无处施加**的键是 `TOOL_APPROVAL_ENABLED=false`(协调器整体不构造,core 走它自己的
|
|
1181
|
+
* headless 姿态)。**不造推断腿**:旋钮只在协调器在场时有意义,这一句是成文而不是代码分支。
|
|
1182
|
+
* · ⚠️ **与 `STREAM_APPROVAL_ENABLED` 正交**(codex 对抗复审 R2-F2 验真后纠正 —— 本注上一版把两者
|
|
1183
|
+
* 写成了一回事,是错的):协议开关关掉只是不注入 `askStore`/协议窗,协调器照常在场,于是 R-13 的
|
|
1184
|
+
* D1 窗到期臂**照样**走 park 路由。这不是漏网:设计稿 §2 明写 park 的承载是 **core checkpoint**
|
|
1185
|
+
* 而不是 server 的 ask 行,「无 askStore 但有 durable 设施」正是 R-13 点名的**主受益部署**。
|
|
1186
|
+
* 要回滚 R-13 的行为面,键是 `UNATTENDED_APPROVAL_POLICY=deny`,**不是**协议开关。
|
|
1187
|
+
* 射程钉:`test/unattended-approval-policy.test.ts` 的 codex R2-F2 两格。
|
|
1188
|
+
* · APPROVAL_REQUIRE 名单工具走 `createApprovalBaselinePolicy` 的 durable gate 类,**不经流内臂**,
|
|
1189
|
+
* 结构上不受本旋钮影响(裁定「非名单工具」的限定因此无需码内名单分支)。
|
|
1190
|
+
* · **持久回放**(窗到期后向真源收敛、`ensureAsk` 幂等重入撞上已终结的行)读出 PARKING/PARKED 时
|
|
1191
|
+
* **同样按旋钮改判** —— 它们与 (a) 是同一件事(赢家可能是别的副本或上一次调用),不统一的话同一只
|
|
1192
|
+
* ask 的终局会取决于「谁赢了那把 CAS / 重试没重试」。人的真决议回放不受影响。
|
|
1193
|
+
* · 唯一**不受**改判的是 `ensureAsk` 身份未定 / 坏行两臂(「身份不确定」的处置,不是「没有人」的
|
|
1194
|
+
* 处置),于是 `deny` 部署上仍可能落极少数 park(店抖动期),已知且有意的残余。
|
|
1195
|
+
* · `deny` 部署上 ask **行**仍会经 PARKING 记账(旋钮改的是交给引擎的终局,不是店语义)。收敛器只把
|
|
1196
|
+
* PARKING 行绑到**已经存在的** checkpoint 上,而 deny 部署里引擎从不 park ⇒ 绑不上,行按 run 终局
|
|
1197
|
+
* 自行收敛成 DENIED/VOID,不会凭空生出 park;残余 = 那类行的归因记成 `routing_failure`。
|
|
1198
|
+
*/
|
|
1199
|
+
unattendedApprovalPolicy: UnattendedApprovalPolicy;
|
|
1160
1200
|
/** [824]① (clay A 案 = workflow 权限全面 CC parity): workflow 子 agent 的默认权限基线与主 LLM 同权(base `{}`)。
|
|
1161
1201
|
* 安全论证:主 LLM 与子 agent 同 root 同信任域,主 LLM 本就能写这棵树([816] ask 门照管),单独钳子 agent 的
|
|
1162
1202
|
* 安全增益≈0(只防绕路不防直路的门不是边界);实测产品代价=[814]A 死锁。`WORKFLOW_AGENTS_READONLY=true` =
|
|
@@ -1296,7 +1336,7 @@ export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "ses
|
|
|
1296
1336
|
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
|
|
1297
1337
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
1298
1338
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
1299
|
-
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
1339
|
+
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
1300
1340
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
1301
1341
|
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
1302
1342
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
package/dist/config.js
CHANGED
|
@@ -1194,6 +1194,27 @@ function streamApprovalConfig() {
|
|
|
1194
1194
|
}
|
|
1195
1195
|
return cfg;
|
|
1196
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* #280 R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的闭集解析(#210 A 档:坏值**拒启**)。
|
|
1199
|
+
*
|
|
1200
|
+
* 词表 = `park`(缺省)| `deny`,**逐字精确匹配**(小写、两侧空白裁掉):`PARK` / `Deny` / `true` / `off`
|
|
1201
|
+
* 一律落到拒启臂 —— 与 `MANUAL_MODE_SHELL_GATE` 同姿势,理由逐字同(「看起来被接受了」= 它其实什么都没做,
|
|
1202
|
+
* 而这条轴决定的是权限门在无人值守时的终局方向,静默失效是运维面最贵的那一类)。
|
|
1203
|
+
* ⚠️ 与 `MANUAL_MODE_SHELL_GATE` 的一处**刻意不同**:本键**没有** `off` 词。它的两个值都是真姿态
|
|
1204
|
+
* (park / deny),不存在「这个旋钮不参与」的第三态;空串/未设 = 缺省 `park`,那是文档化的出厂姿态,
|
|
1205
|
+
* 不是「关掉」。语义与射程成文见 `config-types.ts` 的域注。
|
|
1206
|
+
*/
|
|
1207
|
+
function unattendedApprovalPolicyEnv() {
|
|
1208
|
+
const raw = (process.env.UNATTENDED_APPROVAL_POLICY ?? "").trim();
|
|
1209
|
+
if (raw === "")
|
|
1210
|
+
return "park";
|
|
1211
|
+
if (raw === "park" || raw === "deny")
|
|
1212
|
+
return raw;
|
|
1213
|
+
throw new Error(`env UNATTENDED_APPROVAL_POLICY="${raw.slice(0, 120)}" is not a recognized value — use "park" (default: an ask nobody can answer becomes a durable park, the run suspends and waits for a human) ` +
|
|
1214
|
+
`or "deny" (an unattended/headless deployment declaring that NOBODY will ever answer: such asks are denied on the spot instead of piling up parked runs). ` +
|
|
1215
|
+
`The words are matched EXACTLY (lowercase, surrounding whitespace trimmed) and there is no "off" — both values are real postures, and unset simply means the documented default "park". ` +
|
|
1216
|
+
`Refusing to boot rather than treating a misspelled value as "unset": that runs the deployment on a policy the operator did not choose, with no trace anywhere`);
|
|
1217
|
+
}
|
|
1197
1218
|
function parseApprovalDomain(ctx) {
|
|
1198
1219
|
const { postureOn } = ctx; // 跨域入参②:posture 三态(single-user turnkey ⇒ HITL 面默认 ON)
|
|
1199
1220
|
// design/80 D-G: the direct-connect approval door anchors (all must be present to ACTIVATE; fail-closed).
|
|
@@ -1309,6 +1330,9 @@ function parseApprovalDomain(ctx) {
|
|
|
1309
1330
|
// 的上一版正是这么漂的(默认翻 ON 之后它还写着「总开关默认 false」整整一版)。
|
|
1310
1331
|
// 三问记录(翻转前那一版)见 config-types.ts 的 StreamApprovalConfig。
|
|
1311
1332
|
streamApproval: streamApprovalConfig(),
|
|
1333
|
+
// #280 R-13 C:无人值守政策(闭集 park|deny,缺省 park)。语义/三问/射程边界**一处成文** =
|
|
1334
|
+
// `config-types.ts` 的 `unattendedApprovalPolicy` 域注,这里只解析,不复述(复述的两份必然各自漂)。
|
|
1335
|
+
unattendedApprovalPolicy: unattendedApprovalPolicyEnv(),
|
|
1312
1336
|
// [875]a 成文:0(缺省)= durable HITL 无限期等人,时间型 reapSuspended 不跑;file/memory lane 的回收
|
|
1313
1337
|
// 探针只在 DURABLE_APPROVAL=true 时注入(无 durable 的部署两只 suspended 回收器恒 NO-OP,parked 行的
|
|
1314
1338
|
// 恢复把手 = POST /v1/runs/:id/cancel,[868]①)。checkpoint 过期驱动的那只不受本旋钮门控。
|
|
@@ -2314,7 +2338,7 @@ const MODEL_PLANE_GROUP_KEYS = [
|
|
|
2314
2338
|
const APPROVAL_GROUP_KEYS = [
|
|
2315
2339
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
2316
2340
|
"approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
|
|
2317
|
-
"resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "mcpElicitation",
|
|
2341
|
+
"resourceSuspendTtlSec", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
|
|
2318
2342
|
"sensitiveWritePatterns", "manualModeShellGate",
|
|
2319
2343
|
];
|
|
2320
2344
|
const MEMORY_GROUP_KEYS = [
|
|
@@ -1070,6 +1070,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
: undefined;
|
|
1072
1072
|
if (deps.toolApproval) {
|
|
1073
|
+
// 🔴 具名一次再进闭包(自审:`deps.toolApproval!` 的非空断言在这里是**可避免**的 —— 断言只
|
|
1074
|
+
// 在本 if 成立时正确,而闭包是稍后才跑的;拿捕获到的常量才是真的把那条不变量携进去)。
|
|
1075
|
+
const toolApproval = deps.toolApproval;
|
|
1073
1076
|
// [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
|
|
1074
1077
|
// runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
|
|
1075
1078
|
// #151 车3 刀 3b(§8.3 ⑦):`windowZero` ⇒ 换成**立即** `"unavailable"` 的闭包。为什么不是
|
|
@@ -1077,9 +1080,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1077
1080
|
// per-task 的「缺席」结构上不可达;而 core 的 `approverUnavailable === true` 分支
|
|
1078
1081
|
// (prepare-task.js:3328-3335)**直接绕过** onAsk 在场判定去铸 checkpoint —— 与「onAsk 缺席」
|
|
1079
1082
|
// 在 park 结局上逐字等价,且可 per-task 表达。
|
|
1083
|
+
// #280 R-13 C 臂(d):这条短路的取值走协调器公开的**同一个**政策口
|
|
1084
|
+
// (`unattendedAskOutcome()`)—— `UNATTENDED_APPROVAL_POLICY=deny` 的部署上它是裸 `false`。
|
|
1085
|
+
// 🔴 不在这里读 `deps.config` 自己算一遍:五臂一处施加是本件的硬条款,两处各算必然漂
|
|
1086
|
+
// (协调器与装配层对同一只 ask 给出不同终局)。
|
|
1080
1087
|
prepared.spec.onAsk = windowZero
|
|
1081
|
-
? () => Promise.resolve(
|
|
1082
|
-
:
|
|
1088
|
+
? () => Promise.resolve(toolApproval.unattendedAskOutcome())
|
|
1089
|
+
: toolApproval.boundAsk({
|
|
1083
1090
|
owner: askOwner,
|
|
1084
1091
|
taskId: askTaskId,
|
|
1085
1092
|
...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -68,6 +68,15 @@ export interface ToolApprovalFrame {
|
|
|
68
68
|
* 本键让 live-only 部署(店缺席、帧无卡)也能读到出身位;安全类标记不得依赖店在场([4392] 结构性
|
|
69
69
|
* 缺口成文)。消费语义(cli 挂点已预埋):在场 ⇒ 一切自动放行让位(含记住的规则与 bypass 姿态)。 */
|
|
70
70
|
requiresRealApproval?: true;
|
|
71
|
+
/** #288([4429]② cli 请托;**ADDITIVE**,`"tool_approval"` only,7.34.0+)——窗三键,与
|
|
72
|
+
* `approval_request` 帧(design/172 呈卡帧)同名同义:`expiresAtMs` = 本 ask 的绝对到期墙钟
|
|
73
|
+
* (有店=行上一次铸定的 `expiresAtMs`,与卡帧/落库同值;无店 D1=注册时刻算定的同名初值),
|
|
74
|
+
* `serverNowMs` = **帧铸造时刻**(cli [4429] 锚定语义:倒计时锚=帧到手时刻的服务器钟),
|
|
75
|
+
* `expiresInMs` = `max(0, expiresAtMs - serverNowMs)` 现算。三键同生同缺(windowZero 腿不发帧,
|
|
76
|
+
* 发出的帧恒带);旧消费端无感,新壳借此给旧族帧补倒计时([4297] Q2 那类 77 分钟死卡的根治半场)。 */
|
|
77
|
+
expiresAtMs?: number;
|
|
78
|
+
expiresInMs?: number;
|
|
79
|
+
serverNowMs?: number;
|
|
71
80
|
/**
|
|
72
81
|
* #144(core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,`"tool_approval"` only)——
|
|
73
82
|
* 这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。值 = 被越级的那条规则
|
|
@@ -256,6 +265,20 @@ export declare function parseToolApprovalResponse(body: unknown): {
|
|
|
256
265
|
ok: false;
|
|
257
266
|
error: string;
|
|
258
267
|
};
|
|
268
|
+
/**
|
|
269
|
+
* #280 R-13 C —— `UNATTENDED_APPROVAL_POLICY` 的**闭集**词表([4297]Q1「裁2:A+C」/[4338] 裁②)。
|
|
270
|
+
*
|
|
271
|
+
* 语义轴 = 「一只治理 ask 到了、而**没有任何人**能答」时这条部署要什么终局:
|
|
272
|
+
* · `park`(缺省)—— 交 `"unavailable"`,由 core 走 durable park:run 挂起候人(done(suspended) +
|
|
273
|
+
* pendingGate),人回来补批就能续。要求部署真有 park 设施(`durableApproval` + checkpoint 店);
|
|
274
|
+
* 没有的话 core 自己 fail-closed deny(文案自带「no durable approval gate is armed」)。
|
|
275
|
+
* · `deny` —— 真无人值守 / headless 部署的**显式**自声明:不积压 park,当场 deny 让模型自己改道。
|
|
276
|
+
*
|
|
277
|
+
* 闭集 + 穷举 `switch`/三元的理由(#157 无静默 fail-open):这条轴决定的是**权限门的终局方向**,
|
|
278
|
+
* 一个拼错的词若被当成「未设」静默回缺省,运维会以为自己关掉了 park 积压而实际没有。坏值 boot 拒启
|
|
279
|
+
* (`config.ts` 的解析腿,#210 A 档)。
|
|
280
|
+
*/
|
|
281
|
+
export type UnattendedApprovalPolicy = "park" | "deny";
|
|
259
282
|
/**
|
|
260
283
|
* #151 车3 刀 3b —— 「流内审批协议到底上不上场」的**单一谓词**(设计稿 §2.3 注入 / §2.4 能力面 /
|
|
261
284
|
* §8.4 park 设施自检,三处同一份判据)。
|
|
@@ -442,6 +465,12 @@ export declare class ToolApprovalCoordinator {
|
|
|
442
465
|
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
443
466
|
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
444
467
|
private readonly ruleConsent;
|
|
468
|
+
/** #280 R-13 C(`UNATTENDED_APPROVAL_POLICY`,设计稿 §2):**无人可答**时这条部署要的终局。
|
|
469
|
+
* `park`(缺省)= 交 `"unavailable"` 走 core durable park;`deny` = 显式自声明「真无人值守、不积压
|
|
470
|
+
* park、要模型当场自走」⇒ 同样五臂改判 deny。**纯部署级**:不看任何客户端 posture / 表态
|
|
471
|
+
* (memory: operator-knob-must-be-unconditional —— #153 shellGate 让部署级旋钮的生死由客户端表态
|
|
472
|
+
* 决定,同病两犯过一次)。安全轴:`deny` 是收紧方向(不放行任何东西),fail-closed 铁律不受损。 */
|
|
473
|
+
private readonly unattendedPolicy;
|
|
445
474
|
constructor(opts?: {
|
|
446
475
|
ttlMs?: number;
|
|
447
476
|
askStore?: ApprovalAskStore;
|
|
@@ -452,7 +481,40 @@ export declare class ToolApprovalCoordinator {
|
|
|
452
481
|
governanceAskMarks?: GovernanceAskMarks;
|
|
453
482
|
/** #154 车二:同意车道(在场 = 规则店已装配)。 */
|
|
454
483
|
ruleConsent?: RuleConsentLane;
|
|
484
|
+
/** #280 R-13 C:无人值守政策(见 {@link ToolApprovalCoordinator.unattendedPolicy});缺省 `park`。 */
|
|
485
|
+
unattendedPolicy?: UnattendedApprovalPolicy;
|
|
455
486
|
});
|
|
487
|
+
/**
|
|
488
|
+
* #280 R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。
|
|
489
|
+
*
|
|
490
|
+
* 五臂(设计稿 §1 的表)全部读它,`unattendedPolicy` 因此是一处施加、五处消费:
|
|
491
|
+
* (a) 有店窗到期 `expireAsk` 赢 CAS / (b) 无店窗到期(D1)/ (c) 有店但 `expireAsk` 报错
|
|
492
|
+
* —— 三条走 `windowRouteUnavailable` 标志,在 {@link shapeOutcome}(askBroadcast 内)成形;
|
|
493
|
+
* (d) `windowZero`(装配层短路)与无 ALS 上下文的 headless 腿 —— 走本方法/{@link ask};
|
|
494
|
+
* (e) 断连 `forceParkNow` / emit 全灭 / 写侧准入超限 —— 走本方法。
|
|
495
|
+
*
|
|
496
|
+
* **持久回放的两个口也归它管**(codex R1-F1 验真后收口,红先钉在
|
|
497
|
+
* `test/unattended-approval-policy.test.ts`):`convergeFromDurableState` 与 `ensureAsk` 幂等重入,
|
|
498
|
+
* 只要回放出来的是 `"unavailable"`(行 PARKING/PARKED)就过这个口。理由:那三处与臂 (a) 是**同一件
|
|
499
|
+
* 事**(本 ask 的等待以「行进了投递面」收场,赢家可能是别的副本 / 上一次调用),不统一的话同一只 ask
|
|
500
|
+
* 的终局会取决于「谁赢了那把 CAS」「重试没重试」—— 比残余更坏的不可解释性。人的**真决议**(回放出
|
|
501
|
+
* `true`/`false`)当然不过这个口:那不是「无人可答」。
|
|
502
|
+
*
|
|
503
|
+
* 🔴 **不**归它管的一类(刻意,不是遗漏):`ensureAsk` **身份未定 / 坏行**两臂 —— 那里的 park 路由是对
|
|
504
|
+
* **身份不确定**的处置(与「有没有人」无关),且背后可能正躺着一条真行。于是 `deny` 部署上仍可能落
|
|
505
|
+
* 极少数 park(店抖动期),已知且有意的残余,成文在 config-types 的域注。
|
|
506
|
+
*
|
|
507
|
+
* ⚠️ **`deny` 部署上持久行仍会经 PARKING 记账**(codex R1-F1 的另一半,验真后**不采纳**其「给 deny
|
|
508
|
+
* 造一条原子 deny 终态转移」的建议):那是店语义变更(新转移 + 批内兄弟连坐语义 + 真双库套件),且与
|
|
509
|
+
* 已裁的设计稿(§2:(a) 在 deny 下只改交给 core 的终局)相左。**危害亲验为零**:收敛器判据①的
|
|
510
|
+
* `bindBatch` 只把 PARKING 行绑到**已经存在的 core checkpoint** 上,而 `deny` 部署里 core 从不 park
|
|
511
|
+
* ⇒ 结构上绑不上,该行按判据②/④/⑤ 自行收敛成 DENIED/VOID,不会凭空生出一张 park 或幻影 gate。
|
|
512
|
+
* 残余 = 那类行的归因会记成 `routing_failure`(其实是政策 deny),归因精化登记为候件。
|
|
513
|
+
*/
|
|
514
|
+
private unattendedOutcome;
|
|
515
|
+
/** {@link unattendedOutcome} 的**公开**口:`windowZero` 的 immediate-unavailable 闭包长在装配层
|
|
516
|
+
* (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。 */
|
|
517
|
+
unattendedAskOutcome(): "unavailable" | false;
|
|
456
518
|
/**
|
|
457
519
|
* #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
|
|
458
520
|
*
|
|
@@ -461,7 +523,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
461
523
|
* 上限,`MAX_ALLOW_SESSIONS` 只管 grant 集合),所以回决腿与恢复扫描都只能在**已经分配之后**动手 ——
|
|
462
524
|
* 门必须长在这里。
|
|
463
525
|
*
|
|
464
|
-
* 超限的处置是 `"unavailable"`(park 路由)
|
|
526
|
+
* 超限的处置是 `"unavailable"`(park 路由),**缺省 park 政策下永不 deny**:过载是**我方**的容量事实,不是人对这次
|
|
465
527
|
* 操作的判断;用 deny 表达过载会把一次「本可以 park 后由人补批」的操作变成任务失败(§3.3 硬条款)。
|
|
466
528
|
*
|
|
467
529
|
* 只在 `askStore` 在场(= 协议开着)时把关:开关关闭时本方法恒放行,现行为逐字不变(D1/A-1)。
|
|
@@ -511,11 +573,39 @@ export declare class ToolApprovalCoordinator {
|
|
|
511
573
|
* 地输,只会「什么都不做」,永远等不到本地事件驱动 settle)。这里直接反查这些 askId 各自的本地 pending
|
|
512
574
|
* 条目集合并逐个调用它们自己的 `settle` 闭包(复用它们自己的 timer 清理/abort 监听器摘除/pending
|
|
513
575
|
* 删除),把远程/其他调用栈发生的终局如实同步回本地——查无对应条目(不在这个副本、或从未真正注册过)
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
576
|
+
* 是正常情况,静默跳过。
|
|
577
|
+
*
|
|
578
|
+
* 🔴 **本口只收「行真的没了」那一类**(#280 codex R3-F2,验真后拆分;红先钉在
|
|
579
|
+
* `test/unattended-approval-policy.test.ts`):`expireAsk` 返回的 `voidedSiblings` 的行是 **VOID**,
|
|
580
|
+
* 裸 `(false,"expired")` 正是它们的真终局。而**调用方自己那个 askId** 的行是 **PARKING**(已转投递
|
|
581
|
+
* 面)—— 把它混进本名单会让「同 askId 的重复本地注册」拿到裸 deny,而赢家拿的是 park 路由:同一条
|
|
582
|
+
* 持久行、两种 live 终局。那一支改走 {@link settleSameAskIdParkRoute}。 */
|
|
518
583
|
private settleVoidedSiblings;
|
|
584
|
+
/**
|
|
585
|
+
* #280 codex R3-F1 —— 「行上此刻有没有一个**真决议**」的一次性窄读(窗到期臂的店报错支专用)。
|
|
586
|
+
*
|
|
587
|
+
* 返回 `true`/`false` = 行是 `DECIDED` 且决议是 approve/deny(**人的**裁决,或车4 端点代人落的那一次);
|
|
588
|
+
* `undefined` = 其它一切(读失败、行不在、行是 STREAM_PENDING/PARKING/PARKED/DENIED/VOID)——调用方按
|
|
589
|
+
* 自己的臂继续。判别复用 {@link replayTerminalAskRow}(不造第二套映射),这里只把它的**布尔臂**取出来:
|
|
590
|
+
* `"unavailable"`(PARKING/PARKED)刻意**不**在本口消费,那属于路由面、归 `unattendedPolicy` 管。
|
|
591
|
+
*
|
|
592
|
+
* 一次读 + 墙钟 deadline(不重试):店已经在报错的语境里,重试只会把窗到期的收尾拖成秒级,而这一口要
|
|
593
|
+
* 回答的问题(「有没有人已经答过了」)读一次就够 —— 读不到就按「不知道」处置,与本臂原语义一致。
|
|
594
|
+
*
|
|
595
|
+
* ⚠️ **已登记的残余(codex R4-F1/F2,验真后不在本批修)**:本读与随后的本地 settle 之间没有持久 CAS,
|
|
596
|
+
* 所以「读到 STREAM_PENDING、另一副本**随后**才提交 DECIDED」这条窄窗仍会得到 park/deny 终局(行是
|
|
597
|
+
* approve 而本腿不执行);`emit` 全灭臂与取消臂的 store-未知态 catch 有**同形**缺口(两者都早于本件、
|
|
598
|
+
* 各自有既有判据钉着)。方向仍是 fail-closed(没有任何东西在未经人批时执行),补偿是 core 侧那张仍然
|
|
599
|
+
* pending 的 checkpoint —— 人再批一次就走。真正的收口 = 把窗/emit-全灭/取消三条 ambiguous-CAS catch
|
|
600
|
+
* 统一到一次**持久**恢复流程(带 CAS 与终态回读),那是店语义级改动,须由本模块属主立设计件,施工车
|
|
601
|
+
* 不私开(本批已试过整段复用 `convergeFromDurableState`,被自己的全量跑打回,理由见其顶注)。
|
|
602
|
+
*/
|
|
603
|
+
private readDecidedForRecovery;
|
|
604
|
+
/** #280 codex R3-F2:同一 askId 下**其余本地注册**(重复注册,见 {@link pendingByAskId} 顶注)按
|
|
605
|
+
* **park 路由**收尾 —— 行已 PARKING,它们看的是同一条行,终局必须与赢家同形(`unattendedPolicy`
|
|
606
|
+
* 由各自的闭包读同一个口,deny 部署上一起是 deny)。赢家自己此刻已 settle 过、已从集合里摘除,
|
|
607
|
+
* 故对它重复调用天然无操作。 */
|
|
608
|
+
private settleSameAskIdParkRoute;
|
|
519
609
|
/**
|
|
520
610
|
* #151 车4 §12-E(F29/F30 裁定形):外部回决(车4 端点或本类 respond 腿)**赢下持久 CAS 之后**,把同
|
|
521
611
|
* askId 下全部本地悬挂条目按**真实决议**结算——端点已是唯一权威(CAS 已落),本地只是同步终局;查无
|
package/dist/tool-approval.js
CHANGED
|
@@ -108,8 +108,11 @@ const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
|
|
|
108
108
|
const DURABLE_CALL_TIMEOUT_MS = 5_000;
|
|
109
109
|
/** {@link ToolApprovalCoordinator.withStoreDeadline} 超时时抛的**具名**错误。
|
|
110
110
|
* 🔴 具名类而不是「按 message 文本判别」:错误文案是给人看的,拿它当控制流 = 上游改一句人话下游静默
|
|
111
|
-
* 失效(#90 工程规范 v3 门②,机械门在 test/engineering-code-gates.test.ts 盯着)
|
|
112
|
-
* `instanceof` 分「超时(
|
|
111
|
+
* 失效(#90 工程规范 v3 门②,机械门在 test/engineering-code-gates.test.ts 盯着)。
|
|
112
|
+
* ⚠️ **判别用途已退役**(#280 R-13 A2,2026-08-18):窗到期 catch 臂此前用 `instanceof` 分「超时(不知道
|
|
113
|
+
* 做没做成)」与「店真报错」两条方向不同的臂;A2 把两支合判成 park 路由之后没有调用点再分它。类留着的
|
|
114
|
+
* 理由是**归因**(`noteStoreError` 的日志/计数里那个 name 区分得出「店挂了」与「店慢到没意义」)与
|
|
115
|
+
* `withStoreDeadline` 的迟到回调语义,不是控制流。 */
|
|
113
116
|
class ApprovalStoreDeadlineError extends Error {
|
|
114
117
|
constructor(label, timeoutMs) {
|
|
115
118
|
super(`approval store call ${label} timed out after ${timeoutMs}ms`);
|
|
@@ -387,6 +390,12 @@ export class ToolApprovalCoordinator {
|
|
|
387
390
|
* `RunnerDeps.permissionRuleStore` 同源于一个对象)⇒ ①ask 帧投 `ruleSuggestions`;②回决带
|
|
388
391
|
* `persistRule` 时兑付进店。缺席 ⇒ 两件都不做(诚实缺席,不发无处可兑的候选)。 */
|
|
389
392
|
ruleConsent;
|
|
393
|
+
/** #280 R-13 C(`UNATTENDED_APPROVAL_POLICY`,设计稿 §2):**无人可答**时这条部署要的终局。
|
|
394
|
+
* `park`(缺省)= 交 `"unavailable"` 走 core durable park;`deny` = 显式自声明「真无人值守、不积压
|
|
395
|
+
* park、要模型当场自走」⇒ 同样五臂改判 deny。**纯部署级**:不看任何客户端 posture / 表态
|
|
396
|
+
* (memory: operator-knob-must-be-unconditional —— #153 shellGate 让部署级旋钮的生死由客户端表态
|
|
397
|
+
* 决定,同病两犯过一次)。安全轴:`deny` 是收紧方向(不放行任何东西),fail-closed 铁律不受损。 */
|
|
398
|
+
unattendedPolicy;
|
|
390
399
|
constructor(opts) {
|
|
391
400
|
this.ruleConsent = opts?.ruleConsent;
|
|
392
401
|
this.governanceAskMarks = opts?.governanceAskMarks;
|
|
@@ -395,6 +404,42 @@ export class ToolApprovalCoordinator {
|
|
|
395
404
|
this.windowMarginMs = opts?.windowMarginMs ?? DEFAULT_WINDOW_MARGIN_MS;
|
|
396
405
|
this.admitMaxPerTask = opts?.admitMaxPerTask ?? DEFAULT_ADMIT_MAX_PER_TASK;
|
|
397
406
|
this.admitMaxPerOwner = opts?.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
|
|
407
|
+
this.unattendedPolicy = opts?.unattendedPolicy ?? "park";
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* #280 R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。
|
|
411
|
+
*
|
|
412
|
+
* 五臂(设计稿 §1 的表)全部读它,`unattendedPolicy` 因此是一处施加、五处消费:
|
|
413
|
+
* (a) 有店窗到期 `expireAsk` 赢 CAS / (b) 无店窗到期(D1)/ (c) 有店但 `expireAsk` 报错
|
|
414
|
+
* —— 三条走 `windowRouteUnavailable` 标志,在 {@link shapeOutcome}(askBroadcast 内)成形;
|
|
415
|
+
* (d) `windowZero`(装配层短路)与无 ALS 上下文的 headless 腿 —— 走本方法/{@link ask};
|
|
416
|
+
* (e) 断连 `forceParkNow` / emit 全灭 / 写侧准入超限 —— 走本方法。
|
|
417
|
+
*
|
|
418
|
+
* **持久回放的两个口也归它管**(codex R1-F1 验真后收口,红先钉在
|
|
419
|
+
* `test/unattended-approval-policy.test.ts`):`convergeFromDurableState` 与 `ensureAsk` 幂等重入,
|
|
420
|
+
* 只要回放出来的是 `"unavailable"`(行 PARKING/PARKED)就过这个口。理由:那三处与臂 (a) 是**同一件
|
|
421
|
+
* 事**(本 ask 的等待以「行进了投递面」收场,赢家可能是别的副本 / 上一次调用),不统一的话同一只 ask
|
|
422
|
+
* 的终局会取决于「谁赢了那把 CAS」「重试没重试」—— 比残余更坏的不可解释性。人的**真决议**(回放出
|
|
423
|
+
* `true`/`false`)当然不过这个口:那不是「无人可答」。
|
|
424
|
+
*
|
|
425
|
+
* 🔴 **不**归它管的一类(刻意,不是遗漏):`ensureAsk` **身份未定 / 坏行**两臂 —— 那里的 park 路由是对
|
|
426
|
+
* **身份不确定**的处置(与「有没有人」无关),且背后可能正躺着一条真行。于是 `deny` 部署上仍可能落
|
|
427
|
+
* 极少数 park(店抖动期),已知且有意的残余,成文在 config-types 的域注。
|
|
428
|
+
*
|
|
429
|
+
* ⚠️ **`deny` 部署上持久行仍会经 PARKING 记账**(codex R1-F1 的另一半,验真后**不采纳**其「给 deny
|
|
430
|
+
* 造一条原子 deny 终态转移」的建议):那是店语义变更(新转移 + 批内兄弟连坐语义 + 真双库套件),且与
|
|
431
|
+
* 已裁的设计稿(§2:(a) 在 deny 下只改交给 core 的终局)相左。**危害亲验为零**:收敛器判据①的
|
|
432
|
+
* `bindBatch` 只把 PARKING 行绑到**已经存在的 core checkpoint** 上,而 `deny` 部署里 core 从不 park
|
|
433
|
+
* ⇒ 结构上绑不上,该行按判据②/④/⑤ 自行收敛成 DENIED/VOID,不会凭空生出一张 park 或幻影 gate。
|
|
434
|
+
* 残余 = 那类行的归因会记成 `routing_failure`(其实是政策 deny),归因精化登记为候件。
|
|
435
|
+
*/
|
|
436
|
+
unattendedOutcome() {
|
|
437
|
+
return this.unattendedPolicy === "deny" ? false : "unavailable";
|
|
438
|
+
}
|
|
439
|
+
/** {@link unattendedOutcome} 的**公开**口:`windowZero` 的 immediate-unavailable 闭包长在装配层
|
|
440
|
+
* (`http/routes/tasks.ts` 的 sync 腿),它必须与协调器内五臂读**同一个**旋钮值,而不是各算一遍。 */
|
|
441
|
+
unattendedAskOutcome() {
|
|
442
|
+
return this.unattendedOutcome();
|
|
398
443
|
}
|
|
399
444
|
/**
|
|
400
445
|
* #151 车3 刀 3b —— design/172 §3.3 / 设计稿 §0 X-2 的**写侧准入门**。
|
|
@@ -404,7 +449,7 @@ export class ToolApprovalCoordinator {
|
|
|
404
449
|
* 上限,`MAX_ALLOW_SESSIONS` 只管 grant 集合),所以回决腿与恢复扫描都只能在**已经分配之后**动手 ——
|
|
405
450
|
* 门必须长在这里。
|
|
406
451
|
*
|
|
407
|
-
* 超限的处置是 `"unavailable"`(park 路由)
|
|
452
|
+
* 超限的处置是 `"unavailable"`(park 路由),**缺省 park 政策下永不 deny**:过载是**我方**的容量事实,不是人对这次
|
|
408
453
|
* 操作的判断;用 deny 表达过载会把一次「本可以 park 后由人补批」的操作变成任务失败(§3.3 硬条款)。
|
|
409
454
|
*
|
|
410
455
|
* 只在 `askStore` 在场(= 协议开着)时把关:开关关闭时本方法恒放行,现行为逐字不变(D1/A-1)。
|
|
@@ -602,10 +647,13 @@ export class ToolApprovalCoordinator {
|
|
|
602
647
|
* 地输,只会「什么都不做」,永远等不到本地事件驱动 settle)。这里直接反查这些 askId 各自的本地 pending
|
|
603
648
|
* 条目集合并逐个调用它们自己的 `settle` 闭包(复用它们自己的 timer 清理/abort 监听器摘除/pending
|
|
604
649
|
* 删除),把远程/其他调用栈发生的终局如实同步回本地——查无对应条目(不在这个副本、或从未真正注册过)
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
650
|
+
* 是正常情况,静默跳过。
|
|
651
|
+
*
|
|
652
|
+
* 🔴 **本口只收「行真的没了」那一类**(#280 codex R3-F2,验真后拆分;红先钉在
|
|
653
|
+
* `test/unattended-approval-policy.test.ts`):`expireAsk` 返回的 `voidedSiblings` 的行是 **VOID**,
|
|
654
|
+
* 裸 `(false,"expired")` 正是它们的真终局。而**调用方自己那个 askId** 的行是 **PARKING**(已转投递
|
|
655
|
+
* 面)—— 把它混进本名单会让「同 askId 的重复本地注册」拿到裸 deny,而赢家拿的是 park 路由:同一条
|
|
656
|
+
* 持久行、两种 live 终局。那一支改走 {@link settleSameAskIdParkRoute}。 */
|
|
609
657
|
settleVoidedSiblings(askIds) {
|
|
610
658
|
for (const askId of askIds) {
|
|
611
659
|
const entries = this.pendingByAskId.get(askId);
|
|
@@ -615,6 +663,51 @@ export class ToolApprovalCoordinator {
|
|
|
615
663
|
entry.settle(false, "expired"); // 拷贝快照:settle 会修改原集合
|
|
616
664
|
}
|
|
617
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* #280 codex R3-F1 —— 「行上此刻有没有一个**真决议**」的一次性窄读(窗到期臂的店报错支专用)。
|
|
668
|
+
*
|
|
669
|
+
* 返回 `true`/`false` = 行是 `DECIDED` 且决议是 approve/deny(**人的**裁决,或车4 端点代人落的那一次);
|
|
670
|
+
* `undefined` = 其它一切(读失败、行不在、行是 STREAM_PENDING/PARKING/PARKED/DENIED/VOID)——调用方按
|
|
671
|
+
* 自己的臂继续。判别复用 {@link replayTerminalAskRow}(不造第二套映射),这里只把它的**布尔臂**取出来:
|
|
672
|
+
* `"unavailable"`(PARKING/PARKED)刻意**不**在本口消费,那属于路由面、归 `unattendedPolicy` 管。
|
|
673
|
+
*
|
|
674
|
+
* 一次读 + 墙钟 deadline(不重试):店已经在报错的语境里,重试只会把窗到期的收尾拖成秒级,而这一口要
|
|
675
|
+
* 回答的问题(「有没有人已经答过了」)读一次就够 —— 读不到就按「不知道」处置,与本臂原语义一致。
|
|
676
|
+
*
|
|
677
|
+
* ⚠️ **已登记的残余(codex R4-F1/F2,验真后不在本批修)**:本读与随后的本地 settle 之间没有持久 CAS,
|
|
678
|
+
* 所以「读到 STREAM_PENDING、另一副本**随后**才提交 DECIDED」这条窄窗仍会得到 park/deny 终局(行是
|
|
679
|
+
* approve 而本腿不执行);`emit` 全灭臂与取消臂的 store-未知态 catch 有**同形**缺口(两者都早于本件、
|
|
680
|
+
* 各自有既有判据钉着)。方向仍是 fail-closed(没有任何东西在未经人批时执行),补偿是 core 侧那张仍然
|
|
681
|
+
* pending 的 checkpoint —— 人再批一次就走。真正的收口 = 把窗/emit-全灭/取消三条 ambiguous-CAS catch
|
|
682
|
+
* 统一到一次**持久**恢复流程(带 CAS 与终态回读),那是店语义级改动,须由本模块属主立设计件,施工车
|
|
683
|
+
* 不私开(本批已试过整段复用 `convergeFromDurableState`,被自己的全量跑打回,理由见其顶注)。
|
|
684
|
+
*/
|
|
685
|
+
async readDecidedForRecovery(askId) {
|
|
686
|
+
if (!this.askStore)
|
|
687
|
+
return undefined;
|
|
688
|
+
try {
|
|
689
|
+
const row = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(expire-error-recovery)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
690
|
+
if (row === null || row.state !== "DECIDED")
|
|
691
|
+
return undefined;
|
|
692
|
+
const replayed = replayTerminalAskRow(row);
|
|
693
|
+
return typeof replayed === "boolean" ? replayed : undefined;
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
this.noteStoreError(err, "getAsk(expire-error-recovery)");
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/** #280 codex R3-F2:同一 askId 下**其余本地注册**(重复注册,见 {@link pendingByAskId} 顶注)按
|
|
701
|
+
* **park 路由**收尾 —— 行已 PARKING,它们看的是同一条行,终局必须与赢家同形(`unattendedPolicy`
|
|
702
|
+
* 由各自的闭包读同一个口,deny 部署上一起是 deny)。赢家自己此刻已 settle 过、已从集合里摘除,
|
|
703
|
+
* 故对它重复调用天然无操作。 */
|
|
704
|
+
settleSameAskIdParkRoute(askId) {
|
|
705
|
+
const entries = this.pendingByAskId.get(askId);
|
|
706
|
+
if (!entries)
|
|
707
|
+
return;
|
|
708
|
+
for (const entry of [...entries])
|
|
709
|
+
entry.settleParkRoute(); // 拷贝快照:结算会修改原集合
|
|
710
|
+
}
|
|
618
711
|
/**
|
|
619
712
|
* #151 车4 §12-E(F29/F30 裁定形):外部回决(车4 端点或本类 respond 腿)**赢下持久 CAS 之后**,把同
|
|
620
713
|
* askId 下全部本地悬挂条目按**真实决议**结算——端点已是唯一权威(CAS 已落),本地只是同步终局;查无
|
|
@@ -745,8 +838,10 @@ export class ToolApprovalCoordinator {
|
|
|
745
838
|
const ctx = this.als.getStore();
|
|
746
839
|
// No run context (background/workflow/headless/durable-submit leg — [820]④) = nobody to render the card ⇒
|
|
747
840
|
// "unavailable"(交 durable park),不再把「无人可问」翻译成 deny(1.295 前无三值只能 fail-closed)。
|
|
841
|
+
// #280 R-13 C 臂(d):bg/resume 两腿的 `windowZero` 在装配层的表达就是**不包 ALS**
|
|
842
|
+
// (`resolveApprovalLeg` 顶注),于是它与 headless 腿一起落在本臂上 —— 政策取值走同一个口。
|
|
748
843
|
if (!ctx)
|
|
749
|
-
return
|
|
844
|
+
return this.unattendedOutcome();
|
|
750
845
|
// 出处 = 本 ctx 自身(ALS 腿的投递集合恒是它一个),显式传,不靠 askBroadcast 去猜。
|
|
751
846
|
return this.askBroadcast({ taskId: ctx.taskId, legKey: ctx.legKey ?? "", ...(ctx.legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic: ctx.legDeadlineMonotonic } : {}) }, [ctx], req, signal);
|
|
752
847
|
};
|
|
@@ -767,8 +862,10 @@ export class ToolApprovalCoordinator {
|
|
|
767
862
|
return (req, signal) => {
|
|
768
863
|
const set = this.streams.get(this.streamKey(identity.owner, identity.sessionId, identity.taskId));
|
|
769
864
|
const live = set && set.size > 0 ? [...set] : [];
|
|
865
|
+
// #280 R-13 C 臂(e)的入口形:查无活流 = 「卡送不到任何人」的**到达时刻**版本(emit 全灭是它的
|
|
866
|
+
// 事后版本)—— 同一类事实,同一个政策口。
|
|
770
867
|
if (live.length === 0)
|
|
771
|
-
return Promise.resolve(
|
|
868
|
+
return Promise.resolve(this.unattendedOutcome());
|
|
772
869
|
// 🔴 codex 交叉复审(刀 3a,high)修:身份轴取**捕获的出处身份**,不取投递集合的第一条流。
|
|
773
870
|
// `streamKey` 的 session 形不含 taskId ⇒ 同一集合里可以躺着不同 run 的 ctx,`aliveCtxs[0]` 只是
|
|
774
871
|
// 「当下第一条活着的投递连接」。用它当 runId 会双向出错:同一只出处 ask 在集合组成变化后重入拿到
|
|
@@ -802,11 +899,14 @@ export class ToolApprovalCoordinator {
|
|
|
802
899
|
// Session allow-all (CC "Yes, allow all edits during this session"): keyed [owner, sessionId, category] — the
|
|
803
900
|
// asking tool's OWN capability category must have been granted in THIS owner's session (修2; module header).
|
|
804
901
|
// 多活集合下 owner/sessionId 对全体 ctxs 恒同(streamKey 分组保证),primary 取值代表整个集合安全。
|
|
805
|
-
|
|
902
|
+
// #287([4434]② P0):`requiresRealApproval` 的成文语义=「必须真人逐次批,一揽子放行不得覆盖」——
|
|
903
|
+
// 这条短路正是它要禁的 blanket-allow,安全类 ask 无视既有 grant、恒往下走真出卡(core 侧持久规则
|
|
904
|
+
// 半场已源头关死:ruleSuggestionsOf 对该位返 {};流内这半在此)。写侧配对臂见 finishRespond。
|
|
905
|
+
if (req.requiresRealApproval !== true && primary.sessionId && this.allowAllSessions.has(sessionAllowKey(primary.owner, primary.sessionId, sessionAllowCategory(req.toolName)))) {
|
|
806
906
|
return true;
|
|
807
907
|
}
|
|
808
908
|
// #151 车3 刀 3b(设计稿 §0 X-2):**写侧准入门** —— 落 pending / 建 timer / 落行 / 发帧之前。
|
|
809
|
-
// 超限 ⇒ `"unavailable"
|
|
909
|
+
// 超限 ⇒ park 路由(缺省 `park` 政策下即 `"unavailable"`,永不 deny)。协议未上场时恒放行(见 {@link admit})。
|
|
810
910
|
const releaseAdmission = this.admit(origin.taskId, primary.owner);
|
|
811
911
|
if (!releaseAdmission) {
|
|
812
912
|
defaultLogger.warn("approval admission gate: too many pending asks — routing to park", {
|
|
@@ -815,7 +915,10 @@ export class ToolApprovalCoordinator {
|
|
|
815
915
|
admitMaxPerTask: this.admitMaxPerTask,
|
|
816
916
|
admitMaxPerOwner: this.admitMaxPerOwner,
|
|
817
917
|
});
|
|
818
|
-
|
|
918
|
+
// #280 R-13 C 臂(e):`deny` 政策下同样改判 deny —— 「过载 ⇒ 永不 deny」那条硬条款的成立前提是
|
|
919
|
+
// **有** park 可落(把一次可补批的操作变成任务失败才是它要禁的);声明了无人值守的部署里 park 本身
|
|
920
|
+
// 就是死信,当场 deny 反而是它要的诚实终局。方向仍是收紧(不放行任何东西)。
|
|
921
|
+
return this.unattendedOutcome();
|
|
819
922
|
}
|
|
820
923
|
// `id` = **本地** pending 条目的键(每次注册各一个,绝不共享 —— {@link pendingByAskId} 顶注记的
|
|
821
924
|
// 「同 askId 重复本地注册」两类成因下,两条条目必须各占一格,否则后到者会覆盖先到者的登记)。
|
|
@@ -1036,7 +1139,12 @@ export class ToolApprovalCoordinator {
|
|
|
1036
1139
|
// 重启。修法:行不是 STREAM_PENDING ⇒ 不重新挂卡,直接回放既有终局(不落 pending、不 emit)。
|
|
1037
1140
|
if (existingOrCreated.row.state !== "STREAM_PENDING") {
|
|
1038
1141
|
releaseAdmission(); // X-2:回放既有终局 = 这只 ask 从未占用一条未决腿,名额立刻还回去
|
|
1039
|
-
|
|
1142
|
+
// 🔴 #280 codex 对抗复审 R1-F1 [high](验真后**部分采纳**,红先复现):回放出来的
|
|
1143
|
+
// `"unavailable"`(行是 PARKING/PARKED)必须**同样过政策口**。不过的话,`deny` 部署上臂 (a)
|
|
1144
|
+
// 写下的 PARKING 行会让**同一只 ask** 的终局取决于「重试没重试」:首次 deny、重入 park。
|
|
1145
|
+
// 人的真决议(DECIDED ⇒ true/false)不经这个口 —— 那不是「无人可答」,回放它是幂等复述。
|
|
1146
|
+
const replayed = replayTerminalAskRow(existingOrCreated.row);
|
|
1147
|
+
return replayed === "unavailable" ? this.unattendedOutcome() : replayed;
|
|
1040
1148
|
}
|
|
1041
1149
|
// 🔴 F2 修:行 = 真源。刚插的行上这三样与本地一致(采信是恒等操作);**重入**拿到既有行时,
|
|
1042
1150
|
// 采信才是唯一正确的做法 —— 帧、定时器、落库三处从此只有一个 deadline、一个 approvalId、一份卡。
|
|
@@ -1184,7 +1292,9 @@ export class ToolApprovalCoordinator {
|
|
|
1184
1292
|
let emitSettled = false;
|
|
1185
1293
|
// #151 车2(D2 窗到期竞争者的返回值覆盖):askStore 在场且 expireAsk 赢 CAS ⇒ 强制返回 "unavailable"
|
|
1186
1294
|
// (park 路由,G1 回路)——与旧形的 emit-race 覆盖判据(!emitSettled && lastOutcome==="expired")并存,
|
|
1187
|
-
// 互不排斥(见方法尾的返回值分派)。
|
|
1295
|
+
// 互不排斥(见方法尾的返回值分派)。
|
|
1296
|
+
// ⚠️ #280 R-13 A1/A2 起,**askStore 缺席的 D1 窗到期臂与店报错臂也置位它**(park 路由是三条窗臂
|
|
1297
|
+
// 共用的终局形),所以「缺席时恒 false」那句话已作废;三臂的差别只剩宿主自报(见各臂注)。
|
|
1188
1298
|
let windowRouteUnavailable = false;
|
|
1189
1299
|
let timer;
|
|
1190
1300
|
let resolveAllowed;
|
|
@@ -1216,13 +1326,14 @@ export class ToolApprovalCoordinator {
|
|
|
1216
1326
|
* 审计面由店的 decisionNote 承载)。理由只从 respond 腿流入(finishRespond),窗到期/取消/
|
|
1217
1327
|
* 外部清算臂无人写理由,恒缺席。
|
|
1218
1328
|
* · 其余 ⇒ 裸 boolean(falsy 纪律)。
|
|
1329
|
+
*
|
|
1330
|
+
* #280 R-13 C:`windowRouteUnavailable` 那一位在 `deny` 政策下**不返回 `"unavailable"`**,而是落到
|
|
1331
|
+
* 同一份 deny 成形上(自报/理由若在场照旧带上)—— 五臂一处施加的落点之一(见
|
|
1332
|
+
* {@link ToolApprovalCoordinator.unattendedOutcome})。⚠️ 那一支**不能**直接 fall through 到下面的
|
|
1333
|
+
* `settled.allowed` 分派:park 路由的语义是「持久终局压过一切」,而它可能与一个 `allowed:true` 的
|
|
1334
|
+
* 本地落定值并存(窗赢了 CAS 而人的 respond 稍后才到的那个交错)—— fall through 会把它渲成放行。
|
|
1219
1335
|
*/
|
|
1220
|
-
const
|
|
1221
|
-
if (windowRouteUnavailable)
|
|
1222
|
-
return "unavailable";
|
|
1223
|
-
if (settled.allowed) {
|
|
1224
|
-
return settled.updatedInput !== undefined ? { allow: true, updatedInput: settled.updatedInput } : true;
|
|
1225
|
-
}
|
|
1336
|
+
const shapeDeny = () => {
|
|
1226
1337
|
if (hostSettled !== undefined || (denyReasonSettled !== undefined && denyReasonSettled !== "")) {
|
|
1227
1338
|
return {
|
|
1228
1339
|
allow: false,
|
|
@@ -1232,6 +1343,14 @@ export class ToolApprovalCoordinator {
|
|
|
1232
1343
|
}
|
|
1233
1344
|
return false;
|
|
1234
1345
|
};
|
|
1346
|
+
const shapeOutcome = (settled) => {
|
|
1347
|
+
if (windowRouteUnavailable)
|
|
1348
|
+
return this.unattendedOutcome() === false ? shapeDeny() : "unavailable";
|
|
1349
|
+
if (settled.allowed) {
|
|
1350
|
+
return settled.updatedInput !== undefined ? { allow: true, updatedInput: settled.updatedInput } : true;
|
|
1351
|
+
}
|
|
1352
|
+
return shapeDeny();
|
|
1353
|
+
};
|
|
1235
1354
|
const settle = (allowed, outcome, updatedInput, hostSettledBy, denyReason) => {
|
|
1236
1355
|
if (done)
|
|
1237
1356
|
return;
|
|
@@ -1278,6 +1397,12 @@ export class ToolApprovalCoordinator {
|
|
|
1278
1397
|
* `DENIED|VOID` ⇒ deny。
|
|
1279
1398
|
* 行读不出(店抖动)⇒ **有界重试**(指数退避,期间随时被真赢家 settle 就退出),用尽仍失败 ⇒
|
|
1280
1399
|
* D5 fail-open 退回纯进程内语义(`settle(false,"expired")`)——绝不无限等。
|
|
1400
|
+
*
|
|
1401
|
+
* ⚠️ #280 施工纪要(留一句免得下一个人重走):codex R3-F1 的第一版修法是让 `expireAsk` 报错臂整段复用
|
|
1402
|
+
* 本收敛,被**自己的全量跑**打回来了 —— 整套收敛会在几秒的退避里把终局让给并发的取消臂,而「行仍
|
|
1403
|
+
* STREAM_PENDING」恰是那条臂的**期望态**(我们这次 expire 就是没做成),预算等于白烧;两条既有判据
|
|
1404
|
+
* (codex-R2-2 / codex-R3-1)当场红。最终收窄成一次性窄读,见
|
|
1405
|
+
* {@link ToolApprovalCoordinator.readDecidedForRecovery}。
|
|
1281
1406
|
*/
|
|
1282
1407
|
const convergeFromDurableState = async (where) => {
|
|
1283
1408
|
if (!this.askStore || askId === undefined)
|
|
@@ -1294,7 +1419,8 @@ export class ToolApprovalCoordinator {
|
|
|
1294
1419
|
if (done)
|
|
1295
1420
|
return;
|
|
1296
1421
|
if (row === null) {
|
|
1297
|
-
// 行不在(被 deleteByTask 清过 / 从未落盘)
|
|
1422
|
+
// 行不在(被 deleteByTask 清过 / 从未落盘)——没有可信终局可读,退回进程内语义
|
|
1423
|
+
// (调用方声明自己收尾时不落终局,见 opts 顶注)。
|
|
1298
1424
|
settle(false, "expired");
|
|
1299
1425
|
return;
|
|
1300
1426
|
}
|
|
@@ -1306,7 +1432,10 @@ export class ToolApprovalCoordinator {
|
|
|
1306
1432
|
}
|
|
1307
1433
|
const outcome = replayTerminalAskRow(row);
|
|
1308
1434
|
if (outcome === "unavailable") {
|
|
1309
|
-
|
|
1435
|
+
// park 语义 ⇒ onAsk 交 `"unavailable"`(G1 回路)。#280 R-13 C:本臂与臂 (a) 是**同一件事**
|
|
1436
|
+
// (本 ask 的等待以「行进了投递面」收场,只是赢家可能在别的副本)⇒ `deny` 政策下随 (a) 一起
|
|
1437
|
+
// 改判 deny(自报缺席 —— 结束等待的不是本仓的窗)。理由全文见 unattendedOutcome 顶注。
|
|
1438
|
+
windowRouteUnavailable = true;
|
|
1310
1439
|
settle(false, "expired");
|
|
1311
1440
|
}
|
|
1312
1441
|
else if (outcome === true) {
|
|
@@ -1325,12 +1454,44 @@ export class ToolApprovalCoordinator {
|
|
|
1325
1454
|
if (!done)
|
|
1326
1455
|
settle(false, "expired"); // 重试用尽:D5 fail-open,绝不挂死
|
|
1327
1456
|
};
|
|
1457
|
+
/**
|
|
1458
|
+
* park 路由 + 结算的**单赢者**提交(🔴 #280 codex 对抗复审 R2-F1 [high],验真后修,红先复现器 =
|
|
1459
|
+
* `test/unattended-approval-policy.test.ts` 的 R2-F1 格)。
|
|
1460
|
+
*
|
|
1461
|
+
* 病灶:`windowRouteUnavailable = true` 与受 `done` 保护的 `settle` 是**两句**,而 `shapeOutcome` 把
|
|
1462
|
+
* 标志读在最前面。于是「回决先落定 → 迟到的窗臂再置标志」这条交错里(实测确定性可复现:窗先到期
|
|
1463
|
+
* 让 `expireAsk` 在飞,回决晚一拍赢下 CAS 并**回了 200**,店的报错续接恰好排在 settle 之后、
|
|
1464
|
+
* `askBroadcast` 尾部读标志之前),端点已经告诉人「已批准」、持久行也是 DECIDED(approve),
|
|
1465
|
+
* `onAsk` 却交出 `"unavailable"`(deny 政策下是 `false`)—— 人批了、工具不跑、run 反而再挂起。
|
|
1466
|
+
*
|
|
1467
|
+
* 修法 = 让改判路由这件事**继承 `settle` 的单赢者语义**:没赢下这次结算就一个字都不改。
|
|
1468
|
+
* ⚠️ 只给「不知道持久真源是什么」的两条臂用(D1 无店 / `expireAsk` 报错)。臂 (a)(`expireAsk` 真赢
|
|
1469
|
+
* CAS)与向持久真源收敛那两处**故意不走它**:那里的 PARKING/PARKED 是**读出来的持久事实**,按设计
|
|
1470
|
+
* (§D2「赢 ⇒ 无条件 park 路由」)压过一切本地落定值 —— 那条语义早于本件,不在本修射程内。
|
|
1471
|
+
*/
|
|
1472
|
+
const settleParkRouteIfUnsettled = (hostSettledBy) => {
|
|
1473
|
+
if (done)
|
|
1474
|
+
return;
|
|
1475
|
+
windowRouteUnavailable = true;
|
|
1476
|
+
settle(false, "expired", undefined, hostSettledBy);
|
|
1477
|
+
};
|
|
1328
1478
|
const windowExpired = () => {
|
|
1329
1479
|
void (async () => {
|
|
1330
1480
|
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1331
|
-
// D1(无持久 ask 店)——终局**完全**由本仓这个窗决定,没有第二个写者。
|
|
1332
|
-
//
|
|
1333
|
-
|
|
1481
|
+
// D1(无持久 ask 店)——终局**完全**由本仓这个窗决定,没有第二个写者。
|
|
1482
|
+
//
|
|
1483
|
+
// 🔴 #280 R-13 A1(设计稿 §2;[4297] 现场那条 error 的铸点):本臂**改判 park 路由**
|
|
1484
|
+
// (`windowRouteUnavailable` ⇒ `"unavailable"`),与 forceParkNow 的无店支同形。park 的承载是
|
|
1485
|
+
// **core checkpoint**(`spec.durableApproval`),不是 server 的 ask 行 —— 所以无 askStore 的
|
|
1486
|
+
// 部署同样受益;两者皆缺席时 core 以 `approverUnavailable` 文案 deny(fail-closed 保持,
|
|
1487
|
+
// 文字从「窗走完」变「无 durable 门可停靠」,对模型更可行动)。
|
|
1488
|
+
//
|
|
1489
|
+
// `"timeout"` 自报**留着但只在 `deny` 政策下才读得到**(shapeOutcome 的 park 支在它之前短路):
|
|
1490
|
+
// 5.23 立这个词是为了防「没人理」被记成「一个人拒绝了」—— park 路由下那种误记结构上不存在
|
|
1491
|
+
// (根本不是 deny),词的存在理由被 park 语义整体取代;而 `deny` 政策下结束这次等待的**确实**
|
|
1492
|
+
// 是本仓的窗,词仍然如实,必须留。
|
|
1493
|
+
// 单赢者提交(codex R2-F1):人已落定 ⇒ 一个字都不改(D1 的 respond 是同步收尾,这条交错真实可达)。
|
|
1494
|
+
settleParkRouteIfUnsettled("timeout");
|
|
1334
1495
|
return;
|
|
1335
1496
|
}
|
|
1336
1497
|
try {
|
|
@@ -1339,15 +1500,20 @@ export class ToolApprovalCoordinator {
|
|
|
1339
1500
|
// 是这次事务真做出来的事实,必须补做 —— 否则兄弟闭包挂死、壳上留一张已作废的卡。
|
|
1340
1501
|
if (!late.won)
|
|
1341
1502
|
return;
|
|
1342
|
-
|
|
1503
|
+
// #280 codex R3-F2:两类分开收 —— 同 askId 重复注册的行是 **PARKING**(park 路由),
|
|
1504
|
+
// `voidedSiblings` 的行是 **VOID**(裸 expired)。混在一个名单里会让前者拿到裸 deny。
|
|
1505
|
+
this.settleSameAskIdParkRoute(askId);
|
|
1506
|
+
this.settleVoidedSiblings(late.voidedSiblings);
|
|
1343
1507
|
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, late.voidedSiblings, "superseded_by_park", Date.now()));
|
|
1344
1508
|
});
|
|
1345
1509
|
if (res.won) {
|
|
1346
1510
|
windowRouteUnavailable = true;
|
|
1347
1511
|
settle(false, "expired");
|
|
1348
1512
|
// round3:同批兄弟被远程撤卡,本地同步收尾;round5 追加自己的 askId——覆盖「同 askId 重复本地
|
|
1349
|
-
// 注册」那一支(
|
|
1350
|
-
|
|
1513
|
+
// 注册」那一支(顶注)。#280 codex R3-F2:那一支的行是 **PARKING** ⇒ 走 park 路由口,不能与
|
|
1514
|
+
// 行已 VOID 的兄弟混用同一个裸 expired 结算(否则同一条行两种 live 终局)。
|
|
1515
|
+
this.settleSameAskIdParkRoute(askId);
|
|
1516
|
+
this.settleVoidedSiblings(res.voidedSiblings);
|
|
1351
1517
|
// #151 车6 发射点①:降级连坐的兄弟被原子撤卡 ⇒ 壳上那些卡必须消失(live only)。
|
|
1352
1518
|
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, res.voidedSiblings, "superseded_by_park", Date.now()));
|
|
1353
1519
|
}
|
|
@@ -1358,27 +1524,50 @@ export class ToolApprovalCoordinator {
|
|
|
1358
1524
|
}
|
|
1359
1525
|
catch (err) {
|
|
1360
1526
|
this.noteStoreError(err, "expireAsk");
|
|
1361
|
-
// R3-1
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1527
|
+
// R3-1 原文只把**超时**支改判 park:超时 = 不知道那次 `expireAsk` 做没做成 —— 若它稍后提交,
|
|
1528
|
+
// 行就是 PARKING(已转投递面);此时把 onAsk 判成 deny 会与持久真源矛盾,而且违反 §3.3
|
|
1529
|
+
// 「窗不得把一个可 park 的 ask 拖成 abort-deny」。交 `"unavailable"`(park 路由)对两种结局都
|
|
1530
|
+
// 成立:真提交了 ⇒ 与行一致;没提交 ⇒ core 自己走 durable park,不劣于 deny。
|
|
1531
|
+
//
|
|
1532
|
+
// 🔴 #280 R-13 A2:**同一条理由逐字覆盖「店真报错」那一支** —— SQL 店的 `expireAsk` 同样是
|
|
1533
|
+
// 「事务提交 + 另一跳确认」的形,一次报错既可能是事务没做成,也可能是做成了而回报断了;两种
|
|
1534
|
+
// 结局下 park 路由都不劣于 deny。于是 deadline 与非 deadline 两支从此**同判**,分支消失。
|
|
1535
|
+
// fail-open(D5)不变:店抖动不挡窗到期的进程内机械照旧完成。`"timeout"` 自报的存留理由与 D1
|
|
1536
|
+
// 臂逐字同(park 政策下读不到,`deny` 政策下如实)。
|
|
1537
|
+
// 🔴 单赢者提交(codex R2-F1,红先复现):回决可能已经赢下 CAS 并**回了 200**,而这次报错的
|
|
1538
|
+
// 续接恰好排在它之后 —— 无条件置标志会把一次人类批准改判成 park/deny。没赢下结算就不改判。
|
|
1539
|
+
//
|
|
1540
|
+
// 🔴 **先看一眼行上有没有真决议**(codex R3-F1,三轮追问后改判采纳;红先钉 =
|
|
1541
|
+
// `test/unattended-approval-policy.test.ts` 的 R3-F1 两格):店报错 ≠ 没有真决议 —— **另一个
|
|
1542
|
+
// 副本**的回决腿可能刚把行判成 `DECIDED`,而本臂此前直接按政策收窗,等于把一次已经落盘的
|
|
1543
|
+
// 人类决议丢掉(工具不跑、run 还要人再批一次)。
|
|
1544
|
+
//
|
|
1545
|
+
// ⚠️ **只消费 `DECIDED`,而且只读一次**(收窄是刻意的,第一版写成整套
|
|
1546
|
+
// `convergeFromDurableState` 被自己的全量跑打回来了,两条既有判据当场红):
|
|
1547
|
+
// · 只读一次(一次带墙钟的 `getAsk`)⇒ 不把整段退避预算烧在「行仍 STREAM_PENDING」这个**期望
|
|
1548
|
+
// 态**上,也不会在那几秒里把终局让给并发的取消臂(codex-R2-2 钉的形当场翻)。
|
|
1549
|
+
// · 只认 `DECIDED` ⇒ `VOID`(批内兄弟被撤卡)/`PARKING`/读不出 三态的处置**逐字不动**,
|
|
1550
|
+
// 它们各自的判据早有钉(codex-R3-1 的迟到提交形、settleVoidedSiblings 的 F29 语义)。
|
|
1551
|
+
// 本修只补「人的真决议不许被丢」这**一条**缺口,不顺手改别人的语义。
|
|
1552
|
+
const decided = await this.readDecidedForRecovery(askId);
|
|
1553
|
+
if (decided !== undefined && !done) {
|
|
1554
|
+
settle(decided, decided ? "allowed" : "denied");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
settleParkRouteIfUnsettled("timeout");
|
|
1371
1558
|
}
|
|
1372
1559
|
})();
|
|
1373
1560
|
};
|
|
1374
1561
|
// #241:断连强制转 park(接口注释见 PendingApproval.forceParkNow)。有店 ⇒ 走窗到期同路(expireAsk
|
|
1375
1562
|
// CAS+兄弟撤卡,赢=unavailable/输=向持久真源收敛——人已决就尊重人);无店 ⇒ 进程内直接改判 park 路由。
|
|
1563
|
+
// ⚠️ #280 R-13 之后无店支**仍不**复用 windowExpired,理由换了一条:两臂的 park/deny 方向如今同判
|
|
1564
|
+
// (A1),但**宿主自报**不同——窗到期臂说得出 `"timeout"`(结束等待的是本仓的窗),断连臂说不出
|
|
1565
|
+
// (结束等待的是连接没了,那个词会把「任务的流没了」谎成「没人答」)。自报只在 `deny` 政策下可见。
|
|
1376
1566
|
const forceParkNow = () => {
|
|
1377
1567
|
if (done)
|
|
1378
1568
|
return;
|
|
1379
1569
|
if (!this.askStore || askId === undefined || batchId === undefined) {
|
|
1380
|
-
|
|
1381
|
-
settle(false, "expired");
|
|
1570
|
+
settleParkRouteIfUnsettled(); // 单赢者提交(自报缺席:结束等待的是断连,不是窗)
|
|
1382
1571
|
return;
|
|
1383
1572
|
}
|
|
1384
1573
|
windowExpired();
|
|
@@ -1507,6 +1696,9 @@ export class ToolApprovalCoordinator {
|
|
|
1507
1696
|
const pendingEntry = {
|
|
1508
1697
|
settle,
|
|
1509
1698
|
forceParkNow,
|
|
1699
|
+
// #280 codex R3-F2:同 askId 重复注册的 park 路由收尾口(自报缺席 —— 结束这份注册等待的不是它
|
|
1700
|
+
// 自己的窗,是「这只 askId 已经有了投递面终局」这件事)。
|
|
1701
|
+
settleParkRoute: () => settleParkRouteIfUnsettled(),
|
|
1510
1702
|
owner: primary.owner,
|
|
1511
1703
|
ctxTaskId: primary.taskId,
|
|
1512
1704
|
originCtx: primary,
|
|
@@ -1519,6 +1711,8 @@ export class ToolApprovalCoordinator {
|
|
|
1519
1711
|
// #204 件7:治理标随条目落定(回决侧第二道门的输入,理由见字段顶注)。读的是**并集后**的值
|
|
1520
1712
|
// (`effectiveGovernanceForced`),不是本地 peek —— 幂等命中既有行那条路上,只有并集才是武装的。
|
|
1521
1713
|
...(effectiveGovernanceForced ? { governanceForced: true } : {}),
|
|
1714
|
+
// #287:安全类出身位随条目落定(回决侧拒铸 grant 的输入;真才带,同 governanceForced 形)。
|
|
1715
|
+
...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
1522
1716
|
...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
|
|
1523
1717
|
};
|
|
1524
1718
|
selfEntry = pendingEntry; // 供 settle() 结算时把自己从 pendingByAskId 的 Set 里摘除(上方顶注)
|
|
@@ -1544,6 +1738,15 @@ export class ToolApprovalCoordinator {
|
|
|
1544
1738
|
const cardFrame = askId !== undefined && cardForFrame !== undefined
|
|
1545
1739
|
? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now() })
|
|
1546
1740
|
: undefined;
|
|
1741
|
+
// #288([4429]②):旧族帧补窗三键 —— 必须在这里(emit 前、ensureAsk 后)而不是 frame 字面量里:
|
|
1742
|
+
// `persistedExpiresAtMs` 在有店重入既有行时才被行值覆盖(F2 修),字面量铸造点还拿不到最终值;
|
|
1743
|
+
// 这里与 cardFrame 用**同一个数**,两族帧的到期读数从此同源(锚=此刻的服务器钟)。
|
|
1744
|
+
{
|
|
1745
|
+
const serverNowMs = Date.now();
|
|
1746
|
+
frame.expiresAtMs = persistedExpiresAtMs;
|
|
1747
|
+
frame.serverNowMs = serverNowMs;
|
|
1748
|
+
frame.expiresInMs = Math.max(0, persistedExpiresAtMs - serverNowMs);
|
|
1749
|
+
}
|
|
1547
1750
|
/**
|
|
1548
1751
|
* 一条连接的**两种**投递结果。分开记是 codex 交叉复审 F1(2026-08-06 真 finding)的修:
|
|
1549
1752
|
* · `legacy` —— 旧 `tool_approval` 帧送达没有。它单独决定**闭合帧**发不发(没见过开卡帧的连接
|
|
@@ -1619,7 +1822,9 @@ export class ToolApprovalCoordinator {
|
|
|
1619
1822
|
emitFailed = true;
|
|
1620
1823
|
settle(false, "expired");
|
|
1621
1824
|
// round3:同批兄弟被远程撤卡,本地同步收尾;round5 追加自己的 askId(同精神,windowExpired 侧注)。
|
|
1622
|
-
|
|
1825
|
+
// #280 codex R3-F2:同 askId 那一支的行是 PARKING ⇒ park 路由口;兄弟(VOID)裸 expired。
|
|
1826
|
+
this.settleSameAskIdParkRoute(askId);
|
|
1827
|
+
this.settleVoidedSiblings(res.voidedSiblings);
|
|
1623
1828
|
}
|
|
1624
1829
|
// 干净地输:什么都不做(见上方顶注)。
|
|
1625
1830
|
}
|
|
@@ -1659,20 +1864,23 @@ export class ToolApprovalCoordinator {
|
|
|
1659
1864
|
.catch(() => undefined);
|
|
1660
1865
|
}
|
|
1661
1866
|
// emit 全灭 = 卡从未送达任何人(≠人拒绝/TTL 走人)——同属「无活人可达」类 ⇒ "unavailable" 交 durable
|
|
1662
|
-
// park。
|
|
1867
|
+
// park(#280 R-13 C 臂(e):`deny` 政策下同样改判 deny,取值走 unattendedOutcome 那**一个**口)。
|
|
1663
1868
|
if (emitFailed)
|
|
1664
|
-
return
|
|
1869
|
+
return this.unattendedOutcome();
|
|
1665
1870
|
// #151 车2(D2/D3):窗到期赢了持久 CAS ⇒ 强制 park 路由,不看 emit 是否已经确认送达——D2 原话
|
|
1666
1871
|
// 「赢 ⇒ settle + onAsk 返回 "unavailable"」是无条件的,与下面 emit-race 覆盖判据(供 store 缺席/
|
|
1667
|
-
// emit 尚未确认两种场景)并存、互不排斥。askStore
|
|
1872
|
+
// emit 尚未确认两种场景)并存、互不排斥。askStore 缺席时本判据在 #280 之前恒 false;A1 之后 D1 窗到期
|
|
1873
|
+
// 臂也会置位它(park 路由是三臂共用的终局),两条路因此汇到同一个成形口。
|
|
1874
|
+
// 🔴 经 {@link shapeOutcome} 而不是就地 `return "unavailable"`:政策与宿主自报的判据只写一份
|
|
1875
|
+
// (deny 政策下 (b)(c) 要带 `settledBy:"timeout"`,就地返回会把它丢掉)。
|
|
1668
1876
|
if (windowRouteUnavailable)
|
|
1669
|
-
return
|
|
1877
|
+
return shapeOutcome(settled);
|
|
1670
1878
|
// emit 广播尚未有任何一路确认完成(既非成功也非已知失败)就被 TTL/abort 抢先判定 ⇒ 无法确认卡是否
|
|
1671
1879
|
// 曾送达任何人,不可扣一个武断的「deny」终局——按 G1 回路交 "unavailable"(同 emit 失败同一处置,
|
|
1672
|
-
// core [1559]三
|
|
1673
|
-
//
|
|
1880
|
+
// core [1559]三 确认方向;`deny` 政策同臂(e)一并改判)。respond() 落地的人类裁决(outcome
|
|
1881
|
+
// allowed/denied)不受影响,原因见本方法顶注。
|
|
1674
1882
|
if (!emitSettled && lastOutcome === "expired")
|
|
1675
|
-
return
|
|
1883
|
+
return this.unattendedOutcome();
|
|
1676
1884
|
// [1458] 编辑放行对象臂 + core 5.23.0(#187/#114②)宿主自报臂 —— 两条判据都在
|
|
1677
1885
|
// {@link shapeOutcome} 里,与早退出口**共用同一份**(codex 复审 round2:此前这里与早退臂各写一份
|
|
1678
1886
|
// 等价分派,本批的 `settledBy` 于是只加到了其中一份上)。
|
|
@@ -1883,10 +2091,14 @@ export class ToolApprovalCoordinator {
|
|
|
1883
2091
|
// grant 落桥内店)= true;allow_session 但无 sessionId(adhoc 无会话,记不了)= false(壳不得渲染
|
|
1884
2092
|
// 「本 session 全放行」);普通 allow/deny 不涉 remember = 字段省略。durable decide 腿的同名回显
|
|
1885
2093
|
// (`routes/approvals-assistant.ts` 的 `rememberApplied`)语义对齐。
|
|
2094
|
+
// #287([4434]② P0 的写侧配对臂):安全类 ask(requiresRealApproval)的卡上点了 allow_session,
|
|
2095
|
+
// 只放行**这一次**调用、绝不铸 session grant——铸了就等于把「必须逐次真人批」的位翻译成「批一次
|
|
2096
|
+
// 解锁整族」。rememberApplied=false 诚实回显(壳不得渲染「本 session 全放行」)。读侧配对臂
|
|
2097
|
+
// (askBroadcast 短路跳过)保证即使别的普通 ask 铸过同族 grant,安全类 ask 也照样出卡。
|
|
1886
2098
|
let rememberApplied;
|
|
1887
2099
|
if (parsed.value === "allow_session")
|
|
1888
|
-
rememberApplied = Boolean(entry.sessionId);
|
|
1889
|
-
if (parsed.value === "allow_session" && entry.sessionId) {
|
|
2100
|
+
rememberApplied = Boolean(entry.sessionId) && entry.requiresRealApproval !== true;
|
|
2101
|
+
if (parsed.value === "allow_session" && entry.sessionId && entry.requiresRealApproval !== true) {
|
|
1890
2102
|
// Bounded remember (insertion-order evict). 修2 scope: the grant is keyed by THIS ask's own capability
|
|
1891
2103
|
// category — a Write/Edit-family ask unlocks the fs-write family for the (owner, session); ANY other tool's
|
|
1892
2104
|
// allow_session unlocks only that same tool. A cheap non-write ask can therefore never become a session-wide
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.34.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.43.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.18.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|