@sema-agent/server 7.1.0 → 7.3.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/README.md +3 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +6 -1
- package/dist/approval-ask-machine.d.ts +39 -0
- package/dist/approval-ask-machine.js +101 -0
- package/dist/approval-card.d.ts +244 -0
- package/dist/approval-card.js +237 -0
- package/dist/approval-deny-reasons.d.ts +56 -0
- package/dist/approval-deny-reasons.js +54 -0
- package/dist/approval-reconciler.d.ts +167 -0
- package/dist/approval-reconciler.js +307 -0
- package/dist/boot/coordinators.d.ts +1 -0
- package/dist/boot/coordinators.js +36 -3
- package/dist/boot/lexical-path-env.d.ts +14 -0
- package/dist/boot/lexical-path-env.js +116 -0
- package/dist/boot/org-memory.d.ts +8 -5
- package/dist/boot/org-memory.js +23 -12
- package/dist/boot/reapers.d.ts +34 -0
- package/dist/boot/reapers.js +198 -23
- package/dist/boot/resolve-spec.d.ts +16 -1
- package/dist/boot/resolve-spec.js +104 -46
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +19 -0
- package/dist/config-center/http-client.js +81 -32
- package/dist/config-types.d.ts +76 -7
- package/dist/config.d.ts +1 -0
- package/dist/config.js +134 -4
- package/dist/elicitation.d.ts +4 -0
- package/dist/elicitation.js +2 -2
- package/dist/http/routes/capabilities.js +18 -2
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +548 -16
- package/dist/http/routes/tasks.js +190 -12
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +124 -8
- package/dist/http/sse-log.d.ts +51 -0
- package/dist/http/sse-log.js +64 -0
- package/dist/http/wire-types.d.ts +20 -5
- package/dist/leader/wire.js +4 -0
- package/dist/main.js +7 -4
- package/dist/org-memory-admission.d.ts +2 -1
- package/dist/org-memory-admission.js +10 -4
- package/dist/parked-decide.js +5 -2
- package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
- package/dist/plugins/approval-ask-store-memory.js +299 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
- package/dist/plugins/approval-ask-store-sql.js +705 -0
- package/dist/plugins/background-agent-store-sql.js +20 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
- package/dist/plugins/checkpoint-store-sql.js +297 -16
- package/dist/plugins/local-checkpoint-store.d.ts +6 -5
- package/dist/plugins/local-checkpoint-store.js +4 -0
- package/dist/plugins/pg-pool.js +11 -0
- package/dist/plugins/store-backend.d.ts +18 -0
- package/dist/plugins/store-backend.js +10 -0
- package/dist/plugins/tidb-pool.js +27 -4
- package/dist/question.d.ts +18 -3
- package/dist/question.js +20 -6
- package/dist/runs.d.ts +16 -1
- package/dist/runs.js +61 -3
- package/dist/runtime-caps-resolver.d.ts +7 -1
- package/dist/runtime-caps-resolver.js +65 -3
- package/dist/runtime-governance.d.ts +11 -4
- package/dist/runtime-governance.js +16 -5
- package/dist/spec-fields.d.ts +4 -0
- package/dist/spec-fields.js +6 -0
- package/dist/task-settings.d.ts +35 -15
- package/dist/task-settings.js +19 -5
- package/dist/tool-approval.d.ts +296 -3
- package/dist/tool-approval.js +1066 -50
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.js +14 -1
- package/dist/trace/project.d.ts +55 -0
- package/dist/trace/project.js +135 -0
- package/package.json +4 -3
|
@@ -8,6 +8,7 @@ import { FileRunStore } from "./file-run-store.js";
|
|
|
8
8
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
9
9
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
10
10
|
import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
11
|
+
import { type ApprovalAskStore as ApprovalAskStoreType } from "./approval-ask-store-sql.js";
|
|
11
12
|
import { type SendFileLedger } from "./send-file-ledger.js";
|
|
12
13
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
13
14
|
import type { ServiceConfig } from "../config-types.js";
|
|
@@ -27,6 +28,9 @@ export type RunStore = TiDBRunStore | PgRunStore | FileRunStore;
|
|
|
27
28
|
* FileResumeAnchorStore is the FILE-backed local twin (P0.5 variant-1; survives restart). */
|
|
28
29
|
export type ResumeAnchorStore = TiDBResumeAnchorStore | PgResumeAnchorStore | FileResumeAnchorStore;
|
|
29
30
|
export type { ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
31
|
+
/** #151(design/172 v3.1 §3.0-§3.2 流内审批协议持久层):`ApprovalAskStore` 接口 + 行/补丁类型。
|
|
32
|
+
* 车3 刀 3a 起 `StoreBackend.approvalAsk()` 已在(见接口体的那条注)。 */
|
|
33
|
+
export type { ApprovalAskStore, AskRow, NewAskRow, AskTransitionPatch, DecideAskInput, DecideResult, ExpireResult, BindGateInput, BatchRow, AskDecision } from "./approval-ask-store-sql.js";
|
|
30
34
|
/** E6 SessionPolicyStore — core's interface PLUS the service `deleteBySession` (E21 purge; only the durable twins carry
|
|
31
35
|
* it, core's InMemory omits it → optional). tidb/pg = durable twins; local = core's InMemorySessionPolicyStore (works
|
|
32
36
|
* local — rules need no cloud-only infra). The shared env-gated equivalence suite keeps the twins byte-matched to InMemory. */
|
|
@@ -107,6 +111,20 @@ export interface StoreBackend {
|
|
|
107
111
|
/** Per-session per-toolName approval exemption ("本会话不再询问") — an approval MEMORY the F4
|
|
108
112
|
* ask-gate consults after deny/neverAuto. REQUIRED on all backends (works local, like resumeAnchor). */
|
|
109
113
|
approvalExemption(): ApprovalExemptionStore;
|
|
114
|
+
/** #151(design/172 §3.0-§3.2)流内审批协议持久层:每个流内工具调用一行的 ask 状态机 + 批行。
|
|
115
|
+
* REQUIRED on all backends — tidb/pg = SQL 双方言 twin;local = {@link InMemoryApprovalAskStore}。
|
|
116
|
+
*
|
|
117
|
+
* 🔴 local 形的代价必须在这里说清楚(而不是留给读者自己发现):InMemory 的 STREAM_PENDING/DECIDED/
|
|
118
|
+
* PARKING 全在两个进程内 Map 里 ⇒ **重启同时丢重放基准、丢已接受的决议、丢审计**——「一次真实的人类
|
|
119
|
+
* 批准凭空消失」和「对账便利丢失」不是一个量级。因此协议在 local 车道**默认不上场**:能力面
|
|
120
|
+
* (`streamApproval`)的判据带一条「ask 账必须是持久的」合取项,local 不报 true(诚实缺席,同
|
|
121
|
+
* `park 设施缺席 ⇒ 协议不上场` 的姿势)。要在 local 真上协议,先落 File 形(现成模子 =
|
|
122
|
+
* {@link FileApprovalExemptionStore}:core `AppendLog`/`readJsonlRecords`,内存索引 + 追加日志即可
|
|
123
|
+
* 满足单实例 boot-lock 下的 CAS),那时这里换成 File twin、能力面自然翻真。
|
|
124
|
+
*
|
|
125
|
+
* 无 backend 的 env-only worker 压根没有 `StoreBackend` ⇒ 协调器拿到 undefined askStore ⇒ 车2 的
|
|
126
|
+
* D1 逐字现行为(store 缺席 = 现行 `tool_approval` 活卡腿一字不变)。 */
|
|
127
|
+
approvalAsk(): ApprovalAskStoreType;
|
|
110
128
|
/** SendUserFile scope↔object ledger (multi-tenant list/revoke handle; the hashed key segment hides the
|
|
111
129
|
* mapping from URLs). REQUIRED on all backends (works local — one JSONL, like approvalExemption). */
|
|
112
130
|
sendFileLedger(): SendFileLedger;
|
|
@@ -18,6 +18,8 @@ import { LocalSessionStore } from "./local-session-store.js";
|
|
|
18
18
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
19
19
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
20
20
|
import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
21
|
+
import { TiDBApprovalAskStore, PgApprovalAskStore } from "./approval-ask-store-sql.js";
|
|
22
|
+
import { InMemoryApprovalAskStore } from "./approval-ask-store-memory.js";
|
|
21
23
|
import { TiDBSendFileLedger, PgSendFileLedger, FileSendFileLedger } from "./send-file-ledger.js";
|
|
22
24
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
23
25
|
import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-store-sql.js";
|
|
@@ -94,6 +96,7 @@ class TiDBBackend {
|
|
|
94
96
|
run() { return new TiDBRunStore(this.pool); }
|
|
95
97
|
resumeAnchor() { return new TiDBResumeAnchorStore(this.pool); }
|
|
96
98
|
approvalExemption() { return new TiDBApprovalExemptionStore(this.pool); }
|
|
99
|
+
approvalAsk() { return new TiDBApprovalAskStore(this.pool); } // #151 流内审批协议持久层(无状态 store,每次新建同兄弟)
|
|
97
100
|
sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
|
|
98
101
|
sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
|
|
99
102
|
fileSnapshot() { return new TiDBFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "tidb", this.pool), snapshotBoundsFromConfig(this.config)); }
|
|
@@ -127,6 +130,7 @@ class PgBackend {
|
|
|
127
130
|
run() { return new PgRunStore(this.pool); }
|
|
128
131
|
resumeAnchor() { return new PgResumeAnchorStore(this.pool); }
|
|
129
132
|
approvalExemption() { return new PgApprovalExemptionStore(this.pool); }
|
|
133
|
+
approvalAsk() { return new PgApprovalAskStore(this.pool); } // #151 流内审批协议持久层(SQL twin)
|
|
130
134
|
sendFileLedger() { return new PgSendFileLedger(this.pool); }
|
|
131
135
|
sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
|
|
132
136
|
fileSnapshot() { return new PgFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "pg", this.pool), snapshotBoundsFromConfig(this.config)); }
|
|
@@ -178,6 +182,8 @@ class LocalBackend {
|
|
|
178
182
|
sessionStore;
|
|
179
183
|
resumeAnchorStore;
|
|
180
184
|
approvalExemptionStore;
|
|
185
|
+
/** #151 —— 进程内易失(见 approvalAsk() 的注);File 形落地前它就是 local 车道的占位。 */
|
|
186
|
+
approvalAskStore = new InMemoryApprovalAskStore();
|
|
181
187
|
sendFileLedgerStore;
|
|
182
188
|
// SVC-2: FILE-backed durable journal (was core's InMemory — a TOC restart silently resolved
|
|
183
189
|
// resumeFromRunId to an empty journal → full live re-run). Now restart-durable on one box; crash-safe JSONL,
|
|
@@ -223,6 +229,10 @@ class LocalBackend {
|
|
|
223
229
|
run() { return this.runStore; }
|
|
224
230
|
resumeAnchor() { return this.resumeAnchorStore; } // singleton — state lives in the instance (mirrors run()/session())
|
|
225
231
|
approvalExemption() { return this.approvalExemptionStore; } // singleton — same posture
|
|
232
|
+
/** #151 流内审批协议持久层的 local 形。**必须**是 singleton:状态全在实例的两个 Map 里,每次
|
|
233
|
+
* new 一个就等于每个消费者各拿一份互不可见的账(而 SQL twin 天然共享一张表)。易失代价 +
|
|
234
|
+
* 「local 车道默认不上协议」的裁定见 StoreBackend.approvalAsk() 的接口注。 */
|
|
235
|
+
approvalAsk() { return this.approvalAskStore; }
|
|
226
236
|
sendFileLedger() { return this.sendFileLedgerStore; } // singleton — append fd lives in the instance
|
|
227
237
|
/** Durable HITL / plan-review parking on the LOCAL lane. Lazy singleton (the inner core store holds
|
|
228
238
|
* an append fd). taskId join ← FileRunStore; E21 owner guard ← LocalSessionStore.ownerOf (same root). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
|
+
import { TIDB_APPROVAL_ASK_STATEMENTS } from "./approval-ask-store-sql.js";
|
|
2
3
|
/**
|
|
3
4
|
* Shared TiDB (MySQL wire-compatible) connection pool for the durable Session center (L1).
|
|
4
5
|
* Owned by `main.ts`; closed on shutdown.
|
|
@@ -244,11 +245,28 @@ export const SCHEMA_STATEMENTS = [
|
|
|
244
245
|
-- D-1 TOCTOU binding guard is UNREACHABLE by the portal without it — it can then only do the unbound legacy
|
|
245
246
|
-- fallback). NULL on PRE-D-1 rows ⇒ that unbound fallback.
|
|
246
247
|
bound_input_hash VARCHAR(190) NULL,
|
|
247
|
-
-- pending_steer (design/80 D-A, durable steering)
|
|
248
|
-
--
|
|
249
|
-
--
|
|
250
|
-
-- Never touches status/resolve (design/80 inv #4).
|
|
248
|
+
-- pending_steer (design/80 D-A, durable steering):**队列化之前**的单座列,存 {text,trusted}。
|
|
249
|
+
-- core 5.14.0(#147 BREAKING)之后本服务**只读不写**它:存量行的那一座由 core readPendingSteerQueue
|
|
250
|
+
-- 折成 member 0,零迁移。NULL = 没有旧座。Never touches status/resolve (design/80 inv #4)。
|
|
251
251
|
pending_steer TEXT NULL,
|
|
252
|
+
-- pending_steer_queue:core 5.14.0(#147)的**有界有序队列**(PendingSteerEntry[] 的 JSON)。
|
|
253
|
+
-- 为什么队列另起一列、而不是把信封写回 pending_steer(codex 复审 2026-08-06 F3,采纳):滚动升级/
|
|
254
|
+
-- 回滚窗里旧副本仍在跑,它对 pending_steer 是**无条件覆盖**写 —— 队列若躺在同一列,旧副本接的下一条
|
|
255
|
+
-- steer 会把 5.14 排好的整队悄悄抹掉,正是 #147 要消灭的那种静默丢失换了个位置复发。分列之后:
|
|
256
|
+
-- 旧副本写它的单座 ⇒ 落 pending_steer,新读侧把它折成 member 0,**两边都活着**;旧副本读不到队列
|
|
257
|
+
-- 是 core 自己已披露的回滚代价(CHANGELOG:rolling back across the queue event loses steers),
|
|
258
|
+
-- 那半无法在店面消除,但**写侧互相清零**这半可以,且代价只是一个可空列。
|
|
259
|
+
-- 容量:core 的 PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000 字节是对 {pendingSteer,queue} 整信封的
|
|
260
|
+
-- 硬上界(越界 fail-loud steering.queue_full,绝不驱逐)⇒ 本列恒小于它,MySQL TEXT 的 65535 字节
|
|
261
|
+
-- 够用,**不升 LONGTEXT**(core 常量注里点名「under a MySQL TEXT column with UTF-8 headroom」)。
|
|
262
|
+
pending_steer_queue TEXT NULL,
|
|
263
|
+
-- pending_steer_rev:pending_steer_queue 列**自己**的乐观锁计数(与 resolve/reopen 的 rev 是两个轴,
|
|
264
|
+
-- 刻意不复用)。队列化之后 setPendingSteer 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须
|
|
265
|
+
-- 有 CAS 守着,否则两个并发 steer 各读到同一份队列、后写者把前者的条目整个盖掉。
|
|
266
|
+
-- 🔴 为什么不拿队列列的旧值当守卫:本表没有显式 COLLATE,库默认多为 *_ci —— 字符串 <=> 在大小写
|
|
267
|
+
-- 不敏感的排序规则下会把两份只差字母大小写的 JSON 判等,CAS 假命中 = 丢一条指令。整数列没有这个面。
|
|
268
|
+
-- **绝不**用它做 resolve 的 OCC:bump 它不影响 core 的 rev 语义。
|
|
269
|
+
pending_steer_rev BIGINT NOT NULL DEFAULT 0,
|
|
252
270
|
-- risk_descriptor (design/80 riskDescriptor inbox): core's INERT CheckpointGate.riskDescriptor ({severity
|
|
253
271
|
-- 1-5, axes, toolName, redacted summary, touchedPaths}), stamped at put() so listPending can surface it +
|
|
254
272
|
-- triage-sort the supervisor inbox by severity DESC — WITHOUT parsing the checkpoint blob per row.
|
|
@@ -602,6 +620,11 @@ export const SCHEMA_STATEMENTS = [
|
|
|
602
620
|
pool VARCHAR(32) NOT NULL,
|
|
603
621
|
PRIMARY KEY (pool)
|
|
604
622
|
)`,
|
|
623
|
+
// #151(design/172)流内审批协议持久层的两张表。真源 = approval-ask-store-sql.ts 的
|
|
624
|
+
// `TIDB_APPROVAL_ASK_STATEMENTS`(那里与 PG twin 并排,方言差异一眼可对);展开进这个数组是为了让它们
|
|
625
|
+
// 跟着中央 `ensureSchema` 在 **named-lock 的那条 conn** 上建 —— 把 pool 级 ensure 函数塞进执行循环
|
|
626
|
+
// 会绕开 DDL 串行化。挂进来同时也是「这张表算生产 schema」的声明(基线导出按定义只收录被执行的语句)。
|
|
627
|
+
...TIDB_APPROVAL_ASK_STATEMENTS,
|
|
605
628
|
];
|
|
606
629
|
/** Named MySQL/TiDB advisory lock that serializes the whole `ensureSchema` DDL run (see below). */
|
|
607
630
|
export const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
|
package/dist/question.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface QuestionFrame {
|
|
|
11
11
|
/** "question_complete" only: whether the human answered (`answered`) or the ask was released unanswered
|
|
12
12
|
* (`unanswered` — ttl/abort → the model got the headless default). Cosmetic dialog-dismiss. */
|
|
13
13
|
outcome?: "answered" | "unanswered";
|
|
14
|
+
/** Server-signed emit timestamp (additive, [2805]§五→[2806]). Server-minted side-frame ⇒ no core-stream
|
|
15
|
+
* `eventId` to carry (absence is honest); this is the frame's own time coordinate. */
|
|
16
|
+
serverNowMs?: number;
|
|
14
17
|
}
|
|
15
18
|
/** The per-run context `onQuestion` recovers via ALS. `emit` delivers a frame to the run's live stream; `abortSignal`
|
|
16
19
|
* (the run's own cancel signal) releases a question parked awaiting a human when the run aborts. */
|
|
@@ -19,6 +22,14 @@ export interface QuestionRunContext {
|
|
|
19
22
|
owner: string | null;
|
|
20
23
|
emit: (frame: QuestionFrame) => void | Promise<void>;
|
|
21
24
|
abortSignal?: AbortSignal;
|
|
25
|
+
/** OPTIONAL reachability predicate for this leg's delivery face, consulted by {@link QuestionCoordinator.hasLiveContext}
|
|
26
|
+
* (the park-vs-live adjudication probe), NOT by `question` itself. CONSTRAINT: a leg whose face can go away WHILE the
|
|
27
|
+
* leg keeps running MUST supply it — `/v1/tasks/stream` under `x-detach-on-disconnect: true` keeps executing inside
|
|
28
|
+
* this ALS scope after the client left, and `emit` there writes to a dead socket and is silently dropped (question
|
|
29
|
+
* frames have no durable ledger twin, unlike SendUserFile's ledgerSink). Without the predicate the adjudication would
|
|
30
|
+
* read "live" and hand the leg to a face nobody can reach. ABSENT ⇒ always reachable, which is exactly right for the
|
|
31
|
+
* background leg (emit appends to the durable events tail, readable whenever a client attaches). */
|
|
32
|
+
deliverable?: () => boolean;
|
|
22
33
|
}
|
|
23
34
|
export interface QuestionThrottle {
|
|
24
35
|
/** Max concurrent in-flight questions per run leg (parallel tool calls can each ask). Breach ⇒ headless default. */
|
|
@@ -67,10 +78,14 @@ export declare class QuestionCoordinator {
|
|
|
67
78
|
};
|
|
68
79
|
/** Test/observability hook: number of currently-parked questions. */
|
|
69
80
|
pendingCount(): number;
|
|
70
|
-
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion
|
|
71
|
-
*
|
|
81
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
|
|
82
|
+
* 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
|
|
72
83
|
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
73
|
-
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
84
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
85
|
+
* 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
|
|
86
|
+
* 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
|
|
87
|
+
* 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
|
|
88
|
+
* fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
|
|
74
89
|
hasLiveContext(): boolean;
|
|
75
90
|
private countersFor;
|
|
76
91
|
}
|
package/dist/question.js
CHANGED
|
@@ -158,7 +158,7 @@ export class QuestionCoordinator {
|
|
|
158
158
|
// fire-and-forget append can commit out of seq order and be skipped by the monotonic events-tail cursor). A FAILED
|
|
159
159
|
// emit headless-defaults (nobody can answer) and must NOT burn the per-run total.
|
|
160
160
|
try {
|
|
161
|
-
await ctx.emit({ type: "question", questionId: id, questions });
|
|
161
|
+
await ctx.emit({ type: "question", questionId: id, questions, serverNowMs: Date.now() });
|
|
162
162
|
rc.total += 1;
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
@@ -168,7 +168,7 @@ export class QuestionCoordinator {
|
|
|
168
168
|
// Completion breadcrumb (dialog dismiss) — FIRE-AND-FORGET so a slow durable append can never delay returning the
|
|
169
169
|
// answer to core. Best-effort + ordering-uncritical (the shell already has the answer via respond).
|
|
170
170
|
void Promise.resolve()
|
|
171
|
-
.then(() => ctx.emit({ type: "question_complete", questionId: id, outcome: lastOutcome }))
|
|
171
|
+
.then(() => ctx.emit({ type: "question_complete", questionId: id, outcome: lastOutcome, serverNowMs: Date.now() }))
|
|
172
172
|
.catch(() => undefined);
|
|
173
173
|
return answer;
|
|
174
174
|
};
|
|
@@ -192,12 +192,26 @@ export class QuestionCoordinator {
|
|
|
192
192
|
pendingCount() {
|
|
193
193
|
return this.pending.size;
|
|
194
194
|
}
|
|
195
|
-
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion
|
|
196
|
-
*
|
|
195
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
|
|
196
|
+
* 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
|
|
197
197
|
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
198
|
-
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
198
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
199
|
+
* 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
|
|
200
|
+
* 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
|
|
201
|
+
* 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
|
|
202
|
+
* fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
|
|
199
203
|
hasLiveContext() {
|
|
200
|
-
|
|
204
|
+
const ctx = this.als.getStore();
|
|
205
|
+
if (ctx === undefined)
|
|
206
|
+
return false;
|
|
207
|
+
if (ctx.deliverable === undefined)
|
|
208
|
+
return true;
|
|
209
|
+
try {
|
|
210
|
+
return ctx.deliverable();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
201
215
|
}
|
|
202
216
|
countersFor(taskId) {
|
|
203
217
|
let rc = this.counters.get(taskId);
|
package/dist/runs.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Metrics } from "./observability/metrics.js";
|
|
|
5
5
|
import type { ModelUsageTracker, PromptManifestTracker } from "./budget.js";
|
|
6
6
|
import type { ElicitationCoordinator } from "./elicitation.js";
|
|
7
7
|
import type { QuestionCoordinator } from "./question.js";
|
|
8
|
+
import { type ToolApprovalCoordinator } from "./tool-approval.js";
|
|
8
9
|
import { type FleetRunPublisher } from "./fleet/fleet-bus.js";
|
|
9
10
|
import { type WorkflowCompletionInbox } from "./orchestration/workflow-completion-inbox.js";
|
|
10
11
|
import type { VerifyRoundsSpec } from "./http/verify-rounds.js";
|
|
@@ -250,7 +251,21 @@ completionDiagLog?: (msg: string, meta?: Record<string, unknown>) => void,
|
|
|
250
251
|
sendUserFile?: SendUserFileEmitter,
|
|
251
252
|
/** [998]② the shared prompt-manifest accumulator (tracer records at prepare; this leg drains the pending
|
|
252
253
|
* record into a durable `prompt_assembled` event — turns/stream read it). undefined ⇒ not wired. */
|
|
253
|
-
promptManifests?: PromptManifestTracker
|
|
254
|
+
promptManifests?: PromptManifestTracker,
|
|
255
|
+
/**
|
|
256
|
+
* #151 车3 刀 3b:**bg 腿的流内审批 ctx**(design/172 §4.3(b))。今天为止这条腿连 approval ALS 都没有
|
|
257
|
+
* ——`deps.onAsk` 拿不到 ctx ⇒ 恒 `"unavailable"` ⇒ durable park,一张卡都不产。接上之后,bg run 的
|
|
258
|
+
* 权限 ask 走本腿的 **durable events tail**(`GET /v1/runs/:id/events`,唯一带 SSE `id:` 的面)。
|
|
259
|
+
*
|
|
260
|
+
* `streamApprovalOn` = 协议上场判据(`resolveStreamApprovalGate`,由路由层求值后传进来 —— 本函数是纯
|
|
261
|
+
* 执行腿,不该自己读 backend/config)。为假 ⇒ 只接既有四键 ctx(呈卡口/腿轴都不接),本腿字节逐字不变。
|
|
262
|
+
*/
|
|
263
|
+
approval?: {
|
|
264
|
+
coordinator: ToolApprovalCoordinator;
|
|
265
|
+
streamApprovalOn: boolean;
|
|
266
|
+
windowMs: number;
|
|
267
|
+
windowMarginMs: number;
|
|
268
|
+
}): Promise<void>;
|
|
254
269
|
/** How often a running instance refreshes its run's updated_at (liveness, independent of events).
|
|
255
270
|
* Must stay strictly below `runStaleSec` (asserted at startup) or the reaper would race live runs. */
|
|
256
271
|
export declare const HEARTBEAT_MS = 30000;
|
package/dist/runs.js
CHANGED
|
@@ -3,6 +3,7 @@ import { withPrincipal } from "./observability/principal-context.js";
|
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
5
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
|
+
import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
|
|
6
7
|
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
7
8
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
8
9
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
@@ -331,7 +332,16 @@ completionDiagLog,
|
|
|
331
332
|
sendUserFile,
|
|
332
333
|
/** [998]② the shared prompt-manifest accumulator (tracer records at prepare; this leg drains the pending
|
|
333
334
|
* record into a durable `prompt_assembled` event — turns/stream read it). undefined ⇒ not wired. */
|
|
334
|
-
promptManifests
|
|
335
|
+
promptManifests,
|
|
336
|
+
/**
|
|
337
|
+
* #151 车3 刀 3b:**bg 腿的流内审批 ctx**(design/172 §4.3(b))。今天为止这条腿连 approval ALS 都没有
|
|
338
|
+
* ——`deps.onAsk` 拿不到 ctx ⇒ 恒 `"unavailable"` ⇒ durable park,一张卡都不产。接上之后,bg run 的
|
|
339
|
+
* 权限 ask 走本腿的 **durable events tail**(`GET /v1/runs/:id/events`,唯一带 SSE `id:` 的面)。
|
|
340
|
+
*
|
|
341
|
+
* `streamApprovalOn` = 协议上场判据(`resolveStreamApprovalGate`,由路由层求值后传进来 —— 本函数是纯
|
|
342
|
+
* 执行腿,不该自己读 backend/config)。为假 ⇒ 只接既有四键 ctx(呈卡口/腿轴都不接),本腿字节逐字不变。
|
|
343
|
+
*/
|
|
344
|
+
approval) {
|
|
335
345
|
const startedAt = Date.now();
|
|
336
346
|
metrics?.addGauge("runs_active", 1);
|
|
337
347
|
fleetPublisher?.onStart(); // MF-Fleet: the row goes live (running) the moment the background run starts
|
|
@@ -372,6 +382,21 @@ promptManifests) {
|
|
|
372
382
|
// The owning principal is this run's owner — thread it as the single-DB-fleet null-safe owner guard on every
|
|
373
383
|
// run-store mutation/poll below (the run row was created with owner = principal ?? null).
|
|
374
384
|
const owner = principal ?? null;
|
|
385
|
+
// #151 车3 刀 3b(§7.3 + §8.3):本腿的审批装配裁定走**三腿共用**的那一份判据。
|
|
386
|
+
// 🔴 codex 交叉复审 F3(2026-08-06 真 finding)的修:此前本腿**无条件**包 ALS,只把新协议的口与轴挂在
|
|
387
|
+
// `streamApprovalOn` 后面 —— 于是 ①开关关时本腿的行为变了(此前它根本没有 approval ctx,每只 ask 恒
|
|
388
|
+
// `"unavailable"` ⇒ 恒 park;包上之后变成活卡并等满窗,A-1 当场破),②窗=0 在本腿上不生效(照样落行、
|
|
389
|
+
// 注册资源、发帧,再等一个 0ms 的表)。现在两件都由下面这一个裁定统一管。
|
|
390
|
+
// 取值点 = **本腿开始执行的时刻** = `runInBackground` 进入(路由层 `void runInBackground(...)` 之后
|
|
391
|
+
// 立刻),不是请求到达时刻(§3.3:按任务创建至今算会系统性算错)。
|
|
392
|
+
const approvalLeg = resolveApprovalLeg({
|
|
393
|
+
streamApprovalOn: approval?.streamApprovalOn === true,
|
|
394
|
+
windowMs: approval?.windowMs ?? 0,
|
|
395
|
+
windowMarginMs: approval?.windowMarginMs ?? 0,
|
|
396
|
+
...(typeof spec.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: spec.limits.maxWalltimeMs } : {}),
|
|
397
|
+
nowMonotonicMs: performance.now(),
|
|
398
|
+
});
|
|
399
|
+
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
375
400
|
const heartbeat = setInterval(() => {
|
|
376
401
|
// 鲁棒性批5 A5(2026-08-05):与下面 cancel/preempt 两条兄弟同族——此前裸吞错,store 持续故障期间本 run
|
|
377
402
|
// 的心跳每拍静默落空、运维零信号,直到某个副本的 reapStale 把它误判死亡(心跳失败正是那个误判的前兆)。
|
|
@@ -476,6 +501,15 @@ promptManifests) {
|
|
|
476
501
|
const { type, ...rest } = frame;
|
|
477
502
|
return append(type, rest);
|
|
478
503
|
};
|
|
504
|
+
// #151 车3 刀 3b(design/172 §4.3(b)):同款 durable-append emit,服务**审批**帧 —— 既有
|
|
505
|
+
// `tool_approval`/`tool_approval_complete` 走它,新协议的 `approval_request` 也走它(同一条投递面、
|
|
506
|
+
// 两种帧,§4.3(a) 的「不新增出口」在这条腿上的落实)。`type` 是 SSE 事件名,其余是 data。
|
|
507
|
+
// 撤卡帧(`approval_revoke`)**不接**:它是 live-only 语义(不进账本 —— reaper/收敛器那类调用点没有
|
|
508
|
+
// seq 可分配),而本腿的唯一投递面就是账本;丢帧的结构补偿是重连 preamble 的全量对账基准。
|
|
509
|
+
const approvalEmit = (frame) => {
|
|
510
|
+
const { type, ...rest } = frame;
|
|
511
|
+
return append(type, rest);
|
|
512
|
+
};
|
|
479
513
|
try {
|
|
480
514
|
// P1 ①② follow-on (core): drain this session's async-workflow completion inbox at the START of the
|
|
481
515
|
// background leg (before ANY branch — verify/cascade/plain-stream all get it; parity with the sync leg's
|
|
@@ -742,10 +776,34 @@ promptManifests) {
|
|
|
742
776
|
const withQuestion = () => question
|
|
743
777
|
? question.runWithContext({ taskId, owner: elicitOwner ?? owner, emit: questionEmit, abortSignal: cancelCtrl.signal }, withElicit)
|
|
744
778
|
: withElicit();
|
|
779
|
+
// #151 车3 刀 3b:审批 ALS —— 本腿**首次**拿到卡面(此前 `deps.onAsk` 在这条腿上恒无 ctx ⇒
|
|
780
|
+
// `"unavailable"` ⇒ 恒 park)。嵌套序同样无关(各自独立 ALS)。
|
|
781
|
+
// 🔴 F3 修:包不包 ALS 由 `approvalLeg` 决定,不再是「协调器在场就包」。
|
|
782
|
+
// · `active === false`(协议没上场)⇒ 不包 ⇒ `deps.onAsk` 找不到 ctx ⇒ `"unavailable"` ⇒ core
|
|
783
|
+
// durable park —— 这正是本腿在协议之前的逐字行为(A-1)。
|
|
784
|
+
// · `windowZero`(运维关窗 / 贴 deadline)⇒ 同样不包。本腿没有 per-task `spec.onAsk` 装配点,
|
|
785
|
+
// 「不包 ALS」就是 sync 腿那个 immediate-unavailable 闭包在这条腿上的**等价形**(§8.2 已亲证:
|
|
786
|
+
// `approverUnavailable === true` 与「onAsk 缺席」在 park 结局上逐字等价)。
|
|
787
|
+
const withApproval = () => approval && approvalLeg.active && !approvalLeg.windowZero
|
|
788
|
+
? approval.coordinator.runWithContext({
|
|
789
|
+
taskId,
|
|
790
|
+
owner: elicitOwner ?? owner,
|
|
791
|
+
emit: approvalEmit,
|
|
792
|
+
abortSignal: cancelCtrl.signal,
|
|
793
|
+
...(spec.sessionId ? { sessionId: spec.sessionId } : {}),
|
|
794
|
+
// 同一条 durable tail(§4.3(b))。经共用的投递口工厂:账本 append 失败 ⇒ 如实算「这一路没
|
|
795
|
+
// 接下」(本腿只有这一个 sink,于是=整条没送到),协调器据此走 park 而不是挂到窗到期(R2-1)。
|
|
796
|
+
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
797
|
+
// bg run 的这条腿恒是**首腿**(park→resume 走的是 server.ts 的 resume 腿,那条自己带
|
|
798
|
+
// token 摘要);空串是首腿的真值,不是「未知」。
|
|
799
|
+
legKey: "",
|
|
800
|
+
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
801
|
+
}, withQuestion)
|
|
802
|
+
: withQuestion();
|
|
745
803
|
// SendUserFile 帧与 question 帧同投递面(durable events tail,web 渲染文件卡);嵌套序无关(ALS 互不相交)。
|
|
746
804
|
return sendUserFile
|
|
747
|
-
? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } },
|
|
748
|
-
:
|
|
805
|
+
? sendUserFile.runWithContext({ taskId, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); } }, withApproval)
|
|
806
|
+
: withApproval();
|
|
749
807
|
});
|
|
750
808
|
// Cancelled mid-stream and the generator ended WITHOUT a terminal event (abort can end the stream cleanly
|
|
751
809
|
// rather than throwing) — finalize cancelled here so the run isn't left "running" for the reaper to catch.
|
|
@@ -30,6 +30,9 @@ import type { RuntimeCaps } from "@sema-agent/core";
|
|
|
30
30
|
import type { EntitlementRuntimeCaps } from "@sema-agent/registry-core";
|
|
31
31
|
import { fetchPrincipalCaps, type ExecutionRuling } from "./config-center/facade.js";
|
|
32
32
|
import type { ScenarioRuling } from "./capabilities/scenarios.js";
|
|
33
|
+
import { type Logger } from "./observability/logger.js";
|
|
34
|
+
/** test-only: reset the once-per-process trace so each test observes a fresh "first hit". */
|
|
35
|
+
export declare function resetExecutionLaneDefaultTraceForTest(): void;
|
|
33
36
|
export interface EntitlementsResolverOpts {
|
|
34
37
|
baseUrl: string;
|
|
35
38
|
token: string;
|
|
@@ -99,8 +102,11 @@ export interface PrincipalEntitlementsClient {
|
|
|
99
102
|
* allowlist + this worker's lane (machine code on the wire, prose stays with the shell/web).
|
|
100
103
|
* No ruling / required=false / caps blip ⇒ pass (fail-open + audit). `allowedLanes` is an
|
|
101
104
|
* OPEN domain — unknown names simply never match this worker's lane.
|
|
105
|
+
*
|
|
106
|
+
* `logger` (optional, defaults to this module's `defaultLogger`) is an injection seam purely for the
|
|
107
|
+
* once-per-process default trace below (test observability) — it does not change enforcement.
|
|
102
108
|
*/
|
|
103
|
-
export declare function gateExecutionLane(ruling: ExecutionRuling | undefined, ownLane: string): void;
|
|
109
|
+
export declare function gateExecutionLane(ruling: ExecutionRuling | undefined, ownLane: string, logger?: Logger): void;
|
|
104
110
|
/**
|
|
105
111
|
* Build the per-principal caps client: `resolveRuntimeCaps` matches core's seam signature
|
|
106
112
|
* `(principal) => Promise<RuntimeCaps | undefined>`; `scenarioRuling` rides the SAME fetch/cache/in-flight
|
|
@@ -2,6 +2,48 @@ import { fetchPrincipalCaps } from "./config-center/facade.js";
|
|
|
2
2
|
import { HttpError } from "./security.js";
|
|
3
3
|
import { createLogger } from "./observability/logger.js";
|
|
4
4
|
const defaultLogger = createLogger();
|
|
5
|
+
/** #157 / [2762]§五(sema-comms `audits/failopen-governance-176.md` §7 "已知在办 1 条"):module-scope,
|
|
6
|
+
* once-per-process record of which {@link gateExecutionLane} fail-open DEFAULT form has already been traced.
|
|
7
|
+
* Two forms are counted SEPARATELY (both currently resolve to the same "pass" default, but they are different
|
|
8
|
+
* facts about the world — a caller must be able to tell "we never got a ruling for this principal" apart from
|
|
9
|
+
* "center ruled, and explicitly said not-required"):
|
|
10
|
+
* - `"no_ruling"` — `ruling === undefined` (no principal / center absent-or-blip / older center missing the key).
|
|
11
|
+
* - `"required_false"` — a ruling WAS resolved and it explicitly says `required: false`.
|
|
12
|
+
* Each form logs ONCE per process at first hit (a debug line — this is an expected, non-alarming default, not a
|
|
13
|
+
* warning); every subsequent hit of the SAME form only increments the counter (no repeat log line — this is a
|
|
14
|
+
* per-task gate, so without the throttle a busy deployment would log once per task forever). Pure record-keeping:
|
|
15
|
+
* never changes {@link gateExecutionLane}'s pass/deny outcome. */
|
|
16
|
+
const executionLaneDefaultTrace = new Map();
|
|
17
|
+
/** test-only: reset the once-per-process trace so each test observes a fresh "first hit". */
|
|
18
|
+
export function resetExecutionLaneDefaultTraceForTest() {
|
|
19
|
+
executionLaneDefaultTrace.clear();
|
|
20
|
+
}
|
|
21
|
+
function traceExecutionLaneDefault(form, ownLane, logger) {
|
|
22
|
+
const hitCount = (executionLaneDefaultTrace.get(form) ?? 0) + 1;
|
|
23
|
+
executionLaneDefaultTrace.set(form, hitCount);
|
|
24
|
+
if (hitCount > 1)
|
|
25
|
+
return; // subsequent hits: silently counted only (see map above), no repeat log line
|
|
26
|
+
try {
|
|
27
|
+
// codex 交叉复审第二轮真发现(2026-08-05):a throwing `logger.debug` must NEVER turn this pure
|
|
28
|
+
// observability step into gateExecutionLane itself throwing — that would silently convert a fail-open
|
|
29
|
+
// PASS into an uncaught exception, the exact inversion this module's whole design fights elsewhere
|
|
30
|
+
// (see the FAIL-CLOSED/FAIL-OPEN posture doc above). The hit-count above already advanced, so a retry
|
|
31
|
+
// of the SAME form stays silent rather than storming — the trade-off is "lose one trace line", not
|
|
32
|
+
// "lose one task admission".
|
|
33
|
+
logger.debug("execution_lane_default_applied", {
|
|
34
|
+
dimension: "execution_lane",
|
|
35
|
+
form,
|
|
36
|
+
ownLane,
|
|
37
|
+
default: "pass",
|
|
38
|
+
note: form === "no_ruling"
|
|
39
|
+
? "no execution-lane ruling was ever resolved for this principal (no principal / center absent-or-blip / older center without the key)"
|
|
40
|
+
: "center resolved a ruling and explicitly set required=false",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// tracing must never affect the fail-open admission outcome — see comment above.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
5
47
|
/** Map center's `EntitlementRuntimeCaps` → the subset core's engine enforces. `allowUltracode` (shell/UX) is
|
|
6
48
|
* dropped on purpose (not a core primitive); `allowWorkflows` / `forceDurableGate` / `allowFork` are the three the
|
|
7
49
|
* engine enforces. Returns undefined when no enforceable cap is set (= no restriction). FAIL-CLOSED defense-in-depth:
|
|
@@ -60,10 +102,19 @@ export function scopedTokenNeedsWorker(token, worker) {
|
|
|
60
102
|
* allowlist + this worker's lane (machine code on the wire, prose stays with the shell/web).
|
|
61
103
|
* No ruling / required=false / caps blip ⇒ pass (fail-open + audit). `allowedLanes` is an
|
|
62
104
|
* OPEN domain — unknown names simply never match this worker's lane.
|
|
105
|
+
*
|
|
106
|
+
* `logger` (optional, defaults to this module's `defaultLogger`) is an injection seam purely for the
|
|
107
|
+
* once-per-process default trace below (test observability) — it does not change enforcement.
|
|
63
108
|
*/
|
|
64
|
-
export function gateExecutionLane(ruling, ownLane) {
|
|
65
|
-
if (!ruling
|
|
109
|
+
export function gateExecutionLane(ruling, ownLane, logger = defaultLogger) {
|
|
110
|
+
if (!ruling) {
|
|
111
|
+
traceExecutionLaneDefault("no_ruling", ownLane, logger);
|
|
66
112
|
return;
|
|
113
|
+
}
|
|
114
|
+
if (!ruling.required) {
|
|
115
|
+
traceExecutionLaneDefault("required_false", ownLane, logger);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
67
118
|
if (!ruling.allowedLanes.includes(ownLane)) {
|
|
68
119
|
throw new HttpError(403, `this worker's execution lane "${ownLane}" is not permitted for this principal (execution policy)`, {
|
|
69
120
|
code: "execution_lane_not_allowed",
|
|
@@ -149,7 +200,18 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
149
200
|
return entry;
|
|
150
201
|
}
|
|
151
202
|
catch (err) {
|
|
152
|
-
|
|
203
|
+
// codex 交叉复审第三轮真发现(2026-08-05):a poison rejection reason (throwing `Symbol.toPrimitive`/
|
|
204
|
+
// `toString`) must not make THIS normalization line itself throw — this catch block exists to
|
|
205
|
+
// GUARANTEE the documented FAIL-CLOSED degrade below runs; if normalizing `err` throws, that
|
|
206
|
+
// guarantee is exactly what breaks (resolveRuntimeCaps/scenarioRuling/executionRuling would reject
|
|
207
|
+
// instead of resolving to their fail-closed/fail-open defaults on a degraded center response).
|
|
208
|
+
let e;
|
|
209
|
+
try {
|
|
210
|
+
e = err instanceof Error ? err : new Error(String(err));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
e = new Error("center fetch failed with an unstringifiable rejection reason");
|
|
214
|
+
}
|
|
153
215
|
// Caps face FAIL CLOSED: deny the OPTIONAL amplification caps (workflows + fork); never force the durable
|
|
154
216
|
// gate on error. Scenario face FAIL OPEN: `scenario` stays undefined (no governance — never pin users to
|
|
155
217
|
// the default scenario on a center blip; deliberate posture split, see PrincipalEntitlementsClient). Cache the
|
|
@@ -35,10 +35,17 @@ export type CommandRule = {
|
|
|
35
35
|
* fail-open(那个教训的解=引用常量,不是折叠)。 */
|
|
36
36
|
export declare function isDelegationToolName(name: string): boolean;
|
|
37
37
|
/** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter —
|
|
38
|
-
* not just the name predicate — is pinned by tests):
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation
|
|
39
|
+
* tool at all.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly`
|
|
42
|
+
* (`dist/agents/subagent.js:1501` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume
|
|
43
|
+
* 腿 `:405` 同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以
|
|
44
|
+
* 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松):
|
|
45
|
+
* 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将
|
|
46
|
+
* 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。
|
|
47
|
+
*
|
|
48
|
+
* Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */
|
|
42
49
|
export declare function stripDelegationTools<T extends {
|
|
43
50
|
name: string;
|
|
44
51
|
}>(tools: readonly T[]): T[];
|
|
@@ -32,10 +32,17 @@ export function isDelegationToolName(name) {
|
|
|
32
32
|
return name === DEFAULT_SUBAGENT_TOOL_NAME;
|
|
33
33
|
}
|
|
34
34
|
/** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter —
|
|
35
|
-
* not just the name predicate — is pinned by tests):
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation
|
|
36
|
+
* tool at all.
|
|
37
|
+
*
|
|
38
|
+
* 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly`
|
|
39
|
+
* (`dist/agents/subagent.js:1501` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume
|
|
40
|
+
* 腿 `:405` 同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以
|
|
41
|
+
* 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松):
|
|
42
|
+
* 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将
|
|
43
|
+
* 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。
|
|
44
|
+
*
|
|
45
|
+
* Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */
|
|
39
46
|
export function stripDelegationTools(tools) {
|
|
40
47
|
const kept = tools.filter((t) => !isDelegationToolName(t.name));
|
|
41
48
|
return kept.length === tools.length ? tools : kept;
|
|
@@ -232,7 +239,11 @@ export function applyRuntimeGovernance(base, governance) {
|
|
|
232
239
|
const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
|
|
233
240
|
? overrides.shellGate
|
|
234
241
|
: governance.manualModeShellGate;
|
|
235
|
-
// base 已更严
|
|
242
|
+
// base 已更严 ⇒ 省略,让 base 原样保留(省略=行为等价,不 throw)。当前**装配上不可达、纯防御**:
|
|
243
|
+
// resolveSpec 的 spec 字面量从不写 shellGate,governance 是本进程里第一个产它的人(SUP 路由姿态在其**后**
|
|
244
|
+
// 才叠),所以每次真实调用的 base.shellGate 都是缺席。若将来有人给 base 种上更严的值,这里省略而不是让
|
|
245
|
+
// tightenTaskSpec 因「override 更松」throw——candidate 已是 autonomy 派生值与旋钮的较大者,省略它不会
|
|
246
|
+
// 丢掉 autonomy 那一半(autonomy 只产 "always",即最高 rank,永远不会落进这一支)。
|
|
236
247
|
if (SHELL_GATE_RANK[candidate] >= SHELL_GATE_RANK[base.shellGate ?? "off"])
|
|
237
248
|
overrides.shellGate = candidate;
|
|
238
249
|
else
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -85,6 +85,10 @@ export declare function toolNameListFromBody(raw: unknown): string[] | undefined
|
|
|
85
85
|
* ⇒ 无租户门(deferTools 同姿势);全委托树继承由 core 管。缺省不挂键(引擎缺省 simple,byte-compat)。
|
|
86
86
|
* DEFENSIVE(resume 重放不过 HTTP 400 门):非法值 ⇒ undefined;提交面另有 fail-loud 400(邻居姿势)。 */
|
|
87
87
|
export declare function promptProfileFromBody(raw: unknown): "simple" | "classic" | undefined;
|
|
88
|
+
/** [2856]②(core 5.14.0 `TaskSpec.toolMaterializeStrategy`):deferred 工具激活后的 schema 供给策略。
|
|
89
|
+
* 与 {@link promptProfileFromBody} 同姿势的**防御性归一**——提交面的 fail-loud 400 在 HTTP 门,这里
|
|
90
|
+
* 只负责 resume 重放存量 body 时把不认识的值静默 DROP(回落 core 的 env/缺省链),而不是把它带上 spec。 */
|
|
91
|
+
export declare function toolMaterializeStrategyFromBody(raw: unknown): "static" | "swap" | undefined;
|
|
88
92
|
/** [854]④ — 每键的运营方上限旋钮(env,缺省不设=不封顶)。 */
|
|
89
93
|
export interface TaskLimitCaps {
|
|
90
94
|
/** TASK_TIMEOUT_MAX_SEC(env 名不动,仍按**秒**配置;core 5.8.0 起封顶对象是 limits.maxWalltimeMs,
|
package/dist/spec-fields.js
CHANGED
|
@@ -351,6 +351,12 @@ export function toolNameListFromBody(raw) {
|
|
|
351
351
|
export function promptProfileFromBody(raw) {
|
|
352
352
|
return raw === "simple" || raw === "classic" ? raw : undefined;
|
|
353
353
|
}
|
|
354
|
+
/** [2856]②(core 5.14.0 `TaskSpec.toolMaterializeStrategy`):deferred 工具激活后的 schema 供给策略。
|
|
355
|
+
* 与 {@link promptProfileFromBody} 同姿势的**防御性归一**——提交面的 fail-loud 400 在 HTTP 门,这里
|
|
356
|
+
* 只负责 resume 重放存量 body 时把不认识的值静默 DROP(回落 core 的 env/缺省链),而不是把它带上 spec。 */
|
|
357
|
+
export function toolMaterializeStrategyFromBody(raw) {
|
|
358
|
+
return raw === "static" || raw === "swap" ? raw : undefined;
|
|
359
|
+
}
|
|
354
360
|
/** core 5.8.0 时限重构的退役键(fresh-submit 在 HTTP 门 400 点名;resume 重放在 normalize 层 DROP)。 */
|
|
355
361
|
const RETIRED_LIMIT_KEYS = ["timeoutSec", "deadlineNudge", "callCapByDeadline", "gracefulFinalize"];
|
|
356
362
|
/** 在场的退役键(normalizeLimits/resolveTaskLimits 共用判据源:留痕事件的 droppedKeys 字段)。 */
|